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 helpers.cimar import CimarAPI, NotFoundError
|
||||
from pydicom.uid import generate_uid
|
||||
from datetime import datetime
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.base import ContentFile
|
||||
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.series_instance_uid = generate_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"])):
|
||||
arr2d = cfg["slice_fn"](idx)
|
||||
@@ -404,18 +400,16 @@ def series_reconstruct_task(
|
||||
ds_new.Rows = int(arr2d.shape[0])
|
||||
ds_new.Columns = int(arr2d.shape[1])
|
||||
ds_new.InstanceNumber = idx + 1
|
||||
ds_new.SOPInstanceUID = generate_uid()
|
||||
ds_new.SeriesInstanceUID = recon_series.series_instance_uid
|
||||
ds_new.SeriesDescription = recon_series.description or f"Recon {plane_norm.title()}"
|
||||
ds_new.ImageType = ["DERIVED", "SECONDARY", "MPR"]
|
||||
ds_new.DerivationDescription = (
|
||||
atlas_views._stamp_derived_dataset(
|
||||
ds_new,
|
||||
series_instance_uid=recon_series.series_instance_uid,
|
||||
series_description=recon_series.description or f"Recon {plane_norm.title()}",
|
||||
derivation_description=(
|
||||
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"
|
||||
),
|
||||
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.PixelSpacing = [float(cfg["pixel_spacing_out"][0]), float(cfg["pixel_spacing_out"][1])]
|
||||
ds_new.ImageOrientationPatient = cfg["image_orientation_out"]
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
<button class="btn btn-outline-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#truncate-series-modal">
|
||||
<i class="bi bi-scissors"></i> Truncate series
|
||||
<i class="bi bi-sliders"></i> Series tools
|
||||
</button>
|
||||
|
||||
<div class="card mt-2">
|
||||
@@ -255,7 +255,7 @@
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
</div>
|
||||
<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)]
|
||||
|
||||
|
||||
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:
|
||||
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)))
|
||||
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.Columns = int(target_cols)
|
||||
ds.PixelData = resized.tobytes()
|
||||
ds.pop("SmallestImagePixelValue", None)
|
||||
ds.pop("LargestImagePixelValue", None)
|
||||
ds.pop("PixelAspectRatio", None)
|
||||
return ds
|
||||
|
||||
|
||||
@@ -606,7 +669,12 @@ def series_optimize_htmx(request, series_id):
|
||||
continue
|
||||
|
||||
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:
|
||||
logger.warning("Downsample preview failed for image {}: {}", image.pk, exc)
|
||||
continue
|
||||
@@ -645,6 +713,7 @@ def series_optimize_htmx(request, series_id):
|
||||
)
|
||||
|
||||
downsampled = []
|
||||
replacement_series_uid = generate_uid()
|
||||
for image in full_series_images:
|
||||
ds = _read_series_image_dataset(image)
|
||||
if ds is None:
|
||||
@@ -652,7 +721,12 @@ def series_optimize_htmx(request, series_id):
|
||||
|
||||
try:
|
||||
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()
|
||||
ds_out.save_as(out_io, write_like_original=False)
|
||||
out_io.seek(0)
|
||||
@@ -671,7 +745,8 @@ def series_optimize_htmx(request, series_id):
|
||||
|
||||
if downsampled:
|
||||
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(
|
||||
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