start migration to dv3d
This commit is contained in:
+1
-1
@@ -175,7 +175,7 @@ class StructureForm(ModelForm):
|
|||||||
class SeriesFindingForm(ModelForm):
|
class SeriesFindingForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SeriesFinding
|
model = SeriesFinding
|
||||||
exclude = ["series", "annotation_json", "viewport_json", "current_image_id_index"]
|
exclude = ["series", "annotation_json", "viewport_json", "current_image_id_index", "annotation_json_3d", "viewer_state_3d"]
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
"findings": autocomplete.ModelSelect2Multiple(
|
"findings": autocomplete.ModelSelect2Multiple(
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-06-09 10:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0069_caseprior_prior_visibility'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='seriesfinding',
|
||||||
|
name='annotation_json_3d',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='seriesfinding',
|
||||||
|
name='viewer_state_3d',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -628,6 +628,8 @@ class SeriesFinding(FindingBase):
|
|||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
findings = self.findings.all().values_list("name")
|
findings = self.findings.all().values_list("name")
|
||||||
|
if self.series is None:
|
||||||
|
return f"SeriesFinding: {findings}/{self.description} (no series)"
|
||||||
return f"SeriesFinding: {self.series.id}/{findings}/{self.description}"
|
return f"SeriesFinding: {self.series.id}/{findings}/{self.description}"
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
|
|||||||
@@ -3,28 +3,28 @@
|
|||||||
<div>{{ series.modality}}, {{ series.examination }}, {{ series.plane }}, {{ series.contrast }}</div>
|
<div>{{ series.modality}}, {{ series.examination }}, {{ series.plane }}, {{ series.contrast }}</div>
|
||||||
<div>Description: {{series.description}}</div>
|
<div>Description: {{series.description}}</div>
|
||||||
|
|
||||||
Associated case:
|
Associated case:
|
||||||
{% for case in series.case.all %}
|
{% for case in series.case.all %}
|
||||||
<span id="case-link-{{ case.pk }}" class="case-item">
|
<span id="case-link-{{ case.pk }}" class="case-item">
|
||||||
<a href="{% url 'atlas:case_detail' pk=case.pk %}">{{case}}</a>
|
<a href="{% url 'atlas:case_detail' pk=case.pk %}">{{case}}</a>
|
||||||
{% if can_edit %}
|
|
||||||
<button hx-post="{% url 'atlas:remove_series_from_case' series.pk case.pk %}"
|
|
||||||
hx-target="#case-link-{{ case.pk }}"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-confirm="Are you sure you want to remove this series from the case?"
|
|
||||||
class="btn btn-link btn-sm button-unset remove-button">
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{% empty %}
|
|
||||||
This series is not associated with any cases.
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
|
<button hx-post="{% url 'atlas:remove_series_from_case' series.pk case.pk %}"
|
||||||
|
hx-target="#case-link-{{ case.pk }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-confirm="Are you sure you want to remove this series from the case?"
|
||||||
|
class="btn btn-link btn-sm button-unset remove-button">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% empty %}
|
||||||
|
This series is not associated with any cases.
|
||||||
|
{% if can_edit %}
|
||||||
<details>
|
<details>
|
||||||
<summary>Add Series to Case</summary>
|
<summary>Add Series to Case</summary>
|
||||||
<form hx-post="{% url 'atlas:add_series_to_case' series.pk %}"
|
<form hx-post="{% url 'atlas:add_series_to_case' series.pk %}"
|
||||||
hx-target="#add-series-to-case-result"
|
hx-target="#add-series-to-case-result"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label for="case-select">Select Case:</label>
|
<label for="case-select">Select Case:</label>
|
||||||
<select name="case_id" id="case-select" required>
|
<select name="case_id" id="case-select" required>
|
||||||
@@ -36,23 +36,29 @@
|
|||||||
</form>
|
</form>
|
||||||
<div id="add-series-to-case-result"></div>
|
<div id="add-series-to-case-result"></div>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% with image_url_array_and_count=series.get_image_url_array_and_count %}
|
{% with image_url_array_and_count=series.get_image_url_array_and_count %}
|
||||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ image_url_array_and_count.0 }}" data-annotations=''>
|
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ image_url_array_and_count.0 }}" data-annotations=''>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="root" class="dicom-viewer-root" data-images="{{ image_url_array_and_count.0 }}"
|
||||||
|
style="width: 1000px; height: 600px; box-sizing: border-box; background: #222;"
|
||||||
|
data-auto-cache-stack=true
|
||||||
|
></div>
|
||||||
|
|
||||||
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
{% if editing_finding < 1 %}
|
{% if editing_finding < 1 %}
|
||||||
<button id="add-finding-button">Add finding</button>
|
<button id="add-finding-button">Add finding</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button id="clone-finding-button" title="Click to copy the details of a finding that is already associated with the series/case"
|
<button id="clone-finding-button" title="Click to copy the details of a finding that is already associated with the series/case"
|
||||||
hx-get="{% url 'atlas:series_finding_related' series.pk %}"
|
hx-get="{% url 'atlas:series_finding_related' series.pk %}"
|
||||||
hx-target="#clone-findings-modal"
|
hx-target="#clone-findings-modal"
|
||||||
hx-trigger="click"
|
hx-trigger="click"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#clone-findings-modal"
|
data-bs-target="#clone-findings-modal"
|
||||||
>Clone existing finding</button>
|
>Clone existing finding</button>
|
||||||
<button id="reset-viewport-button">Reset viewport</button>
|
<button id="reset-viewport-button">Reset viewport</button>
|
||||||
<div id="finding-form">
|
<div id="finding-form">
|
||||||
@@ -80,6 +86,11 @@
|
|||||||
<div class="finding-box">
|
<div class="finding-box">
|
||||||
<button id="finding-{{finding.pk}}" class="view-finding-button" data-annotationjson={{finding.annotation_json}}
|
<button id="finding-{{finding.pk}}" class="view-finding-button" data-annotationjson={{finding.annotation_json}}
|
||||||
data-viewportjson={{finding.viewport_json}} data-findingid={{finding.id}} data-currentimageid={{finding.current_image_id_index}}>Click to view</button>
|
data-viewportjson={{finding.viewport_json}} data-findingid={{finding.id}} data-currentimageid={{finding.current_image_id_index}}>Click to view</button>
|
||||||
|
|
||||||
|
{% if finding.viewer_state_3d %}
|
||||||
|
<button id="finding-{{finding.pk}}-3d" class="view-finding-button-3d" data-annotationjson3d='{{finding.annotation_json_3d}}' data-viewerstatejson='{{finding.viewer_state_3d}}'>Click to view 3D</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<span class="view-finding-details">
|
<span class="view-finding-details">
|
||||||
Finding(s): {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}<br />
|
Finding(s): {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}<br />
|
||||||
Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}<br />
|
Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}<br />
|
||||||
@@ -95,6 +106,11 @@
|
|||||||
<pre>{{finding.viewport_json}}</pre>
|
<pre>{{finding.viewport_json}}</pre>
|
||||||
<h4>Image ID</h4>
|
<h4>Image ID</h4>
|
||||||
<pre>{{finding.current_image_id_index}}</pre>
|
<pre>{{finding.current_image_id_index}}</pre>
|
||||||
|
|
||||||
|
<h4>3D Annotation JSON</h4>
|
||||||
|
<pre>{{finding.annotation_json_3d}}</pre>
|
||||||
|
<h4>Viewer State</h4>
|
||||||
|
<pre>{{finding.viewer_state_3d}}</pre>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
@@ -124,76 +140,76 @@
|
|||||||
|
|
||||||
|
|
||||||
<div>Author: {{ series.get_author_display }}</div>
|
<div>Author: {{ series.get_author_display }}</div>
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<details>
|
|
||||||
<summary>Series info</summary>
|
|
||||||
<div>
|
|
||||||
<a href="{% url 'atlas:series_anonymise_dicom' pk=series.pk %}"
|
|
||||||
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
|
||||||
<a href="{% url 'atlas:series_order_dicom' pk=series.pk %}" title="orders dicom by slice location">Order dicoms
|
|
||||||
by slice location</a>
|
|
||||||
<a href="{% url 'atlas:series_order_dicom_instance' pk=series.pk %}"
|
|
||||||
title="orders dicom by instance number">Order dicoms by instance number</a>
|
|
||||||
<a href="{% url 'atlas:series_order_dicom_SeriesInstanceUID' pk=series.pk %}"
|
|
||||||
title="orders dicom by instance number">Order dicoms by SeriesInstanceUID</a>
|
|
||||||
<a href="{% url 'atlas:series_order_upload_filename' pk=series.pk %}"
|
|
||||||
title="orders dicom by uploaded filename">Order by uploaded filename</a><br/>
|
|
||||||
<a href="{% url 'api-1:series_split_by_tag' series.pk 'ImageType' %}"
|
|
||||||
title="split series by dicom tag ImageType">Split the series by the ImageType dicom tag</a>
|
|
||||||
</div>
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>Series info</summary>
|
||||||
Image details
|
<div>
|
||||||
</summary>
|
<a href="{% url 'atlas:series_anonymise_dicom' pk=series.pk %}"
|
||||||
<form hx-post="{% url 'atlas:image_diff' %}"
|
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
||||||
hx-target="#temp"
|
<a href="{% url 'atlas:series_order_dicom' pk=series.pk %}" title="orders dicom by slice location">Order dicoms
|
||||||
>
|
by slice location</a>
|
||||||
{% for image in series.get_images %}
|
<a href="{% url 'atlas:series_order_dicom_instance' pk=series.pk %}"
|
||||||
[{{ image.id }}] <a href='{% url "atlas:series_image_dicom" image.pk %}'>{{image.image.url}}</a>, pos: {{image.position}}, {{image.upload_filename}}
|
title="orders dicom by instance number">Order dicoms by instance number</a>
|
||||||
|
<a href="{% url 'atlas:series_order_dicom_SeriesInstanceUID' pk=series.pk %}"
|
||||||
|
title="orders dicom by instance number">Order dicoms by SeriesInstanceUID</a>
|
||||||
|
<a href="{% url 'atlas:series_order_upload_filename' pk=series.pk %}"
|
||||||
|
title="orders dicom by uploaded filename">Order by uploaded filename</a><br/>
|
||||||
|
<a href="{% url 'api-1:series_split_by_tag' series.pk 'ImageType' %}"
|
||||||
|
title="split series by dicom tag ImageType">Split the series by the ImageType dicom tag</a>
|
||||||
|
</div>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Image details
|
||||||
|
</summary>
|
||||||
|
<form hx-post="{% url 'atlas:image_diff' %}"
|
||||||
|
hx-target="#temp"
|
||||||
|
>
|
||||||
|
{% for image in series.get_images %}
|
||||||
|
[{{ image.id }}] <a href='{% url "atlas:series_image_dicom" image.pk %}'>{{image.image.url}}</a>, pos: {{image.position}}, {{image.upload_filename}}
|
||||||
|
|
||||||
|
|
||||||
{% if image.image %}
|
{% if image.image %}
|
||||||
[{{image.get_file_size|filesizeformat}}]
|
[{{image.get_file_size|filesizeformat}}]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{image.image_md5_hash}} ({{image.is_dicom}}) {{image.image_blake3_hash}}
|
{{image.image_md5_hash}} ({{image.is_dicom}}) {{image.image_blake3_hash}}
|
||||||
|
|
||||||
<input type="checkbox" name="image-id" value="{{image.id}}">
|
<input type="checkbox" name="image-id" value="{{image.id}}">
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
{% comment %} {{image.get_dicom_info|safe}}<br /> {% endcomment %}
|
{% comment %} {{image.get_dicom_info|safe}}<br /> {% endcomment %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button>Diff files</button>
|
<button>Diff files</button>
|
||||||
</form>
|
</form>
|
||||||
<span id="temp"></span>
|
<span id="temp"></span>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<p>Total image size: {{series.get_total_image_size|filesizeformat}}</p>
|
<p>Total image size: {{series.get_total_image_size|filesizeformat}}</p>
|
||||||
<p><a href="{% url 'atlas:series_download' pk=series.pk %}">Download images</a></p>
|
<p><a href="{% url 'atlas:series_download' pk=series.pk %}">Download images</a></p>
|
||||||
</details>
|
</details>
|
||||||
<span id="diff-controls">show <span id="diff-show-all" _="on click
|
<span id="diff-controls">show <span id="diff-show-all" _="on click
|
||||||
log '1'
|
log '1'
|
||||||
for row in .diff.rows
|
for row in .diff.rows
|
||||||
show row
|
show row
|
||||||
end">all</span> <span id="diff-show-diff" _="
|
end">all</span> <span id="diff-show-diff" _="
|
||||||
on click
|
on click
|
||||||
log 'go'
|
log 'go'
|
||||||
for row in <.diff tr/>
|
for row in <.diff tr/>
|
||||||
log row.children[0].innerHTML
|
log row.children[0].innerHTML
|
||||||
if row.children[0].innerHTML is empty
|
if row.children[0].innerHTML is empty
|
||||||
hide row
|
hide row
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
">diff</span></span>
|
">diff</span></span>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div id="clone-findings-modal"
|
<div id="clone-findings-modal"
|
||||||
class="modal modal-blur fade"
|
class="modal modal-blur fade"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
aria-hidden="false"
|
aria-hidden="false"
|
||||||
tabindex="-1">
|
tabindex="-1">
|
||||||
<div class="modal-dialog modal-dialog-centered related-finding-modal" style="max-width: 700px;">
|
<div class="modal-dialog modal-dialog-centered related-finding-modal" style="max-width: 700px;">
|
||||||
<div class="modal-content"></div>
|
<div class="modal-content"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,6 +300,17 @@
|
|||||||
cornerstone.resize(dicom_element, true);
|
cornerstone.resize(dicom_element, true);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
$(".view-finding-button-3d").each((n, el) => {
|
||||||
|
$(el).click((e) => {
|
||||||
|
dicom_element = $(".cornerstone-element").get(0);
|
||||||
|
annotationjson3d = JSON.parse(e.currentTarget.dataset.annotationjson3d);
|
||||||
|
viewer_state_json = JSON.parse(e.currentTarget.dataset.viewerstatejson);
|
||||||
|
console.log("Loading 3D annotation and viewer state", annotationjson3d, viewer_state_json);
|
||||||
|
importAnnotations_root(annotationjson3d);
|
||||||
|
importViewerState_root(viewer_state_json);
|
||||||
|
$(el).addClass("active");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(".view-finding-button").each((n, el) => {
|
$(".view-finding-button").each((n, el) => {
|
||||||
$(el).click((e) => {
|
$(el).click((e) => {
|
||||||
@@ -293,6 +320,11 @@
|
|||||||
current_image_id_index = e.currentTarget.dataset.currentimageid;
|
current_image_id_index = e.currentTarget.dataset.currentimageid;
|
||||||
console.log(e.currentTarget, current_image_id_index)
|
console.log(e.currentTarget, current_image_id_index)
|
||||||
loadAnnotationAndViewportOnElement(annotationjson, viewport, dicom_element, current_image_id_index);
|
loadAnnotationAndViewportOnElement(annotationjson, viewport, dicom_element, current_image_id_index);
|
||||||
|
|
||||||
|
importLegacyViewport_root(viewport)
|
||||||
|
importLegacyAnnotations_root(annotationjson);
|
||||||
|
jumpToSliceByImageId_root(0, current_image_id_index);
|
||||||
|
console.log("Loaded annotation and viewport on element", dicom_element, annotationjson, viewport, current_image_id_index);
|
||||||
$(el).addClass("active");
|
$(el).addClass("active");
|
||||||
|
|
||||||
|
|
||||||
@@ -368,6 +400,8 @@
|
|||||||
annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager
|
annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager
|
||||||
.saveToolState()),
|
.saveToolState()),
|
||||||
viewport_json: JSON.stringify(c.viewport),
|
viewport_json: JSON.stringify(c.viewport),
|
||||||
|
annotation_json_3d: window.exportAnnotations_root(),
|
||||||
|
viewer_state_3d: window.exportViewerState_root(),
|
||||||
findings: JSON.stringify($('#finding-form select[name="findings"]').find(":selected")
|
findings: JSON.stringify($('#finding-form select[name="findings"]').find(":selected")
|
||||||
.map((i, el) => {
|
.map((i, el) => {
|
||||||
return $(el).val()
|
return $(el).val()
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% with image_url_array_and_count=series.get_image_url_array_and_count %}
|
||||||
|
{% comment %} <div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ image_url_array_and_count.0 }}" data-annotations=''>
|
||||||
|
</div> {% endcomment %}
|
||||||
|
|
||||||
|
<div id="root" class="dicom-viewer-root" data-images="{{ image_url_array_and_count.0 }}"
|
||||||
|
style="width: 1000px; height: 600px; box-sizing: border-box; background: #222;"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
<script type="module" src="{% static 'dv3d/index-DCNn6AQH.js' %}" defer="defer" type="module"></script>
|
||||||
|
|
||||||
|
<input type="file" id="dicomInput" multiple />
|
||||||
|
<button onclick="sendToViewer()">Send to Viewer</button>
|
||||||
|
<button onclick="testLoadWadouriStack()">Test Load wadouri Stack</button>
|
||||||
|
<script>
|
||||||
|
function sendToViewer() {
|
||||||
|
const files = document.getElementById('dicomInput').files;
|
||||||
|
if (window.loadDicomStackFromFiles) {
|
||||||
|
window.loadDicomStackFromFiles(files);
|
||||||
|
} else {
|
||||||
|
alert("Viewer API not ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function testLoadWadouriStack() {
|
||||||
|
const urls = [
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_757_XQQyovo.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_766_8fWI0Qc.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_774_c8IpxUu.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_758_6WKI7ws.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_767_TduVOb8.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_775_7zWneQ0.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_759_BtKzgS5.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_768_RdvtM8K.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_776_LhVY3Ty.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_760_a8CiR4J.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_769_YHyJ1l6.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_777_8IFYJmL.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_761_vyvO1EN.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_770_vQwi4LW.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_778_BwGAMOR.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_762_DhSgQsm.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_771_xifitCl.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_779_LyA1zYn.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_763_aVjff3H.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_772_cDQZtxQ.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_780_qaZqsqg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_764_dDoeldL.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_773_93Hvedg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_781_bEx9Owg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_765_qmd5mYJ.dcm"
|
||||||
|
];
|
||||||
|
const wadouriList = urls.map(url => "wadouri:" + url);
|
||||||
|
if (window.loadDicomStackFromWadouriList) {
|
||||||
|
window.loadDicomStackFromWadouriList(wadouriList);
|
||||||
|
} else {
|
||||||
|
alert("Viewer API not ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends 'atlas/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>SeriesFinding Migration Status</h2>
|
||||||
|
<table class="table table-bordered table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Series</th>
|
||||||
|
<th>annotation_json_3d</th>
|
||||||
|
<th>viewer_state_3d</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for f in findings_status %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ f.id }}</td>
|
||||||
|
<td>{{ f.str }}</td>
|
||||||
|
<td>
|
||||||
|
{% if f.series %}
|
||||||
|
<a href="{% url 'atlas:series_detail' f.series.pk %}">{{ f.series }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if f.annotation_json_3d %}
|
||||||
|
<span class="badge bg-success">Populated</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Empty</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if f.viewer_state_3d %}
|
||||||
|
<span class="badge bg-success">Populated</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Empty</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
+3
-1
@@ -266,6 +266,8 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path('series/<int:series_pk>/add-to-case/', views.add_series_to_case, name='add_series_to_case'),
|
path('series/<int:series_pk>/add-to-case/', views.add_series_to_case, name='add_series_to_case'),
|
||||||
path('series/<int:series_pk>/<int:case_pk>/remove-from-case/', views.remove_series_from_case, name='remove_series_from_case'),
|
path('series/<int:series_pk>/<int:case_pk>/remove-from-case/', views.remove_series_from_case, name='remove_series_from_case'),
|
||||||
|
path('series_finding/<int:finding_id>/details/', views.series_finding_details, name='series_finding_details'),
|
||||||
|
path('series_finding/migration-status/', views.seriesfinding_migration_status, name='seriesfinding_migration_status'),
|
||||||
path("uncategorised_dicoms/", views.uncategorised_dicoms, name="uncategorised_dicoms_view"),
|
path("uncategorised_dicoms/", views.uncategorised_dicoms, name="uncategorised_dicoms_view"),
|
||||||
path("question_schema/", views.QuestionSchemaView.as_view(), name="question_schema_overview"),
|
path("question_schema/", views.QuestionSchemaView.as_view(), name="question_schema_overview"),
|
||||||
path("question_schema/<int:pk>", views.question_schema_detail, name="question_schema_detail"),
|
path("question_schema/<int:pk>", views.question_schema_detail, name="question_schema_detail"),
|
||||||
@@ -297,7 +299,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("condition/create", views.ConditionCreate.as_view(), name="condition_create"),
|
path("condition/create", views.ConditionCreate.as_view(), name="condition_create"),
|
||||||
path("finding/", views.FindingView.as_view(), name="finding_view"),
|
path("finding/", views.FindingView.as_view(), name="finding_view"),
|
||||||
path('series_finding/<int:finding_id>/details/', views.series_finding_details, name='series_finding_details'),
|
|
||||||
path("finding/<int:pk>", views.finding_detail, name="finding_detail"),
|
path("finding/<int:pk>", views.finding_detail, name="finding_detail"),
|
||||||
path(
|
path(
|
||||||
"finding/<int:pk>/delete", views.FindingDelete.as_view(), name="finding_delete"
|
"finding/<int:pk>/delete", views.FindingDelete.as_view(), name="finding_delete"
|
||||||
@@ -323,6 +324,7 @@ urlpatterns = [
|
|||||||
path("series/<int:pk>/thumbnail", views.series_thumbnail, name="series_thumbnail"),
|
path("series/<int:pk>/thumbnail", views.series_thumbnail, name="series_thumbnail"),
|
||||||
# TODO: case context series viewing (so that we can view series in the context of a case)
|
# TODO: case context series viewing (so that we can view series in the context of a case)
|
||||||
path("series/<int:pk>", views.series_detail, name="series_detail"),
|
path("series/<int:pk>", views.series_detail, name="series_detail"),
|
||||||
|
path("series/<int:pk>/viewer", views.series_viewer, name="series_viewer"),
|
||||||
path("series/<int:pk>/authors", views.SeriesAuthorUpdate.as_view(), name="series_authors"),
|
path("series/<int:pk>/authors", views.SeriesAuthorUpdate.as_view(), name="series_authors"),
|
||||||
path("series/<int:series_id>/finding/related", views.series_finding_related, name="series_finding_related"),
|
path("series/<int:series_id>/finding/related", views.series_finding_related, name="series_finding_related"),
|
||||||
path("series/", views.SeriesView.as_view(), name="series_view"),
|
path("series/", views.SeriesView.as_view(), name="series_view"),
|
||||||
|
|||||||
+37
-1
@@ -231,6 +231,19 @@ def series_thumbnail(request, pk, fail_loudly=False):
|
|||||||
|
|
||||||
return HttpResponse(thumbnail)
|
return HttpResponse(thumbnail)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
|
||||||
|
def series_viewer(request, pk, finding_pk=None):
|
||||||
|
series = get_object_or_404(Series, pk=pk)
|
||||||
|
context = {
|
||||||
|
"series": series,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"atlas/series_viewer_test.html",
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
|
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
|
||||||
@@ -1440,6 +1453,8 @@ def create_series_findings(request):
|
|||||||
annotation_json = request.POST.get("annotation_json")
|
annotation_json = request.POST.get("annotation_json")
|
||||||
viewport_json = request.POST.get("viewport_json")
|
viewport_json = request.POST.get("viewport_json")
|
||||||
current_image_id_index = request.POST.get("current_image_id_index")
|
current_image_id_index = request.POST.get("current_image_id_index")
|
||||||
|
annotation_json_3d = request.POST.get("annotation_json_3d")
|
||||||
|
viewer_state_3d = request.POST.get("viewer_state_3d")
|
||||||
|
|
||||||
series = Series.objects.get(pk=series_id)
|
series = Series.objects.get(pk=series_id)
|
||||||
findings = Finding.objects.filter(pk__in=findings_ids)
|
findings = Finding.objects.filter(pk__in=findings_ids)
|
||||||
@@ -1464,6 +1479,8 @@ def create_series_findings(request):
|
|||||||
sf.structures.set(structures)
|
sf.structures.set(structures)
|
||||||
sf.conditions.set(conditions)
|
sf.conditions.set(conditions)
|
||||||
sf.current_image_id_index = current_image_id_index
|
sf.current_image_id_index = current_image_id_index
|
||||||
|
sf.annotation_json_3d = annotation_json_3d
|
||||||
|
sf.viewer_state_3d = viewer_state_3d
|
||||||
sf.save()
|
sf.save()
|
||||||
return JsonResponse({"success": True})
|
return JsonResponse({"success": True})
|
||||||
|
|
||||||
@@ -3145,4 +3162,23 @@ def series_finding_details(request, finding_id):
|
|||||||
def series_finding_related(request, series_id):
|
def series_finding_related(request, series_id):
|
||||||
series = get_object_or_404(Series, pk=series_id)
|
series = get_object_or_404(Series, pk=series_id)
|
||||||
findings = series.get_related_findings()
|
findings = series.get_related_findings()
|
||||||
return render(request, 'atlas/series_finding_related.html', {'series': series, 'findings': findings})
|
return render(request, 'atlas/series_finding_related.html', {'series': series, 'findings': findings})
|
||||||
|
|
||||||
|
def seriesfinding_migration_status(request):
|
||||||
|
"""
|
||||||
|
View to list all SeriesFinding objects and their migration status.
|
||||||
|
Shows whether annotation_json_3d and viewer_state_3d are populated.
|
||||||
|
"""
|
||||||
|
findings = SeriesFinding.objects.all().select_related()
|
||||||
|
findings_status = []
|
||||||
|
for finding in findings:
|
||||||
|
findings_status.append({
|
||||||
|
"id": finding.id,
|
||||||
|
"str": str(finding),
|
||||||
|
"annotation_json_3d": bool(finding.annotation_json_3d),
|
||||||
|
"viewer_state_3d": bool(finding.viewer_state_3d),
|
||||||
|
"series": finding.series
|
||||||
|
})
|
||||||
|
return render(request, "atlas/seriesfinding_migration_status.html", {
|
||||||
|
"findings_status": findings_status
|
||||||
|
})
|
||||||
@@ -1998,6 +1998,9 @@ class FindingBase(models.Model):
|
|||||||
# This has been migrated to install the image id (not index)
|
# This has been migrated to install the image id (not index)
|
||||||
current_image_id_index = models.TextField(null=True, blank=True)
|
current_image_id_index = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
annotation_json_3d = models.JSONField(default=dict)
|
||||||
|
viewer_state_3d = models.JSONField(default=dict)
|
||||||
|
|
||||||
# TODO add tags
|
# TODO add tags
|
||||||
# tags = models.ManyToManyField(Tag, blank=True)
|
# tags = models.ManyToManyField(Tag, blank=True)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
html,body,#root{width:100vw;height:100vh;margin:0;padding:0;overflow:hidden;box-sizing:border-box}.dicom-viewer-root,#root{width:100vw;height:100vh;min-height:0;min-width:0;position:relative}.dicom-viewer-test-root{box-sizing:border-box;color:gray;display:block;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:16px;font-weight:400;height:500px;line-height:24px;max-height:500px;overflow-x:auto;overflow-y:auto;text-align:start;text-size-adjust:100%;unicode-bidi:isolate;width:1296px;-webkit-tap-highlight-color:rgba(0,0,0,0)}.cornerstone-viewport,.cornerstone-canvas{cursor:inherit!important}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-06-09 10:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shorts', '0008_useranswer_candidate_feedback_answermarks'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='questionfinding',
|
||||||
|
name='annotation_json_3d',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='questionfinding',
|
||||||
|
name='viewer_state_3d',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -70,6 +70,8 @@
|
|||||||
<script src="{% static 'js/cornerstone/cornerstoneWADOImageLoader.min.js' %}"></script>
|
<script src="{% static 'js/cornerstone/cornerstoneWADOImageLoader.min.js' %}"></script>
|
||||||
<script src="{% static 'js/cornerstone/cornerstone-base64-image-loader.umd.js' %}"></script>
|
<script src="{% static 'js/cornerstone/cornerstone-base64-image-loader.umd.js' %}"></script>
|
||||||
<script src="{% static 'js/anatomy.js' %}" defer="defer" type="module"></script>
|
<script src="{% static 'js/anatomy.js' %}" defer="defer" type="module"></script>
|
||||||
|
<script src="{% static 'dv3d/index-DwrZLsZY.js' %}" type="module" defer="defer" type="module"></script>
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
|
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
|
||||||
|
|||||||
Reference in New Issue
Block a user