Add downsample series functionality and related UI updates
- Implemented asynchronous downsample task for series with progress tracking. - Added new URL endpoint for downsample status. - Updated Series model to include source_series_instance_uid. - Enhanced case display template with new buttons for series actions. - Added password reset functionality in user profile with appropriate alerts. - Created migration for new source_series_instance_uid field in Series model.
This commit is contained in:
+101
-2
@@ -92,6 +92,106 @@ from django.core.cache import cache
|
||||
import copy
|
||||
import io
|
||||
|
||||
|
||||
@task(takes_context=True)
|
||||
def series_downsample_task(context, series_id, user_id, downsample_pct):
|
||||
"""Downsample a series asynchronously while preserving hash continuity."""
|
||||
from loguru import logger
|
||||
from atlas import views as atlas_views
|
||||
|
||||
progress_key = f"series_downsample_progress:{context.task_result.id}"
|
||||
cache.set(
|
||||
progress_key,
|
||||
{"current": 0, "total": 0, "message": "Preparing downsample..."},
|
||||
timeout=60 * 60,
|
||||
)
|
||||
|
||||
series = get_object_or_404(Series, pk=series_id)
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
|
||||
if not series.check_user_can_edit(user):
|
||||
raise PermissionError("Permission denied")
|
||||
|
||||
images = list(series.get_images())
|
||||
total_images = len(images)
|
||||
replacement_series_uid = generate_uid()
|
||||
source_series_uid = series.series_instance_uid
|
||||
updated_images = []
|
||||
|
||||
cache.set(
|
||||
progress_key,
|
||||
{"current": 0, "total": total_images, "message": "Downsample started"},
|
||||
timeout=60 * 60,
|
||||
)
|
||||
|
||||
for idx, image in enumerate(images, start=1):
|
||||
ds = atlas_views._read_series_image_dataset(image)
|
||||
if ds is None:
|
||||
cache.set(
|
||||
progress_key,
|
||||
{"current": idx, "total": total_images, "message": f"Skipping unreadable image ({idx}/{total_images})"},
|
||||
timeout=60 * 60,
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
original_hash = image.image_blake3_hash
|
||||
ds_out = atlas_views._downsample_dicom_dataset(
|
||||
ds,
|
||||
int(downsample_pct),
|
||||
series_instance_uid=replacement_series_uid,
|
||||
source_series_instance_uid=source_series_uid,
|
||||
series_description=series.description,
|
||||
)
|
||||
out_io = io.BytesIO()
|
||||
ds_out.save_as(out_io, write_like_original=False)
|
||||
out_io.seek(0)
|
||||
|
||||
image.image.save(
|
||||
f"downsample_{downsample_pct}_{image.pk}.dcm",
|
||||
ContentFile(out_io.getvalue()),
|
||||
save=False,
|
||||
)
|
||||
image.image_blake3_hash = original_hash
|
||||
image.save(update_fields=["image", "image_blake3_hash"])
|
||||
updated_images.append(image.pk)
|
||||
except Exception as exc:
|
||||
logger.warning("Downsample failed for image {}: {}", image.pk, exc)
|
||||
|
||||
cache.set(
|
||||
progress_key,
|
||||
{
|
||||
"current": idx,
|
||||
"total": total_images,
|
||||
"message": f"Downsampling image {idx}/{total_images}",
|
||||
},
|
||||
timeout=60 * 60,
|
||||
)
|
||||
|
||||
if updated_images:
|
||||
series.modified = Series.SeriesModifiedChocies.RE
|
||||
series.source_series_instance_uid = source_series_uid
|
||||
series.series_instance_uid = replacement_series_uid
|
||||
series.save(update_fields=["modified", "source_series_instance_uid", "series_instance_uid"])
|
||||
|
||||
cache.set(
|
||||
progress_key,
|
||||
{
|
||||
"current": total_images,
|
||||
"total": total_images,
|
||||
"message": "Downsample complete",
|
||||
},
|
||||
timeout=10 * 60,
|
||||
)
|
||||
|
||||
return {
|
||||
"series_id": series.pk,
|
||||
"downsample_pct": int(downsample_pct),
|
||||
"updated_images": len(updated_images),
|
||||
"series_instance_uid": replacement_series_uid,
|
||||
"source_series_instance_uid": source_series_uid,
|
||||
}
|
||||
|
||||
@task
|
||||
def push_case_to_cimar_task(case_id):
|
||||
"""Sends an email when the feedback form has been submitted."""
|
||||
@@ -389,8 +489,6 @@ def series_reconstruct_task(
|
||||
|
||||
for plane_norm, cfg in plane_builders.items():
|
||||
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"])
|
||||
|
||||
for idx in range(int(cfg["count"])):
|
||||
arr2d = cfg["slice_fn"](idx)
|
||||
@@ -403,6 +501,7 @@ def series_reconstruct_task(
|
||||
atlas_views._stamp_derived_dataset(
|
||||
ds_new,
|
||||
series_instance_uid=recon_series.series_instance_uid,
|
||||
source_series_instance_uid=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}; "
|
||||
|
||||
Reference in New Issue
Block a user