many improvements

This commit is contained in:
Ross
2024-04-15 14:02:01 +01:00
parent 8c6361f9b2
commit f1642c7f01
12 changed files with 481 additions and 188 deletions
+2 -1
View File
@@ -167,7 +167,7 @@ $(document).ready(function () {
window.loadDicomViewerEvent = new Event('loadDicomViewer');
window.addEventListener('loadDicomViewer', function (e) {
//console.log("listen", e.detail)
console.log("LoadDicomViewer event", e.detail)
let images = [];
//console.log($("#image_form_set img"));
$(e.detail).each((n, el) => {
@@ -179,6 +179,7 @@ window.addEventListener('loadDicomViewer', function (e) {
window.loadDicomViewerEvent = new Event('loadDicomViewerUrls');
window.addEventListener('loadDicomViewerUrls', function (e) {
console.log("LoadDicomViewer unls event", e.detail)
loadDicomViewer(e.detail.images, e.detail.annotations);
}, false);
+171 -8
View File
@@ -66,6 +66,7 @@
function addFile(f, feedback) {
console.log("add file", f)
let dT = new DataTransfer();
dT.clearData();
dT.items.add(f);
@@ -162,9 +163,6 @@
// Probably no need to await here anymore
//await readFile(el.files[0], el);
$(el).parent().parent().find(".temp-thumb").remove();
let url = URL.createObjectURL(el.files[0])
@@ -196,9 +194,9 @@
checkHash(hash, el);
if ($("#drop-filenames").length > 0) {
$("#drop-filenames").append(
`<span data-input-id='${el.id}'><span>${n}: ${el.files[0].name}</span><img class='uploading${extra_class}' data-input-id='${el.id}' src=${url}></span>`
)
$("#drop-filenames").append(
`<span data-input-id='${el.id}'><span>${n}: ${el.files[0].name}</span><img class='uploading${extra_class}' data-input-id='${el.id}' src=${url}></span>`
)
}
} else {
imageId = `wadouri:${url}`;
@@ -223,7 +221,7 @@
console.log("image", image)
let bytesView = image.getPixelData()
let str = new TextDecoder("utf-8").decode(bytesView, );
let str = new TextDecoder("utf-8").decode(bytesView, );
console.log("bytes", bytesView)
console.log("str", str)
@@ -363,7 +361,7 @@
//}
image_set = $("#image_form_set .temp-thumb").get() //.map(function() { return $(this).attr("src")}).get();
console.log(image_set);
console.log("image set", image_set);
//console.log()
//if (n == image_set.length) {
const event = new CustomEvent('loadDicomViewer', {
@@ -376,4 +374,169 @@
updateImagePositions();
//}
}
function extractDicomDetails(byteArray) {
// We need to setup a try/catch block because parseDicom will throw an exception
// if you attempt to parse a non dicom part 10 file (or one that is corrupted)
try {
console.log("bytearray", byteArray)
// parse byteArray into a DataSet object using the parseDicom library
var dataSet = dicomParser.parseDicom(byteArray);
console.log("ds", dataSet)
// dataSet contains the parsed elements. Each element is available via a property
// in the dataSet.elements object. The property name is based on the elements group
// and element in the following format: xggggeeee where gggg is the group number
// and eeee is the element number with lowercase hex characters.
// To access the data for an element, we need to know its type and its tag.
// We will get the sopInstanceUid from the file which is a string and
// has the tag (0020,000D)
var study_description = dataSet.string('x00081030');
var accession_number = dataSet.string('x00080050').toLowerCase();
var patient_id = dataSet.string('x00100020').toLowerCase();
var modality = dataSet.string('x00080060');
var projection = dataSet.string('x00185101');
try {
var plane = dataSet.string('x00180022').toLowerCase();
} catch (err) {
var plane = "";
}
console.log("STUDY", study_description);
console.log("acc", accession_number);
console.log("pid", patient_id);
console.log("projection", projection);
console.log("plane", plane);
function select_plane(option) {
let plane_select_object = document.getElementsByName("plane")[0]
const $options = Array.from(plane_select_object.options);
const optionToSelect = $options.find(item => item.text === option);
optionToSelect.selected = true;
}
if (plane.includes("axial")) {
select_plane("axial");
} else if (plane.includes("sagittal")) {
select_plane("sagittal");
} else if (plane.includes("coronal")) {
select_plane("coronal");
} else if (plane.includes("ap")) {
select_plane("AP");
} else if (plane.includes("lat") | plane.includes("lateral")) {
select_plane("lateral");
} else if (plane.includes("pa")) {
select_plane("PA");
}
modality_map = {
"DX": "Radiograph",
"CR": "Radiograph",
"CT": "CT",
"MR": "MR",
"RF": "Fluroscopy",
"US": "Ultrasound",
}
if (modality in modality_map) {
let modality_select_object = document.getElementsByName("modality")[0]
const $options = Array.from(modality_select_object.options);
const optionToSelect = $options.find(item => item.text === modality_map[modality]);
optionToSelect.selected = true;
} else {
toastr.info(`Unable to automatically set modality: ${modality}`);
}
// If the dicom study_description contains multiple values select the first
study_description = study_description.split(",")[0].trim();
to_replace = ["lt", "rt", "contrast", "LT", "RT", "Contrast"];
for (let i = 0; i < to_replace.length; i++) {
study_description = study_description.replace(to_replace[i], "");
}
let select_object = document.getElementsByName("examination")[0]
if (select_object.value == "") {
$.get(`${select_object.dataset.autocompleteLightUrl}?q=${study_description}`, (data) => {
console.log(data)
if (data.results.length == 1) {
$(select_object).select2("trigger", "select", {
data: data.results[0]
})
}
})
}
not_anon = false;
site_codes = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"]
for (let i = 0; i < site_codes.length; i++) {
if (
accession_number.startsWith(site_codes[i]) ||
patient_id.startsWith(site_codes[i])) {
not_anon = true;
}
}
if (not_anon) {
toastr.warning(`File does not appear to be annonymised<br/>Accession: ${accession_number} <br/>Patient ID: ${patient_id}<br/>`, 'Anonymisation warning', {
"closeButton": false,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-full-width",
"preventDuplicates": false,
"onclick": null,
"showDuration": "0",
"hideDuration": "0",
"timeOut": 0,
"extendedTimeOut": 0,
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut",
});
}
// TODO: fix
//$.ajax({
// url: `{% url 'generic:examination-autocomplete' %}/${encodeURI(study_description)}`,
// //context: document.body,
// success: function(data){
// console.log(data)
// }
//});
//// try to match with a study description (if not already selected)
//if ($(`#id_examination_to option`).length < 1) {
// option = $(`#id_examination_from option[title*='${study_description}' i]`);
// if (option.length) {
// option.appendTo($("#id_examination_to"));
// toastr.success(`Examination set to ${option.attr('title')} from dicom data`);
// } else {
// toastr.warning(
// `Unable to set examination ${study_description} from dicom data (it needs creating)`);
// }
//}
} catch (err) {
console.log(err);
}
}
+14 -3
View File
@@ -20,7 +20,7 @@ from pydicom.errors import InvalidDicomError
from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.utils.html import format_html
from django.utils.html import format_html, mark_safe
from django.urls import reverse
@@ -39,6 +39,8 @@ from helpers.images import (
pretty_print_dicom,
)
import mimetypes
from generic.models import (
CidUser,
@@ -1199,11 +1201,20 @@ class Resource(models.Model, AuthorMixin):
if self.url:
html = f"<a target='_blank' href='{self.url}'>{self.name}</a>"
elif self.file:
html = f"<a target='_blank' href='{self.file.url}'>{self.name}</a>"
filetype, _ = mimetypes.guess_type(self.file.url)
match filetype.split("/"):
case "image", _:
html = f"<img src='{self.file.url}'>"
case "video", _:
html = f"<video controls><source src='{self.file.url}'></video>"
case _:
html = f"<a target='_blank' href='{self.file.url}'>{self.name}</a>"
else:
html = self.name
return format_html(html)
return format_html("<details class='resource-block'><summary>{}</summary>{}</details>", self.name, mark_safe(html))
@@ -19,14 +19,16 @@
{% if not completed %}
<h5>Resources</h4>
<ul>
{% for caseresource in resources %}
<li>
{{caseresource.resource.get_display}}
</li>
{% endfor %}
</ul>
{% if resources %}
<h5>Resources</h4>
<ul class="no-list-style">
{% for caseresource in resources %}
<li>
{{caseresource.resource.get_display}}
</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
@@ -108,14 +110,16 @@
{% endif %}
{% if completed %}
<h5>Resources</h4>
<ul>
{% for caseresource in resources %}
<li>
{{caseresource.resource.get_display}}
</li>
{% endfor %}
</ul>
{% if resources %}
<h5>Resources</h4>
<ul class="no-list-style">
{% for caseresource in resources %}
<li>
{{caseresource.resource.get_display}}
</li>
{% endfor %}
</ul>
{% endif %}
<p>
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button>Add self review</button></a>
+243
View File
@@ -0,0 +1,243 @@
{% extends 'atlas/base.html' %}
{% load static%}
{% block content %}
<h2>Upload dicoms</h2>
User: {{user}}<br/>
<div role="alert" class="alert alert-warning">
Please note this form will not annonymise files. Please ensure that you have done so prior to using it.
</div>
<div>This form allows you to quickly upload dicom files to the server. Select the folder containing the dicom files below.</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" name="dicom_files" id="filepicker" webkitdirectory multiple>
<input type="submit" value="Upload">
</form>
<div>Selected files:</div>
<div id="single-dicom-viewer" class="dicom-viewer" data-images="" data-annotations=''></div>
<ul id="listing"></ul>
{% endblock %}
{% block js %}
<script src="{% static 'js/upload_form_helpers.js' %}"></script>
<script>
function checkAnnonymisation(byteArray) {
// We need to setup a try/catch block because parseDicom will throw an exception
// if you attempt to parse a non dicom part 10 file (or one that is corrupted)
try {
console.log("bytearray", byteArray)
// parse byteArray into a DataSet object using the parseDicom library
var dataSet = dicomParser.parseDicom(byteArray);
console.log("ds", dataSet)
// dataSet contains the parsed elements. Each element is available via a property
// in the dataSet.elements object. The property name is based on the elements group
// and element in the following format: xggggeeee where gggg is the group number
// and eeee is the element number with lowercase hex characters.
// To access the data for an element, we need to know its type and its tag.
// We will get the sopInstanceUid from the file which is a string and
// has the tag (0020,000D)
var study_description = dataSet.string('x00081030');
var accession_number = dataSet.string('x00080050').toLowerCase();
var patient_id = dataSet.string('x00100020').toLowerCase();
console.log("STUDY", study_description);
console.log("acc", accession_number);
console.log("pid", patient_id);
not_anon = false;
site_codes = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"]
for (let i = 0; i < site_codes.length; i++) {
if (
accession_number.startsWith(site_codes[i]) ||
patient_id.startsWith(site_codes[i]) ||
patient_id.startsWith("pat"))
{
not_anon = true;
}
}
if (not_anon) {
toastr.warning(`File does not appear to be annonymised<br/>Accession: ${accession_number} <br/>Patient ID: ${patient_id}<br/>`, 'Anonymisation warning', {
"closeButton": false,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-full-width",
"preventDuplicates": false,
"onclick": null,
"showDuration": "0",
"hideDuration": "0",
"timeOut": 0,
"extendedTimeOut": 0,
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut",
});
}
// TODO: fix
//$.ajax({
// url: `{% url 'generic:examination-autocomplete' %}/${encodeURI(study_description)}`,
// //context: document.body,
// success: function(data){
// console.log(data)
// }
//});
//// try to match with a study description (if not already selected)
//if ($(`#id_examination_to option`).length < 1) {
// option = $(`#id_examination_from option[title*='${study_description}' i]`);
// if (option.length) {
// option.appendTo($("#id_examination_to"));
// toastr.success(`Examination set to ${option.attr('title')} from dicom data`);
// } else {
// toastr.warning(
// `Unable to set examination ${study_description} from dicom data (it needs creating)`);
// }
//}
return true;
} catch (err) {
console.log(err);
return false;
}
}
async function loadDicomViewerTest(callback) {
console.log("load")
$("#single-dicom-viewer").empty()
//images = []
//Function that returns a promise to read the file
const reader = (file) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader);
fileReader.readAsDataURL(file);
});
}
const readFile = (file, el) => {
reader(file).then(reader => {
console.log("hello", file, el, image)
//if ($("#id_examination").find(":selected")[0].dataset.select2Id == "2") {
// var byteArray = new Uint8Array(reader.result);
// console.log("etrxact", reader)
// extractDicomStudyDescription(byteArray);
//}
var image = new Image();
image.height = 100;
image.title = file.name;
image.src = reader.result;
image.className = "temp-thumb";
console.log("read", el);
$(el).append(image);
//images.push(reader.result);
loadViewer(images);
});
}
const dicomReader = (file) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader);
fileReader.readAsArrayBuffer(file);
});
}
const readDicomFile = (file) => {
return dicomReader(file).then(reader => {
var byteArray = new Uint8Array(reader.result);
console.log("etrxact", reader)
return checkAnnonymisation(byteArray);
});
}
file_set = document.getElementById("filepicker").files;
let output = document.getElementById("listing");
images = [];
for (const file of file_set) {
//await readFile(file);
dicom = await readDicomFile(file);
if (dicom) {
let item = document.createElement("li");
item.textContent = file.webkitRelativePath;
output.appendChild(item);
//await readFile(file, item);
let url = URL.createObjectURL(file)
console.log(url)
imageId = `wadouri:${url}`;
images.push(imageId);
const element = $(`<div class='temp-thumb' src=${imageId}></div>`).get(0)
item.appendChild(element);
cornerstone.enable(element);
cornerstone.loadAndCacheImage(imageId).then(function (image) {
cornerstone.displayImage(element, image);
cornerstone.resize(element)
let bytesView = image.getPixelData()
let str = new TextDecoder("utf-8").decode(bytesView, );
//extractDicomDetails(image.data.byteArray)
});
}
}
console.log(images)
const event = new CustomEvent('loadDicomViewerUrls', {
"detail": {"images": images, "annontations": undefined}
});
window.dispatchEvent(event);
}
$(document).ready(function () {
document.getElementById("filepicker").addEventListener(
"change",
(event) => {
//let output = document.getElementById("listing");
//for (const file of event.target.files) {
// let item = document.createElement("li");
// item.textContent = file.webkitRelativePath;
// output.appendChild(item);
//}
loadDicomViewerTest();
},
false,
);
});
</script>
{% endblock %}
@@ -1,4 +1,5 @@
<div class="floating-header">
<a href="{% url 'atlas:case_detail' pk=case.pk %}" title="View the Case">View</a>
<a href="{% url 'atlas:case_update' pk=case.pk %}" title="Edit the Case">Edit</a>
<a href="{% url 'atlas:case_clone' pk=case.pk %}"
title="Clone the Case (duplicate everything but the images)">Clone</a>
@@ -14,6 +14,10 @@
author: {{resource.get_authors}}<br>
{{resource.case}}
<div>
<a href="{% url 'atlas:resource_view' resource.pk %}" target="_blank">View Resource</a>
</div>
{% if resource.case_set.all %}
<h3>Cases</h3>
-159
View File
@@ -247,165 +247,6 @@
//
// }
function extractDicomDetails(byteArray) {
// We need to setup a try/catch block because parseDicom will throw an exception
// if you attempt to parse a non dicom part 10 file (or one that is corrupted)
try {
console.log("bytearray", byteArray)
// parse byteArray into a DataSet object using the parseDicom library
var dataSet = dicomParser.parseDicom(byteArray);
console.log("ds", dataSet)
// dataSet contains the parsed elements. Each element is available via a property
// in the dataSet.elements object. The property name is based on the elements group
// and element in the following format: xggggeeee where gggg is the group number
// and eeee is the element number with lowercase hex characters.
// To access the data for an element, we need to know its type and its tag.
// We will get the sopInstanceUid from the file which is a string and
// has the tag (0020,000D)
var study_description = dataSet.string('x00081030');
var accession_number = dataSet.string('x00080050').toLowerCase();
var patient_id = dataSet.string('x00100020').toLowerCase();
var modality = dataSet.string('x00080060');
var projection = dataSet.string('x00185101');
var plane = dataSet.string('x00180022').toLowerCase();
console.log("STUDY", study_description);
console.log("acc", accession_number);
console.log("pid", patient_id);
console.log("projection", projection);
console.log("plane", plane);
function select_plane(option) {
let plane_select_object = document.getElementsByName("plane")[0]
const $options = Array.from(plane_select_object.options);
const optionToSelect = $options.find(item => item.text === option);
optionToSelect.selected = true;
}
if (plane.includes("axial")) {
select_plane("axial");
} else if (plane.includes("sagittal")) {
select_plane("sagittal");
} else if (plane.includes("coronal")) {
select_plane("coronal");
} else if (plane.includes("ap")) {
select_plane("AP");
} else if (plane.includes("lat") | plane.includes("lateral")) {
select_plane("lateral");
} else if (plane.includes("pa")) {
select_plane("PA");
}
modality_map = {
"DX": "Radiograph",
"CR": "Radiograph",
"CT": "CT",
"MR": "MR",
"RF": "Fluroscopy",
"US": "Ultrasound",
}
if (modality in modality_map) {
let modality_select_object = document.getElementsByName("modality")[0]
const $options = Array.from(modality_select_object.options);
const optionToSelect = $options.find(item => item.text === modality_map[modality]);
optionToSelect.selected = true;
} else {
toastr.info(`Unable to automatically set modality: ${modality}`);
}
// If the dicom study_description contains multiple values select the first
study_description = study_description.split(",")[0].trim();
to_replace = ["lt", "rt", "contrast", "LT", "RT", "Contrast"];
for (let i = 0; i < to_replace.length; i++) {
study_description = study_description.replace(to_replace[i], "");
}
let select_object = document.getElementsByName("examination")[0]
if (select_object.value == "") {
$.get(`${select_object.dataset.autocompleteLightUrl}?q=${study_description}`, (data) => {
console.log(data)
if (data.results.length==1){
$(select_object).select2("trigger","select", {
data: data.results[0]
})
}
})
}
not_anon = false;
site_codes = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"]
for (let i = 0; i < site_codes.length; i++) {
if (
accession_number.startsWith(site_codes[i])
|| patient_id.startsWith(site_codes[i])) {
not_anon = true;
}
}
if (not_anon) {
toastr.warning(`File does not appear to be annonymised<br/>Accession: ${accession_number} <br/>Patient ID: ${patient_id}<br/>`, 'Anonymisation warning', {
"closeButton": false,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-full-width",
"preventDuplicates": false,
"onclick": null,
"showDuration": "0",
"hideDuration": "0",
"timeOut": 0,
"extendedTimeOut": 0,
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut",
});
}
// TODO: fix
//$.ajax({
// url: `{% url 'generic:examination-autocomplete' %}/${encodeURI(study_description)}`,
// //context: document.body,
// success: function(data){
// console.log(data)
// }
//});
//// try to match with a study description (if not already selected)
//if ($(`#id_examination_to option`).length < 1) {
// option = $(`#id_examination_from option[title*='${study_description}' i]`);
// if (option.length) {
// option.appendTo($("#id_examination_to"));
// toastr.success(`Examination set to ${option.attr('title')} from dicom data`);
// } else {
// toastr.warning(
// `Unable to set examination ${study_description} from dicom data (it needs creating)`);
// }
//}
} catch (err) {
console.log(err);
}
}
</script>
{{ form.media }}
+2
View File
@@ -39,11 +39,13 @@ urlpatterns = [
path("uploads", views.user_uploads, name="user_uploads"),
path("uploads/<int:user_pk>/user", views.other_user_uploads, name="other_user_uploads"),
path("uploads/all", views.all_uploads, name="all_uploads"),
path("uploads/new", views.new_uploads, name="new_uploads"),
path("uploads/case/<int:case_id>", views.user_uploads, name="user_uploads_case"),
path("uploads/<int:user_pk>/user/case/<int:case_id>", views.other_user_uploads, name="other_user_uploads_case"),
path("uploads/series_id/<str:series_instance_uid>", views.user_uploads_series, name="user_uploads_series"),
path("resource/", views.ResourceView.as_view(), name="resource_view"),
path("resource/<int:pk>/", views.resource_detail, name="resource_detail"),
path("resource/<int:pk>/view", views.resource_view, name="resource_view"),
path("resource/<int:pk>/update", views.ResourceUpdate.as_view(), name="resource_update"),
path("resource/<int:pk>/delete", views.ResourceDelete.as_view(), name="resource_delete"),
path(
+9
View File
@@ -370,6 +370,12 @@ def resource_detail(request, pk):
return render(request, "atlas/resource_detail.html", {"resource": resource})
@login_required
def resource_view(request, pk):
resource = get_object_or_404(Resource, pk=pk)
return HttpResponse(resource.get_display())
@login_required
def author_detail(request, pk):
@@ -444,6 +450,9 @@ def user_uploads_series(request, series_instance_uid: str):
{"dicoms": dicoms, "series_id": series_instance_uid},
)
@login_required
def new_uploads(request):
return render(request, "atlas/new_uploads.html", {})
@login_required
@user_passes_test(lambda u: u.is_superuser)
@@ -1,6 +1,7 @@
<br/>
{% if can_edit %}
<a href="{% url exam.get_app_name|add:':exam_update' exam.id %}" title="Edit the Exam">Edit</a>
<a href="{% url exam.get_app_name|add:':exam_dteail' exam.id %}" title="View the exam">Edit</a>
\ <a href="{% url exam.get_app_name|add:':exam_update' exam.id %}" title="Edit the Exam">Edit</a>
\ <a href="{% url exam.get_app_name|add:':exam_delete' exam.id %}" title="Delete the Exam">Delete</a>
\ <a href="{% url exam.get_app_name|add:':exam_clone' exam.id %}" title="Clone the Exam">Clone</a>
\ <a href="{% url exam.get_app_name|add:':exam_authors' exam.id %}" title="Edit Exam Authors">Authors</a>
+13
View File
@@ -1109,4 +1109,17 @@ tr:has(> td > a) {
padding: 0;
text-decoration: underline;
color: #3282b8;
}
.resource-block {
max-width: fit-content;
}
.resource-block video {
max-width: 100%;
}
.no-list-style {
list-style-type: none;
padding-left: 1rem;
}