1550 lines
46 KiB
HTML
1550 lines
46 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}DICOM Analysis - {{ file.filename }}{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
|
<style>
|
|
.analysis-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.analysis-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 350px;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.viewer-section {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.viewer-header {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 1.5rem;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
}
|
|
|
|
.viewer-controls {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
align-items: center;
|
|
padding: 0.25rem 0.5rem;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.control-btn {
|
|
padding: 0.375rem 0.5rem;
|
|
border: none;
|
|
background: transparent;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.control-btn:hover, .control-btn.active {
|
|
background: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
.viewer-canvas {
|
|
position: relative;
|
|
background: #000;
|
|
min-height: 500px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dicom-image {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
cursor: crosshair;
|
|
}
|
|
|
|
.image-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.measurement-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.measurement-line {
|
|
stroke: #00ff00;
|
|
stroke-width: 2;
|
|
fill: none;
|
|
}
|
|
|
|
.measurement-text {
|
|
fill: #00ff00;
|
|
font-size: 12px;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.image-info-overlay {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.windowing-controls {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 0.5rem;
|
|
border-radius: 0.25rem;
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.windowing-slider {
|
|
width: 100px;
|
|
}
|
|
|
|
.analysis-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.panel-section {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 1.5rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.analysis-tabs {
|
|
border-bottom: 1px solid #dee2e6;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.nav-tabs .nav-link {
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
color: #6c757d;
|
|
font-weight: 600;
|
|
padding: 0.75rem 1rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.nav-tabs .nav-link.active {
|
|
color: #007bff;
|
|
border-bottom-color: #007bff;
|
|
background: none;
|
|
}
|
|
|
|
.measurement-item {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid #f1f3f4;
|
|
}
|
|
|
|
.measurement-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.measurement-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.measurement-value {
|
|
color: #28a745;
|
|
font-weight: bold;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.measurement-actions {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.btn-measurement {
|
|
padding: 0.25rem 0.5rem;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.btn-edit { background: #fff3e0; color: #f57c00; }
|
|
.btn-delete { background: #ffebee; color: #d32f2f; }
|
|
|
|
.histogram-container {
|
|
width: 100%;
|
|
height: 200px;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
position: relative;
|
|
}
|
|
|
|
.histogram-canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.roi-list {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.roi-item {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
padding: 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 0.5rem;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.roi-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.roi-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.roi-stats {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.roi-actions {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.analysis-results {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.result-item {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.result-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.result-value {
|
|
color: #007bff;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.tool-palette {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.tool-btn {
|
|
padding: 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-align: center;
|
|
font-size: 0.75rem;
|
|
color: #495057;
|
|
}
|
|
|
|
.tool-btn:hover, .tool-btn.active {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.tool-icon {
|
|
font-size: 1.25rem;
|
|
margin-bottom: 0.25rem;
|
|
display: block;
|
|
}
|
|
|
|
.export-options {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.export-btn {
|
|
padding: 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-align: center;
|
|
font-size: 0.875rem;
|
|
color: #495057;
|
|
}
|
|
|
|
.export-btn:hover {
|
|
background: #f8f9fa;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.progress-indicator {
|
|
background: #e9ecef;
|
|
border-radius: 0.25rem;
|
|
height: 4px;
|
|
overflow: hidden;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.progress-bar {
|
|
background: #007bff;
|
|
height: 100%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.annotation-list {
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.annotation-item {
|
|
padding: 0.5rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 0.5rem;
|
|
background: white;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.annotation-text {
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.annotation-meta {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.analysis-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.analysis-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.viewer-controls {
|
|
flex-wrap: wrap;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.control-group {
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.tool-palette {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.windowing-controls {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.analysis-panel, .viewer-controls, .btn {
|
|
display: none !important;
|
|
}
|
|
|
|
.analysis-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.section-header {
|
|
background: none;
|
|
border-bottom: 2px solid #000;
|
|
color: #000;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<!-- Page Header -->
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div>
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'radiology:dashboard' %}">Radiology</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'radiology:dicom_file_list' %}">DICOM Files</a></li>
|
|
<li class="breadcrumb-item active">Analysis</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-search me-2"></i>DICOM Analysis
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<button type="button" class="btn btn-outline-secondary me-2" onclick="resetAnalysis()">
|
|
<i class="fas fa-undo me-1"></i>Reset
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info me-2" onclick="saveAnalysis()">
|
|
<i class="fas fa-save me-1"></i>Save Analysis
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success me-2" onclick="generateReport()">
|
|
<i class="fas fa-file-alt me-1"></i>Generate Report
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="exportResults()">
|
|
<i class="fas fa-download me-1"></i>Export Results
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analysis Header -->
|
|
<div class="analysis-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h3 class="mb-2">{{ file.filename }}</h3>
|
|
<p class="mb-2">{{ file.patient_name|default:"Unknown Patient" }} - {{ file.study_description|default:"No description" }}</p>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<span class="badge bg-primary">{{ file.modality|default:"Unknown" }}</span>
|
|
<span class="badge bg-info">{{ file.rows|default:0 }}x{{ file.columns|default:0 }}</span>
|
|
<span class="badge bg-success">{{ file.file_size|filesizeformat }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="text-white-50 mb-1">Analysis Started</div>
|
|
<div class="h6 mb-2" id="analysis-time">{{ analysis.started_at|date:"M d, Y g:i A"|default:"Just now" }}</div>
|
|
<div class="text-white-50">Status: <span id="analysis-status">In Progress</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="analysis-layout">
|
|
<!-- DICOM Viewer -->
|
|
<div class="viewer-section">
|
|
<div class="viewer-header">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="fas fa-eye me-2"></i>
|
|
<span class="fw-bold">DICOM Viewer</span>
|
|
<span class="badge bg-secondary" id="zoom-level">100%</span>
|
|
</div>
|
|
|
|
<div class="viewer-controls">
|
|
<!-- Zoom Controls -->
|
|
<div class="control-group">
|
|
<button type="button" class="control-btn" onclick="zoomIn()" title="Zoom In">
|
|
<i class="fas fa-search-plus"></i>
|
|
</button>
|
|
<button type="button" class="control-btn" onclick="zoomOut()" title="Zoom Out">
|
|
<i class="fas fa-search-minus"></i>
|
|
</button>
|
|
<button type="button" class="control-btn" onclick="resetZoom()" title="Reset Zoom">
|
|
<i class="fas fa-expand-arrows-alt"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Pan Controls -->
|
|
<div class="control-group">
|
|
<button type="button" class="control-btn active" onclick="setTool('pan')" title="Pan">
|
|
<i class="fas fa-hand-paper"></i>
|
|
</button>
|
|
<button type="button" class="control-btn" onclick="setTool('measure')" title="Measure">
|
|
<i class="fas fa-ruler"></i>
|
|
</button>
|
|
<button type="button" class="control-btn" onclick="setTool('roi')" title="ROI">
|
|
<i class="fas fa-draw-polygon"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Window/Level -->
|
|
<div class="control-group">
|
|
<button type="button" class="control-btn" onclick="autoWindow()" title="Auto Window">
|
|
<i class="fas fa-adjust"></i>
|
|
</button>
|
|
<button type="button" class="control-btn" onclick="invertImage()" title="Invert">
|
|
<i class="fas fa-adjust"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Presets -->
|
|
<div class="control-group">
|
|
<select class="form-select form-select-sm" onchange="applyPreset(this.value)">
|
|
<option value="">Window Presets</option>
|
|
<option value="lung">Lung</option>
|
|
<option value="bone">Bone</option>
|
|
<option value="soft_tissue">Soft Tissue</option>
|
|
<option value="brain">Brain</option>
|
|
<option value="liver">Liver</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="viewer-canvas" id="viewer-canvas">
|
|
{% if file.thumbnail %}
|
|
<img src="{{ file.thumbnail.url }}" alt="DICOM Image" class="dicom-image" id="dicom-image">
|
|
{% else %}
|
|
<div class="text-center text-muted">
|
|
<i class="fas fa-file-medical fa-3x mb-3"></i>
|
|
<h5>Loading DICOM Image...</h5>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Image Info Overlay -->
|
|
<div class="image-info-overlay" id="image-info">
|
|
<div>Patient: {{ file.patient_name|default:"Unknown" }}</div>
|
|
<div>Study: {{ file.study_date|date:"M d, Y"|default:"N/A" }}</div>
|
|
<div>Series: {{ file.series_number|default:"N/A" }}</div>
|
|
<div>Instance: {{ file.instance_number|default:"N/A" }}</div>
|
|
</div>
|
|
|
|
<!-- Windowing Controls -->
|
|
<div class="windowing-controls">
|
|
<div>
|
|
<label class="form-label mb-1">Window</label>
|
|
<input type="range" class="windowing-slider" id="window-width"
|
|
min="1" max="4000" value="400" onchange="updateWindowing()">
|
|
<div class="text-center" id="window-value">400</div>
|
|
</div>
|
|
<div>
|
|
<label class="form-label mb-1">Level</label>
|
|
<input type="range" class="windowing-slider" id="window-level"
|
|
min="-1000" max="3000" value="40" onchange="updateWindowing()">
|
|
<div class="text-center" id="level-value">40</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Measurement Overlay -->
|
|
<svg class="measurement-overlay" id="measurement-overlay">
|
|
<!-- Measurements will be drawn here -->
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analysis Panel -->
|
|
<div class="analysis-panel">
|
|
<!-- Tools -->
|
|
<div class="panel-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-tools me-2"></i>Analysis Tools
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="tool-palette">
|
|
<button type="button" class="tool-btn active" onclick="selectTool('pointer')" data-tool="pointer">
|
|
<i class="fas fa-mouse-pointer tool-icon"></i>
|
|
Pointer
|
|
</button>
|
|
<button type="button" class="tool-btn" onclick="selectTool('measure')" data-tool="measure">
|
|
<i class="fas fa-ruler tool-icon"></i>
|
|
Measure
|
|
</button>
|
|
<button type="button" class="tool-btn" onclick="selectTool('angle')" data-tool="angle">
|
|
<i class="fas fa-drafting-compass tool-icon"></i>
|
|
Angle
|
|
</button>
|
|
<button type="button" class="tool-btn" onclick="selectTool('roi')" data-tool="roi">
|
|
<i class="fas fa-draw-polygon tool-icon"></i>
|
|
ROI
|
|
</button>
|
|
<button type="button" class="tool-btn" onclick="selectTool('ellipse')" data-tool="ellipse">
|
|
<i class="fas fa-circle tool-icon"></i>
|
|
Ellipse
|
|
</button>
|
|
<button type="button" class="tool-btn" onclick="selectTool('annotation')" data-tool="annotation">
|
|
<i class="fas fa-comment tool-icon"></i>
|
|
Annotate
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Measurements -->
|
|
<div class="panel-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-ruler me-2"></i>Measurements
|
|
</div>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="clearMeasurements()">
|
|
<i class="fas fa-trash me-1"></i>Clear All
|
|
</button>
|
|
</div>
|
|
<div class="section-content">
|
|
<div id="measurements-list">
|
|
<!-- Measurements will be populated here -->
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-ruler fa-2x mb-2"></i>
|
|
<div>No measurements yet</div>
|
|
<small>Use the measurement tools to add measurements</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ROI Analysis -->
|
|
<div class="panel-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-chart-area me-2"></i>ROI Analysis
|
|
</div>
|
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="analyzeROIs()">
|
|
<i class="fas fa-play me-1"></i>Analyze
|
|
</button>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="roi-list" id="roi-list">
|
|
<!-- ROIs will be populated here -->
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-draw-polygon fa-2x mb-2"></i>
|
|
<div>No ROIs defined</div>
|
|
<small>Draw regions of interest for analysis</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analysis Results -->
|
|
<div class="panel-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-chart-bar me-2"></i>Analysis Results
|
|
</div>
|
|
<div class="section-content">
|
|
<ul class="nav nav-tabs analysis-tabs" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#statistics-tab">Statistics</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#histogram-tab">Histogram</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#annotations-tab">Notes</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content">
|
|
<!-- Statistics Tab -->
|
|
<div class="tab-pane fade show active" id="statistics-tab">
|
|
<div class="analysis-results" id="statistics-results">
|
|
<div class="result-item">
|
|
<div class="result-label">Mean Intensity:</div>
|
|
<div class="result-value" id="mean-intensity">-</div>
|
|
</div>
|
|
<div class="result-item">
|
|
<div class="result-label">Standard Deviation:</div>
|
|
<div class="result-value" id="std-deviation">-</div>
|
|
</div>
|
|
<div class="result-item">
|
|
<div class="result-label">Min/Max Values:</div>
|
|
<div class="result-value" id="min-max-values">-</div>
|
|
</div>
|
|
<div class="result-item">
|
|
<div class="result-label">Area (pixels):</div>
|
|
<div class="result-value" id="area-pixels">-</div>
|
|
</div>
|
|
<div class="result-item">
|
|
<div class="result-label">Area (mm²):</div>
|
|
<div class="result-value" id="area-mm">-</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Histogram Tab -->
|
|
<div class="tab-pane fade" id="histogram-tab">
|
|
<div class="histogram-container">
|
|
<canvas class="histogram-canvas" id="histogram-canvas"></canvas>
|
|
</div>
|
|
<div class="mt-2">
|
|
<small class="text-muted">Pixel intensity distribution</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Annotations Tab -->
|
|
<div class="tab-pane fade" id="annotations-tab">
|
|
<div class="mb-3">
|
|
<textarea class="form-control" id="new-annotation" rows="2"
|
|
placeholder="Add a note or annotation..."></textarea>
|
|
<button type="button" class="btn btn-primary btn-sm mt-2" onclick="addAnnotation()">
|
|
<i class="fas fa-plus me-1"></i>Add Note
|
|
</button>
|
|
</div>
|
|
|
|
<div class="annotation-list" id="annotation-list">
|
|
<!-- Annotations will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Export Options -->
|
|
<div class="panel-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-download me-2"></i>Export Options
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="export-options">
|
|
<button type="button" class="export-btn" onclick="exportImage()">
|
|
<i class="fas fa-image mb-2"></i><br>
|
|
Export Image
|
|
</button>
|
|
<button type="button" class="export-btn" onclick="exportMeasurements()">
|
|
<i class="fas fa-table mb-2"></i><br>
|
|
Export Data
|
|
</button>
|
|
<button type="button" class="export-btn" onclick="exportReport()">
|
|
<i class="fas fa-file-pdf mb-2"></i><br>
|
|
PDF Report
|
|
</button>
|
|
<button type="button" class="export-btn" onclick="exportDICOM()">
|
|
<i class="fas fa-file-medical mb-2"></i><br>
|
|
DICOM SR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Measurement Details Modal -->
|
|
<div class="modal fade" id="measurementModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-ruler me-2"></i>Measurement Details
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" id="measurement-label" placeholder="Enter measurement label">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes</label>
|
|
<textarea class="form-control" id="measurement-notes" rows="3"
|
|
placeholder="Add notes about this measurement"></textarea>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Length (pixels)</label>
|
|
<input type="text" class="form-control" id="measurement-pixels" readonly>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Length (mm)</label>
|
|
<input type="text" class="form-control" id="measurement-mm" readonly>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveMeasurement()">
|
|
<i class="fas fa-save me-1"></i>Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<script>
|
|
let currentTool = 'pointer';
|
|
let measurements = [];
|
|
let rois = [];
|
|
let annotations = [];
|
|
let zoomLevel = 1.0;
|
|
let panOffset = { x: 0, y: 0 };
|
|
let isDrawing = false;
|
|
let currentMeasurement = null;
|
|
|
|
$(document).ready(function() {
|
|
initializeViewer();
|
|
loadExistingAnalysis();
|
|
|
|
// Set up canvas event listeners
|
|
const canvas = document.getElementById('viewer-canvas');
|
|
canvas.addEventListener('mousedown', handleMouseDown);
|
|
canvas.addEventListener('mousemove', handleMouseMove);
|
|
canvas.addEventListener('mouseup', handleMouseUp);
|
|
canvas.addEventListener('wheel', handleWheel);
|
|
|
|
// Update analysis time
|
|
updateAnalysisTime();
|
|
setInterval(updateAnalysisTime, 60000); // Update every minute
|
|
});
|
|
|
|
function initializeViewer() {
|
|
// Initialize DICOM viewer
|
|
const image = document.getElementById('dicom-image');
|
|
if (image) {
|
|
image.onload = function() {
|
|
updateImageInfo();
|
|
generateHistogram();
|
|
};
|
|
}
|
|
}
|
|
|
|
function loadExistingAnalysis() {
|
|
// Load existing analysis data if available
|
|
fetch(`/radiology/dicom/{{ file.id }}/analysis/data/`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && data.analysis) {
|
|
measurements = data.analysis.measurements || [];
|
|
rois = data.analysis.rois || [];
|
|
annotations = data.analysis.annotations || [];
|
|
|
|
updateMeasurementsList();
|
|
updateROIList();
|
|
updateAnnotationsList();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading analysis data:', error);
|
|
});
|
|
}
|
|
|
|
function selectTool(tool) {
|
|
currentTool = tool;
|
|
|
|
// Update tool buttons
|
|
document.querySelectorAll('.tool-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
document.querySelector(`[data-tool="${tool}"]`).classList.add('active');
|
|
|
|
// Update cursor
|
|
const canvas = document.getElementById('viewer-canvas');
|
|
switch (tool) {
|
|
case 'measure':
|
|
canvas.style.cursor = 'crosshair';
|
|
break;
|
|
case 'roi':
|
|
canvas.style.cursor = 'crosshair';
|
|
break;
|
|
case 'annotation':
|
|
canvas.style.cursor = 'text';
|
|
break;
|
|
default:
|
|
canvas.style.cursor = 'default';
|
|
}
|
|
}
|
|
|
|
function setTool(tool) {
|
|
selectTool(tool);
|
|
|
|
// Update viewer control buttons
|
|
document.querySelectorAll('.control-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
function handleMouseDown(event) {
|
|
const rect = event.target.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
switch (currentTool) {
|
|
case 'measure':
|
|
startMeasurement(x, y);
|
|
break;
|
|
case 'roi':
|
|
startROI(x, y);
|
|
break;
|
|
case 'annotation':
|
|
addAnnotationAtPoint(x, y);
|
|
break;
|
|
case 'pan':
|
|
startPan(x, y);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleMouseMove(event) {
|
|
if (!isDrawing) return;
|
|
|
|
const rect = event.target.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
switch (currentTool) {
|
|
case 'measure':
|
|
updateMeasurement(x, y);
|
|
break;
|
|
case 'roi':
|
|
updateROI(x, y);
|
|
break;
|
|
case 'pan':
|
|
updatePan(x, y);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleMouseUp(event) {
|
|
if (!isDrawing) return;
|
|
|
|
const rect = event.target.getBoundingClientRect();
|
|
const x = event.clientX - rect.left;
|
|
const y = event.clientY - rect.top;
|
|
|
|
switch (currentTool) {
|
|
case 'measure':
|
|
finishMeasurement(x, y);
|
|
break;
|
|
case 'roi':
|
|
finishROI(x, y);
|
|
break;
|
|
case 'pan':
|
|
finishPan();
|
|
break;
|
|
}
|
|
|
|
isDrawing = false;
|
|
}
|
|
|
|
function handleWheel(event) {
|
|
event.preventDefault();
|
|
|
|
if (event.deltaY < 0) {
|
|
zoomIn();
|
|
} else {
|
|
zoomOut();
|
|
}
|
|
}
|
|
|
|
function startMeasurement(x, y) {
|
|
isDrawing = true;
|
|
currentMeasurement = {
|
|
id: Date.now(),
|
|
type: 'line',
|
|
start: { x, y },
|
|
end: { x, y },
|
|
label: '',
|
|
notes: ''
|
|
};
|
|
}
|
|
|
|
function updateMeasurement(x, y) {
|
|
if (currentMeasurement) {
|
|
currentMeasurement.end = { x, y };
|
|
drawMeasurements();
|
|
}
|
|
}
|
|
|
|
function finishMeasurement(x, y) {
|
|
if (currentMeasurement) {
|
|
currentMeasurement.end = { x, y };
|
|
|
|
// Calculate distance
|
|
const dx = currentMeasurement.end.x - currentMeasurement.start.x;
|
|
const dy = currentMeasurement.end.y - currentMeasurement.start.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
currentMeasurement.distance_pixels = distance;
|
|
currentMeasurement.distance_mm = distance * getPixelSpacing(); // Convert to mm
|
|
|
|
measurements.push(currentMeasurement);
|
|
updateMeasurementsList();
|
|
drawMeasurements();
|
|
|
|
currentMeasurement = null;
|
|
}
|
|
}
|
|
|
|
function startROI(x, y) {
|
|
isDrawing = true;
|
|
// ROI implementation would go here
|
|
}
|
|
|
|
function updateROI(x, y) {
|
|
// ROI update implementation
|
|
}
|
|
|
|
function finishROI(x, y) {
|
|
// ROI finish implementation
|
|
}
|
|
|
|
function startPan(x, y) {
|
|
isDrawing = true;
|
|
// Pan implementation
|
|
}
|
|
|
|
function updatePan(x, y) {
|
|
// Pan update implementation
|
|
}
|
|
|
|
function finishPan() {
|
|
// Pan finish implementation
|
|
}
|
|
|
|
function addAnnotationAtPoint(x, y) {
|
|
const text = prompt('Enter annotation text:');
|
|
if (text) {
|
|
const annotation = {
|
|
id: Date.now(),
|
|
x: x,
|
|
y: y,
|
|
text: text,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
annotations.push(annotation);
|
|
updateAnnotationsList();
|
|
drawAnnotations();
|
|
}
|
|
}
|
|
|
|
function drawMeasurements() {
|
|
const overlay = document.getElementById('measurement-overlay');
|
|
|
|
// Clear existing measurements
|
|
overlay.innerHTML = '';
|
|
|
|
// Draw all measurements
|
|
measurements.forEach(measurement => {
|
|
if (measurement.type === 'line') {
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', measurement.start.x);
|
|
line.setAttribute('y1', measurement.start.y);
|
|
line.setAttribute('x2', measurement.end.x);
|
|
line.setAttribute('y2', measurement.end.y);
|
|
line.setAttribute('class', 'measurement-line');
|
|
overlay.appendChild(line);
|
|
|
|
// Add measurement text
|
|
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
text.setAttribute('x', (measurement.start.x + measurement.end.x) / 2);
|
|
text.setAttribute('y', (measurement.start.y + measurement.end.y) / 2 - 5);
|
|
text.setAttribute('class', 'measurement-text');
|
|
text.textContent = `${measurement.distance_mm.toFixed(1)} mm`;
|
|
overlay.appendChild(text);
|
|
}
|
|
});
|
|
|
|
// Draw current measurement if drawing
|
|
if (currentMeasurement) {
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', currentMeasurement.start.x);
|
|
line.setAttribute('y1', currentMeasurement.start.y);
|
|
line.setAttribute('x2', currentMeasurement.end.x);
|
|
line.setAttribute('y2', currentMeasurement.end.y);
|
|
line.setAttribute('class', 'measurement-line');
|
|
overlay.appendChild(line);
|
|
}
|
|
}
|
|
|
|
function drawAnnotations() {
|
|
// Implementation for drawing annotations
|
|
}
|
|
|
|
function updateMeasurementsList() {
|
|
const list = document.getElementById('measurements-list');
|
|
|
|
if (measurements.length === 0) {
|
|
list.innerHTML = `
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-ruler fa-2x mb-2"></i>
|
|
<div>No measurements yet</div>
|
|
<small>Use the measurement tools to add measurements</small>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
measurements.forEach((measurement, index) => {
|
|
html += `
|
|
<div class="measurement-item">
|
|
<div>
|
|
<div class="measurement-label">
|
|
${measurement.label || `Measurement ${index + 1}`}
|
|
</div>
|
|
<div class="measurement-value">
|
|
${measurement.distance_mm.toFixed(1)} mm (${measurement.distance_pixels.toFixed(0)} px)
|
|
</div>
|
|
</div>
|
|
<div class="measurement-actions">
|
|
<button type="button" class="btn-measurement btn-edit" onclick="editMeasurement(${measurement.id})">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button type="button" class="btn-measurement btn-delete" onclick="deleteMeasurement(${measurement.id})">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
list.innerHTML = html;
|
|
}
|
|
|
|
function updateROIList() {
|
|
const list = document.getElementById('roi-list');
|
|
|
|
if (rois.length === 0) {
|
|
list.innerHTML = `
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-draw-polygon fa-2x mb-2"></i>
|
|
<div>No ROIs defined</div>
|
|
<small>Draw regions of interest for analysis</small>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// ROI list implementation
|
|
}
|
|
|
|
function updateAnnotationsList() {
|
|
const list = document.getElementById('annotation-list');
|
|
|
|
let html = '';
|
|
annotations.forEach(annotation => {
|
|
html += `
|
|
<div class="annotation-item">
|
|
<div class="annotation-text">${annotation.text}</div>
|
|
<div class="annotation-meta">
|
|
${new Date(annotation.timestamp).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
list.innerHTML = html;
|
|
}
|
|
|
|
function zoomIn() {
|
|
zoomLevel *= 1.2;
|
|
updateZoom();
|
|
}
|
|
|
|
function zoomOut() {
|
|
zoomLevel /= 1.2;
|
|
updateZoom();
|
|
}
|
|
|
|
function resetZoom() {
|
|
zoomLevel = 1.0;
|
|
updateZoom();
|
|
}
|
|
|
|
function updateZoom() {
|
|
const image = document.getElementById('dicom-image');
|
|
if (image) {
|
|
image.style.transform = `scale(${zoomLevel})`;
|
|
}
|
|
|
|
document.getElementById('zoom-level').textContent = `${Math.round(zoomLevel * 100)}%`;
|
|
}
|
|
|
|
function updateWindowing() {
|
|
const windowWidth = document.getElementById('window-width').value;
|
|
const windowLevel = document.getElementById('window-level').value;
|
|
|
|
document.getElementById('window-value').textContent = windowWidth;
|
|
document.getElementById('level-value').textContent = windowLevel;
|
|
|
|
// Apply windowing to image (implementation would depend on DICOM library)
|
|
applyWindowLevel(windowWidth, windowLevel);
|
|
}
|
|
|
|
function applyWindowLevel(width, level) {
|
|
// Implementation for applying window/level to DICOM image
|
|
}
|
|
|
|
function applyPreset(preset) {
|
|
const presets = {
|
|
lung: { width: 1500, level: -600 },
|
|
bone: { width: 2000, level: 300 },
|
|
soft_tissue: { width: 400, level: 40 },
|
|
brain: { width: 80, level: 40 },
|
|
liver: { width: 150, level: 30 }
|
|
};
|
|
|
|
if (presets[preset]) {
|
|
document.getElementById('window-width').value = presets[preset].width;
|
|
document.getElementById('window-level').value = presets[preset].level;
|
|
updateWindowing();
|
|
}
|
|
}
|
|
|
|
function autoWindow() {
|
|
// Auto window/level implementation
|
|
showAlert('Auto windowing applied', 'info');
|
|
}
|
|
|
|
function invertImage() {
|
|
const image = document.getElementById('dicom-image');
|
|
if (image.style.filter.includes('invert')) {
|
|
image.style.filter = image.style.filter.replace('invert(1)', '');
|
|
} else {
|
|
image.style.filter += ' invert(1)';
|
|
}
|
|
}
|
|
|
|
function clearMeasurements() {
|
|
if (measurements.length === 0) {
|
|
showAlert('No measurements to clear', 'info');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Are you sure you want to clear all measurements?')) {
|
|
measurements = [];
|
|
updateMeasurementsList();
|
|
drawMeasurements();
|
|
showAlert('All measurements cleared', 'success');
|
|
}
|
|
}
|
|
|
|
function deleteMeasurement(id) {
|
|
measurements = measurements.filter(m => m.id !== id);
|
|
updateMeasurementsList();
|
|
drawMeasurements();
|
|
}
|
|
|
|
function editMeasurement(id) {
|
|
const measurement = measurements.find(m => m.id === id);
|
|
if (measurement) {
|
|
document.getElementById('measurement-label').value = measurement.label || '';
|
|
document.getElementById('measurement-notes').value = measurement.notes || '';
|
|
document.getElementById('measurement-pixels').value = measurement.distance_pixels.toFixed(0);
|
|
document.getElementById('measurement-mm').value = measurement.distance_mm.toFixed(1);
|
|
|
|
// Store current measurement ID for saving
|
|
window.currentEditingMeasurement = id;
|
|
|
|
new bootstrap.Modal(document.getElementById('measurementModal')).show();
|
|
}
|
|
}
|
|
|
|
function saveMeasurement() {
|
|
const id = window.currentEditingMeasurement;
|
|
const measurement = measurements.find(m => m.id === id);
|
|
|
|
if (measurement) {
|
|
measurement.label = document.getElementById('measurement-label').value;
|
|
measurement.notes = document.getElementById('measurement-notes').value;
|
|
|
|
updateMeasurementsList();
|
|
bootstrap.Modal.getInstance(document.getElementById('measurementModal')).hide();
|
|
showAlert('Measurement updated', 'success');
|
|
}
|
|
}
|
|
|
|
function analyzeROIs() {
|
|
if (rois.length === 0) {
|
|
showAlert('No ROIs to analyze', 'warning');
|
|
return;
|
|
}
|
|
|
|
// ROI analysis implementation
|
|
showAlert('ROI analysis started', 'info');
|
|
}
|
|
|
|
function generateHistogram() {
|
|
const canvas = document.getElementById('histogram-canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Generate sample histogram data
|
|
const data = [];
|
|
const labels = [];
|
|
|
|
for (let i = 0; i < 256; i++) {
|
|
labels.push(i);
|
|
data.push(Math.random() * 1000);
|
|
}
|
|
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Pixel Count',
|
|
data: data,
|
|
borderColor: '#007bff',
|
|
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
borderWidth: 1,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Pixel Intensity'
|
|
}
|
|
},
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'Count'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function addAnnotation() {
|
|
const text = document.getElementById('new-annotation').value.trim();
|
|
if (!text) {
|
|
showAlert('Please enter annotation text', 'warning');
|
|
return;
|
|
}
|
|
|
|
const annotation = {
|
|
id: Date.now(),
|
|
text: text,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
annotations.push(annotation);
|
|
updateAnnotationsList();
|
|
|
|
document.getElementById('new-annotation').value = '';
|
|
showAlert('Annotation added', 'success');
|
|
}
|
|
|
|
function updateImageInfo() {
|
|
// Update image information overlay
|
|
const info = document.getElementById('image-info');
|
|
// Implementation would update with actual DICOM metadata
|
|
}
|
|
|
|
function updateAnalysisTime() {
|
|
const timeElement = document.getElementById('analysis-time');
|
|
const statusElement = document.getElementById('analysis-status');
|
|
|
|
// Update with current time
|
|
timeElement.textContent = new Date().toLocaleString();
|
|
statusElement.textContent = 'Active';
|
|
}
|
|
|
|
function getPixelSpacing() {
|
|
// Return pixel spacing in mm (would come from DICOM metadata)
|
|
return 0.5; // Default 0.5mm per pixel
|
|
}
|
|
|
|
function resetAnalysis() {
|
|
if (confirm('Are you sure you want to reset the entire analysis? This will clear all measurements, ROIs, and annotations.')) {
|
|
measurements = [];
|
|
rois = [];
|
|
annotations = [];
|
|
|
|
updateMeasurementsList();
|
|
updateROIList();
|
|
updateAnnotationsList();
|
|
drawMeasurements();
|
|
|
|
showAlert('Analysis reset', 'success');
|
|
}
|
|
}
|
|
|
|
function saveAnalysis() {
|
|
const analysisData = {
|
|
measurements: measurements,
|
|
rois: rois,
|
|
annotations: annotations,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
fetch(`/radiology/dicom/{{ file.id }}/analysis/save/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(analysisData)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Analysis saved successfully', 'success');
|
|
} else {
|
|
showAlert('Error saving analysis', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error saving analysis', 'danger');
|
|
});
|
|
}
|
|
|
|
function generateReport() {
|
|
window.open(`/radiology/dicom/{{ file.id }}/analysis/report/`, '_blank');
|
|
}
|
|
|
|
function exportResults() {
|
|
window.open(`/radiology/dicom/{{ file.id }}/analysis/export/`, '_blank');
|
|
}
|
|
|
|
function exportImage() {
|
|
// Export current view as image
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const image = document.getElementById('dicom-image');
|
|
|
|
canvas.width = image.naturalWidth;
|
|
canvas.height = image.naturalHeight;
|
|
ctx.drawImage(image, 0, 0);
|
|
|
|
// Download the image
|
|
const link = document.createElement('a');
|
|
link.download = `${file.filename}_analysis.png`;
|
|
link.href = canvas.toDataURL();
|
|
link.click();
|
|
}
|
|
|
|
function exportMeasurements() {
|
|
// Export measurements as CSV
|
|
let csv = 'Label,Distance (mm),Distance (pixels),Notes\n';
|
|
measurements.forEach((measurement, index) => {
|
|
csv += `"${measurement.label || `Measurement ${index + 1}`}",${measurement.distance_mm.toFixed(1)},${measurement.distance_pixels.toFixed(0)},"${measurement.notes || ''}"\n`;
|
|
});
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const link = document.createElement('a');
|
|
link.download = `${file.filename}_measurements.csv`;
|
|
link.href = URL.createObjectURL(blob);
|
|
link.click();
|
|
}
|
|
|
|
function exportReport() {
|
|
window.open(`/radiology/dicom/{{ file.id }}/analysis/report/pdf/`, '_blank');
|
|
}
|
|
|
|
function exportDICOM() {
|
|
window.open(`/radiology/dicom/{{ file.id }}/analysis/dicom-sr/`, '_blank');
|
|
}
|
|
|
|
function showAlert(message, type) {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
|
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
|
|
alertDiv.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(alertDiv);
|
|
|
|
setTimeout(() => {
|
|
if (alertDiv.parentNode) {
|
|
alertDiv.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|