feat: Enhance series splitting functionality with DICOM tag analysis and improved user feedback

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Ross
2026-04-30 21:57:19 +01:00
parent 9edac288f5
commit 6574c29a70
2 changed files with 93 additions and 26 deletions
@@ -1,17 +1,41 @@
{% load django_htmx %}
<form method="post"
action="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
hx-post="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
hx-target="#split-by-tag-result"
hx-swap="innerHTML">
{% csrf_token %}
<div class="input-group input-group-sm">
<select name="dicom_tag" class="form-select form-select-sm">
{% for tag_value, tag_label in split_tags %}
<option value="{{ tag_value }}">{{ tag_label }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-outline-warning btn-sm">Split</button>
<div id="series-split-by-tag-panel" class="d-flex flex-column gap-2">
<div class="d-flex flex-wrap gap-2">
<button type="button"
class="btn btn-outline-warning btn-sm"
hx-get="{% url 'atlas:series_split_by_tag' pk=series.pk %}?analyze=1"
hx-target="#series-split-by-tag-panel"
hx-swap="outerHTML">
Analyze split options
</button>
</div>
</form>
<div id="split-by-tag-result" class="mt-2"></div>
{% if analyze_requested %}
{% if split_options %}
<form method="post"
action="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
hx-post="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
hx-target="#split-by-tag-result"
hx-swap="innerHTML">
{% csrf_token %}
<div class="input-group input-group-sm">
<select name="dicom_tag" class="form-select form-select-sm">
{% for option in split_options %}
<option value="{{ option.tag_value }}">{{ option.tag_label }} ({{ option.split_count }} series)</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-outline-warning btn-sm">Split</button>
</div>
</form>
<p class="small text-muted mb-0">Only tags that can produce more than one output series are shown.</p>
{% else %}
<div class="alert alert-secondary mb-0 small">
No common split tags were found that can divide this series into multiple groups.
</div>
{% endif %}
{% else %}
<p class="small text-muted mb-0">Run analysis to detect valid split tags and estimated output series count.</p>
{% endif %}
<div id="split-by-tag-result"></div>
</div>
+54 -11
View File
@@ -10,6 +10,7 @@ from django.db import transaction
from django.shortcuts import render, get_object_or_404, redirect
from django import forms
from django.utils import timezone
from django.utils.html import escape
from django.views.decorators.http import require_POST
from django.views.decorators.http import require_http_methods
from django.template.loader import render_to_string
@@ -3637,6 +3638,43 @@ SERIES_SPLIT_TAGS = [
]
def _normalise_dicom_split_value(value):
if isinstance(value, pydicom.multival.MultiValue):
return ",".join(map(str, value))
return str(value)
def _analyse_series_split_options(series):
tags = [tag_value for tag_value, _ in SERIES_SPLIT_TAGS]
tag_value_sets = {tag: set() for tag in tags}
tag_missing = {tag: False for tag in tags}
for image in series.images.iterator():
ds = image.get_dicom_data()
for tag in tags:
if tag in ds:
tag_value_sets[tag].add(_normalise_dicom_split_value(ds[tag].value))
else:
tag_missing[tag] = True
split_options = []
for tag_value, tag_label in SERIES_SPLIT_TAGS:
if tag_missing[tag_value]:
continue
split_count = len(tag_value_sets[tag_value])
if split_count > 1:
split_options.append(
{
"tag_value": tag_value,
"tag_label": tag_label,
"split_count": split_count,
}
)
split_options.sort(key=lambda item: item["split_count"], reverse=True)
return split_options
@login_required
@user_is_author_or_atlas_series_checker
def series_split_by_tag(request, pk):
@@ -3645,20 +3683,27 @@ def series_split_by_tag(request, pk):
Scope: Atlas series authors and series checkers only (via decorator).
Functionality:
- GET: renders a partial form with a dropdown of common DICOM tags.
- GET: renders a partial form and, on demand, analyzes available DICOM tags
to list only tags that can actually split the series.
- POST: performs the split. The original series retains the images matching
the first unique tag value; new series are cloned for each subsequent value.
On success, redirects to the series detail page.
On success, returns an HTMX success fragment with number of series created.
On error (tag not found or non-DICOM images), returns an HTMX-swappable
error message fragment.
"""
series = get_object_or_404(Series, pk=pk)
if request.method == "GET":
analyze_requested = request.GET.get("analyze") == "1"
split_options = _analyse_series_split_options(series) if analyze_requested else []
return render(
request,
"atlas/partials/_series_split_by_tag.html",
{"series": series, "split_tags": SERIES_SPLIT_TAGS},
{
"series": series,
"split_options": split_options,
"analyze_requested": analyze_requested,
},
)
dicom_tag = request.POST.get("dicom_tag", "").strip()
@@ -3679,18 +3724,14 @@ def series_split_by_tag(request, pk):
ds = image.get_dicom_data()
if dicom_tag not in ds:
return HttpResponse(
f'<div class="alert alert-danger">Tag <code>{dicom_tag}</code> not found in image {image}.</div>'
f'<div class="alert alert-danger">Tag <code>{escape(str(dicom_tag))}</code> not found in image {escape(str(image))}.</div>'
)
val = ds[dicom_tag].value
if isinstance(val, pydicom.multival.MultiValue):
key = ",".join(map(str, val))
else:
key = str(val)
key = _normalise_dicom_split_value(ds[dicom_tag].value)
image_map[key].append(image)
if len(image_map) < 2:
return HttpResponse(
f'<div class="alert alert-warning">Tag <code>{dicom_tag}</code> has only one unique value no split performed.</div>'
f'<div class="alert alert-warning">Tag <code>{escape(str(dicom_tag))}</code> has only one unique value, so no split was performed.</div>'
)
case = list(series.case.all())
@@ -3715,7 +3756,9 @@ def series_split_by_tag(request, pk):
dicom_tag,
new_series_pks,
)
return redirect("atlas:series_detail", pk=new_series_pks[0])
return HttpResponse(
f'<div class="alert alert-success">Split complete: <strong>{len(new_series_pks)}</strong> series created from tag <code>{escape(str(dicom_tag))}</code>.</div>'
)
# Vendored in from BaseZipView