feat(tasks): Refactor series_reconstruct_task and add _stamp_derived_dataset for improved dataset handling
This commit is contained in:
+7
-13
@@ -86,7 +86,6 @@ from generic.models import CimarCase
|
|||||||
from rad.settings import REMOTE_URL, CIMAR_USERNAME, CIMAR_PASSWORD
|
from rad.settings import REMOTE_URL, CIMAR_USERNAME, CIMAR_PASSWORD
|
||||||
from helpers.cimar import CimarAPI, NotFoundError
|
from helpers.cimar import CimarAPI, NotFoundError
|
||||||
from pydicom.uid import generate_uid
|
from pydicom.uid import generate_uid
|
||||||
from datetime import datetime
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@@ -392,9 +391,6 @@ def series_reconstruct_task(
|
|||||||
recon_series = atlas_views._create_series_derivative(series, f"Recon {plane_norm.title()} ({source_plane.title()} src)")
|
recon_series = atlas_views._create_series_derivative(series, f"Recon {plane_norm.title()} ({source_plane.title()} src)")
|
||||||
recon_series.series_instance_uid = generate_uid()
|
recon_series.series_instance_uid = generate_uid()
|
||||||
recon_series.save(update_fields=["series_instance_uid"])
|
recon_series.save(update_fields=["series_instance_uid"])
|
||||||
now = datetime.utcnow()
|
|
||||||
date_str = now.strftime("%Y%m%d")
|
|
||||||
time_str = now.strftime("%H%M%S.%f")[:13]
|
|
||||||
|
|
||||||
for idx in range(int(cfg["count"])):
|
for idx in range(int(cfg["count"])):
|
||||||
arr2d = cfg["slice_fn"](idx)
|
arr2d = cfg["slice_fn"](idx)
|
||||||
@@ -404,18 +400,16 @@ def series_reconstruct_task(
|
|||||||
ds_new.Rows = int(arr2d.shape[0])
|
ds_new.Rows = int(arr2d.shape[0])
|
||||||
ds_new.Columns = int(arr2d.shape[1])
|
ds_new.Columns = int(arr2d.shape[1])
|
||||||
ds_new.InstanceNumber = idx + 1
|
ds_new.InstanceNumber = idx + 1
|
||||||
ds_new.SOPInstanceUID = generate_uid()
|
atlas_views._stamp_derived_dataset(
|
||||||
ds_new.SeriesInstanceUID = recon_series.series_instance_uid
|
ds_new,
|
||||||
ds_new.SeriesDescription = recon_series.description or f"Recon {plane_norm.title()}"
|
series_instance_uid=recon_series.series_instance_uid,
|
||||||
ds_new.ImageType = ["DERIVED", "SECONDARY", "MPR"]
|
series_description=recon_series.description or f"Recon {plane_norm.title()}",
|
||||||
ds_new.DerivationDescription = (
|
derivation_description=(
|
||||||
f"{plane_norm.title()} reconstruction; mode={recon_thickness_mode}; "
|
f"{plane_norm.title()} reconstruction; mode={recon_thickness_mode}; "
|
||||||
f"thickness={float(cfg['slice_thickness_out']):.3f}mm; spacing={float(cfg['spacing_between_slices_out']):.3f}mm"
|
f"thickness={float(cfg['slice_thickness_out']):.3f}mm; spacing={float(cfg['spacing_between_slices_out']):.3f}mm"
|
||||||
|
),
|
||||||
|
image_type=["DERIVED", "SECONDARY", "MPR"],
|
||||||
)
|
)
|
||||||
ds_new.ContentDate = date_str
|
|
||||||
ds_new.ContentTime = time_str
|
|
||||||
ds_new.InstanceCreationDate = date_str
|
|
||||||
ds_new.InstanceCreationTime = time_str
|
|
||||||
ds_new.PixelData = arr2d.tobytes()
|
ds_new.PixelData = arr2d.tobytes()
|
||||||
ds_new.PixelSpacing = [float(cfg["pixel_spacing_out"][0]), float(cfg["pixel_spacing_out"][1])]
|
ds_new.PixelSpacing = [float(cfg["pixel_spacing_out"][0]), float(cfg["pixel_spacing_out"][1])]
|
||||||
ds_new.ImageOrientationPatient = cfg["image_orientation_out"]
|
ds_new.ImageOrientationPatient = cfg["image_orientation_out"]
|
||||||
|
|||||||
@@ -212,7 +212,7 @@
|
|||||||
<button class="btn btn-outline-warning btn-sm"
|
<button class="btn btn-outline-warning btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#truncate-series-modal">
|
data-bs-target="#truncate-series-modal">
|
||||||
<i class="bi bi-scissors"></i> Truncate series
|
<i class="bi bi-sliders"></i> Series tools
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Series Optimization</h5>
|
<h5 class="modal-title">Series Tools</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|||||||
+79
-4
@@ -307,7 +307,45 @@ def _resize_array_nearest(arr, target_rows, target_cols):
|
|||||||
return arr[np.ix_(row_idx, col_idx)]
|
return arr[np.ix_(row_idx, col_idx)]
|
||||||
|
|
||||||
|
|
||||||
def _downsample_dicom_dataset(ds, reduction_pct):
|
def _stamp_derived_dataset(
|
||||||
|
ds,
|
||||||
|
*,
|
||||||
|
series_instance_uid=None,
|
||||||
|
series_description=None,
|
||||||
|
derivation_description=None,
|
||||||
|
image_type=None,
|
||||||
|
regenerate_sop_instance_uid=True,
|
||||||
|
stamp_series_datetime=True,
|
||||||
|
):
|
||||||
|
now = timezone.now()
|
||||||
|
date_str = now.strftime("%Y%m%d")
|
||||||
|
time_str = now.strftime("%H%M%S.%f")[:13]
|
||||||
|
|
||||||
|
if regenerate_sop_instance_uid:
|
||||||
|
ds.SOPInstanceUID = generate_uid()
|
||||||
|
if series_instance_uid:
|
||||||
|
ds.SeriesInstanceUID = series_instance_uid
|
||||||
|
if series_description:
|
||||||
|
ds.SeriesDescription = series_description
|
||||||
|
|
||||||
|
ds.ImageType = image_type or ["DERIVED", "SECONDARY"]
|
||||||
|
|
||||||
|
if derivation_description:
|
||||||
|
ds.DerivationDescription = derivation_description
|
||||||
|
|
||||||
|
ds.ContentDate = date_str
|
||||||
|
ds.ContentTime = time_str
|
||||||
|
ds.InstanceCreationDate = date_str
|
||||||
|
ds.InstanceCreationTime = time_str
|
||||||
|
|
||||||
|
if stamp_series_datetime:
|
||||||
|
ds.SeriesDate = date_str
|
||||||
|
ds.SeriesTime = time_str
|
||||||
|
|
||||||
|
return ds
|
||||||
|
|
||||||
|
|
||||||
|
def _downsample_dicom_dataset(ds, reduction_pct, *, series_instance_uid=None, series_description=None):
|
||||||
if reduction_pct <= 0 or reduction_pct >= 100:
|
if reduction_pct <= 0 or reduction_pct >= 100:
|
||||||
raise ValueError("Downsample percentage must be between 1 and 99")
|
raise ValueError("Downsample percentage must be between 1 and 99")
|
||||||
|
|
||||||
@@ -321,11 +359,36 @@ def _downsample_dicom_dataset(ds, reduction_pct):
|
|||||||
target_cols = max(1, int(round(px.shape[1] * factor)))
|
target_cols = max(1, int(round(px.shape[1] * factor)))
|
||||||
resized = _resize_array_nearest(px, target_rows, target_cols)
|
resized = _resize_array_nearest(px, target_rows, target_cols)
|
||||||
|
|
||||||
|
derivation_description = f"Downsampled by {reduction_pct}% using nearest-neighbour resize"
|
||||||
|
_stamp_derived_dataset(
|
||||||
|
ds,
|
||||||
|
series_instance_uid=series_instance_uid,
|
||||||
|
series_description=series_description,
|
||||||
|
derivation_description=derivation_description,
|
||||||
|
image_type=["DERIVED", "SECONDARY"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(ds, "PixelSpacing") and ds.PixelSpacing and len(ds.PixelSpacing) >= 2:
|
||||||
|
row_spacing = _safe_float(ds.PixelSpacing[0], 1.0)
|
||||||
|
col_spacing = _safe_float(ds.PixelSpacing[1], 1.0)
|
||||||
|
ds.PixelSpacing = [float(row_spacing / factor), float(col_spacing / factor)]
|
||||||
|
|
||||||
|
if hasattr(ds, "ImagerPixelSpacing") and ds.ImagerPixelSpacing and len(ds.ImagerPixelSpacing) >= 2:
|
||||||
|
row_spacing = _safe_float(ds.ImagerPixelSpacing[0], 1.0)
|
||||||
|
col_spacing = _safe_float(ds.ImagerPixelSpacing[1], 1.0)
|
||||||
|
ds.ImagerPixelSpacing = [float(row_spacing / factor), float(col_spacing / factor)]
|
||||||
|
|
||||||
|
if hasattr(ds, "NominalScannedPixelSpacing") and ds.NominalScannedPixelSpacing and len(ds.NominalScannedPixelSpacing) >= 2:
|
||||||
|
row_spacing = _safe_float(ds.NominalScannedPixelSpacing[0], 1.0)
|
||||||
|
col_spacing = _safe_float(ds.NominalScannedPixelSpacing[1], 1.0)
|
||||||
|
ds.NominalScannedPixelSpacing = [float(row_spacing / factor), float(col_spacing / factor)]
|
||||||
|
|
||||||
ds.Rows = int(target_rows)
|
ds.Rows = int(target_rows)
|
||||||
ds.Columns = int(target_cols)
|
ds.Columns = int(target_cols)
|
||||||
ds.PixelData = resized.tobytes()
|
ds.PixelData = resized.tobytes()
|
||||||
ds.pop("SmallestImagePixelValue", None)
|
ds.pop("SmallestImagePixelValue", None)
|
||||||
ds.pop("LargestImagePixelValue", None)
|
ds.pop("LargestImagePixelValue", None)
|
||||||
|
ds.pop("PixelAspectRatio", None)
|
||||||
return ds
|
return ds
|
||||||
|
|
||||||
|
|
||||||
@@ -606,7 +669,12 @@ def series_optimize_htmx(request, series_id):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ds_out = _downsample_dicom_dataset(ds, downsample_pct)
|
ds_out = _downsample_dicom_dataset(
|
||||||
|
ds,
|
||||||
|
downsample_pct,
|
||||||
|
series_instance_uid=preview_series.series_instance_uid,
|
||||||
|
series_description=preview_series.description,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Downsample preview failed for image {}: {}", image.pk, exc)
|
logger.warning("Downsample preview failed for image {}: {}", image.pk, exc)
|
||||||
continue
|
continue
|
||||||
@@ -645,6 +713,7 @@ def series_optimize_htmx(request, series_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
downsampled = []
|
downsampled = []
|
||||||
|
replacement_series_uid = generate_uid()
|
||||||
for image in full_series_images:
|
for image in full_series_images:
|
||||||
ds = _read_series_image_dataset(image)
|
ds = _read_series_image_dataset(image)
|
||||||
if ds is None:
|
if ds is None:
|
||||||
@@ -652,7 +721,12 @@ def series_optimize_htmx(request, series_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
original_hash = image.image_blake3_hash
|
original_hash = image.image_blake3_hash
|
||||||
ds_out = _downsample_dicom_dataset(ds, downsample_pct)
|
ds_out = _downsample_dicom_dataset(
|
||||||
|
ds,
|
||||||
|
downsample_pct,
|
||||||
|
series_instance_uid=replacement_series_uid,
|
||||||
|
series_description=series.description,
|
||||||
|
)
|
||||||
out_io = io.BytesIO()
|
out_io = io.BytesIO()
|
||||||
ds_out.save_as(out_io, write_like_original=False)
|
ds_out.save_as(out_io, write_like_original=False)
|
||||||
out_io.seek(0)
|
out_io.seek(0)
|
||||||
@@ -671,7 +745,8 @@ def series_optimize_htmx(request, series_id):
|
|||||||
|
|
||||||
if downsampled:
|
if downsampled:
|
||||||
series.modified = Series.SeriesModifiedChocies.RE
|
series.modified = Series.SeriesModifiedChocies.RE
|
||||||
series.save(update_fields=["modified"])
|
series.series_instance_uid = replacement_series_uid
|
||||||
|
series.save(update_fields=["modified", "series_instance_uid"])
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
f'<div class="alert alert-success mb-0">Downsample complete ({downsample_pct}%). Updated <strong>{len(downsampled)}</strong> images while preserving original hashes.</div>'
|
f'<div class="alert alert-success mb-0">Downsample complete ({downsample_pct}%). Updated <strong>{len(downsampled)}</strong> images while preserving original hashes.</div>'
|
||||||
|
|||||||
Reference in New Issue
Block a user