Skip to content

Commit a89ed2e

Browse files
authored
Decode HTJ2K (#451)
Support decoding HTJ2K images in Transfer Syntax 3.2.10008.1.2.4.96 Also adds support to fetch either single or multipart data, and an interceptor to rename the request before open (eg to add single part header requests or other redirections).
1 parent bd130d4 commit a89ed2e

18 files changed

+1432
-2296
lines changed

.eslintrc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module.exports = {
77
},
88
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
99
plugins: ['import'],
10-
parser: 'babel-eslint',
10+
parser: '@babel/eslint-parser',
1111
parserOptions: {
1212
sourceType: 'module',
1313
},

.vscode/settings.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@
2626
],
2727
"prettier.disableLanguages": [],
2828
"prettier.endOfLine": "lf",
29-
"workbench.colorCustomizations": {}
29+
"workbench.colorCustomizations": {},
30+
"editor.codeActionsOnSave": {
31+
"source.fixAll.eslint": true
32+
}
3033
}

config/karma/karma-base.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ delete webpackConfig.entry;
1818
webpackConfig.module.rules.push({
1919
test: /\.js$/,
2020
include: path.resolve('./src/'),
21-
loader: 'istanbul-instrumenter-loader',
21+
loader: 'coverage-istanbul-loader',
2222
options: {
2323
esModules: true,
2424
},

examples/dicomfile/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ <h1>Example of displaying a DICOM P10 from the local file system</h1>
109109
<script src="../dicomParser.min.js"></script>
110110

111111
<!-- include the cornerstoneWADOImageLoader library -->
112-
<script src="../dist/cornerstoneWADOImageLoader.bundle.min.js"></script>
112+
<script src="../../dist/cornerstoneWADOImageLoader.bundle.min.js"></script>
113113

114114
<!-- uids -->
115115
<script src="uids.js"></script>

examples/index.html

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ <h1>CornerstoneWADOImageLoader Examples</h1>
2222
<a href="wadouri/index.html">WADO-URI (DICOM P10 via HTTP GET)</a>
2323
</li>
2424
<li>
25+
<a href="progressive-jph/index.html">Progressive JPH Display</a>
26+
</li>
27+
<li>
2528
<a href="wadourimultiframe/index.html"
2629
>WADO-URI (DICOM P10 via HTTP GET) multiframe</a
2730
>

examples/progressive-jph/index.html

+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
4+
<head>
5+
<!-- twitter bootstrap CSS stylesheet - included to make things pretty, not needed or used by cornerstone -->
6+
<link href="../bootstrap.min.css" rel="stylesheet">
7+
<link href="../cornerstone.min.css" rel="stylesheet">
8+
</head>
9+
10+
<body>
11+
<div class="container">
12+
<div class="page-header">
13+
<h1>Example of displaying progressive resolution HTJ2K DICOM P10 image using Cornerstone</h1>
14+
</div>
15+
<br>
16+
<div id="viewports">
17+
18+
</div>
19+
<br>
20+
<!-- <div class="row">
21+
<div class="col-md-6">
22+
<div style="width:512px;height:512px;position:relative;color: white;display:inline-block;border-style:solid;border-color:black;"
23+
oncontextmenu="return false" class='disable-selection noIbar' unselectable='on'
24+
onselectstart='return false;' onmousedown='return false;'>
25+
<div id="dicomImage" style="width:512px;height:512px;top:0px;left:0px; position:absolute">
26+
</div>
27+
</div>
28+
</div>
29+
<div class="col-md-6">
30+
<span>Transfer Syntax: </span><span id="transferSyntax"></span><br>
31+
<span>SOP Class: </span><span id="sopClass"></span><br>
32+
<span>Samples Per Pixel: </span><span id="samplesPerPixel"></span><br>
33+
<span>Photometric Interpretation: </span><span id="photometricInterpretation"></span><br>
34+
<span>Number Of Frames: </span><span id="numberOfFrames"></span><br>
35+
<span>Planar Configuration: </span><span id="planarConfiguration"></span><br>
36+
<span>Rows: </span><span id="rows"></span><br>
37+
<span>Columns: </span><span id="columns"></span><br>
38+
<span>Pixel Spacing: </span><span id="pixelSpacing"></span><br>
39+
<span>Row Pixel Spacing: </span><span id="rowPixelSpacing"></span><br>
40+
<span>Column Pixel Spacing: </span><span id="columnPixelSpacing"></span><br>
41+
<span>Bits Allocated: </span><span id="bitsAllocated"></span><br>
42+
<span>Bits Stored: </span><span id="bitsStored"></span><br>
43+
<span>High Bit: </span><span id="highBit"></span><br>
44+
<span>Pixel Representation: </span><span id="pixelRepresentation"></span><br>
45+
<span>WindowCenter: </span><span id="windowCenter"></span><br>
46+
<span>WindowWidth: </span><span id="windowWidth"></span><br>
47+
<span>RescaleIntercept: </span><span id="rescaleIntercept"></span><br>
48+
<span>RescaleSlope: </span><span id="rescaleSlope"></span><br>
49+
<span>Basic Offset Table Entries: </span><span id="basicOffsetTable"></span><br>
50+
<span>Fragments: </span><span id="fragments"></span><br>
51+
<span>Max Stored Pixel Value: </span><span id="minStoredPixelValue"></span><br>
52+
<span>Min Stored Pixel Value: </span><span id="maxStoredPixelValue"></span><br>
53+
<span>Total Time: </span><span id="totalTime"></span><br>
54+
<span>Load Time: </span><span id="loadTime"></span><br>
55+
<span>Decode Time: </span><span id="decodeTime"></span><br>
56+
</div>
57+
</div> -->
58+
</div>
59+
</body>
60+
61+
<!-- include the cornerstone library -->
62+
<script src="../cornerstone.min.js"></script>
63+
<SCRIPT src="../cornerstoneMath.min.js"></SCRIPT>
64+
<SCRIPT src="../cornerstoneTools.min.js"></SCRIPT>
65+
66+
<!-- include the dicomParser library as the WADO image loader depends on it -->
67+
<script src="../dicomParser.min.js"></script>
68+
69+
<!-- include the cornerstoneWADOImageLoader library -->
70+
<script src="../dist/cornerstoneWADOImageLoader.bundle.min.js"></script>
71+
72+
<script src="../dicomfile/uids.js"></script>
73+
<script src="../utils/initializeWebWorkers.js"></script>
74+
75+
<script>
76+
77+
// Used `fetch` api here just for simplicity. No reason this functionality
78+
// can't be integrated into the existing xhrRequest.js fuction so that all
79+
// the other default functionality like progress events also work.
80+
async function request(url, imageId, options = { byteRange: '0-100000' }) {
81+
const { byteRange } = options;
82+
83+
const headers = new Headers({
84+
'Range': `bytes=${byteRange}`,
85+
});
86+
87+
try {
88+
const response = await fetch(url, { headers });
89+
const partialFileArrayBuffer = await response.arrayBuffer();
90+
const contentRange = response.headers.get('content-range');
91+
const fileTotalLength = parseInt(contentRange.split('/')[1])
92+
93+
return { arrayBuffer: partialFileArrayBuffer, flags: { isPartialContent: true, fileTotalLength } };
94+
} catch (err) {
95+
console.error(err);
96+
}
97+
}
98+
99+
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
100+
101+
const baseUrls = [
102+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/1.jph.dcm',
103+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-1.jph.dcm',
104+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-2.jph.dcm',
105+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-3.jph.dcm',
106+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-4.jph.dcm',
107+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-5.jph.dcm',
108+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-6.jph.dcm',
109+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-7.jph.dcm',
110+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-8.jph.dcm',
111+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-9.jph.dcm',
112+
'https://pacsbin-util.s3.amazonaws.com/test-images/htj2k/xray-10.jph.dcm',
113+
];
114+
115+
const urls = [];
116+
const cachebust = true;
117+
118+
for (let i = 0; i < 20; i += 1) {
119+
let url = baseUrls[i % 11];
120+
if (cachebust) {
121+
url += '?cachebust=' + (Math.random() + 1).toString(36).substring(7);
122+
}
123+
urls.push(url)
124+
}
125+
126+
urls.forEach(url => {
127+
// Create element
128+
const viewportContainer = document.getElementById('viewports');
129+
const viewportDiv = document.createElement('div');
130+
viewportDiv.classList = 'disable-selection noIbar'
131+
viewportDiv.style = 'width:200px;height:200px;position:relative;color: white;display:inline-block;border-style:solid;border-color:black;'
132+
viewportDiv.onmousedown = 'return false;'
133+
const canvasDiv = document.createElement('div');
134+
canvasDiv.style = 'width:200px;height:200px;';
135+
136+
viewportDiv.append(canvasDiv)
137+
viewportContainer.append(viewportDiv)
138+
139+
const element = canvasDiv;
140+
cornerstone.enable(element);
141+
const imageId = `wadouri:${url}`;
142+
let loaded = false;
143+
144+
canvasDiv.onmouseover = () => {
145+
canvasDiv.style.position = 'absolute'
146+
canvasDiv.style.width = '512px'
147+
canvasDiv.style.height = '512px'
148+
canvasDiv.style.zIndex = '10';
149+
cornerstone.resize(element, true);
150+
}
151+
canvasDiv.onmouseout = () => {
152+
canvasDiv.style.position = 'relative';
153+
canvasDiv.style.width = '200px'
154+
canvasDiv.style.height = '200px'
155+
canvasDiv.style.zIndex = '1';
156+
cornerstone.resize(element, true);
157+
}
158+
159+
// Load more of the image on click
160+
canvasDiv.addEventListener('click', async function (e) {
161+
const ee = cornerstone.getEnabledElement(element);
162+
const { sharedCacheKey: uri } = ee.image;
163+
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(uri);
164+
165+
const newDataSet = await dataSet.fetchMore({
166+
lengthToFetch: 500000,
167+
});
168+
169+
console.log(newDataSet);
170+
171+
// Not sure how much of this should be handled automatically inside
172+
// cornerstoneWADOImageLoader, vs explicitly in user scripts. It
173+
// would probably be helpful for everything below here to be one
174+
// function call or automatic upon invoking `fetchMore`.
175+
cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.update(uri, newDataSet)
176+
177+
const imageId = "wadouri:" + uri;
178+
179+
cornerstone.imageCache.removeImageLoadObject(imageId)
180+
cornerstone.loadAndCacheImage(imageId).then((image) => {
181+
cornerstone.invalidate(element);
182+
cornerstone.displayImage(element, image)
183+
})
184+
});
185+
186+
try {
187+
var start = new Date().getTime();
188+
cornerstone.loadAndCacheImage(imageId, { loader: request }).then(function (image) {
189+
console.log(image);
190+
var viewport = cornerstone.getDefaultViewportForImage(element, image);
191+
// document.getElementById('toggleModalityLUT').checked = (viewport.modalityLUT !== undefined);
192+
// document.getElementById('toggleVOILUT').checked = (viewport.voiLUT !== undefined);
193+
cornerstone.displayImage(element, image, viewport);
194+
if (loaded === false) {
195+
cornerstoneTools.mouseInput.enable(element);
196+
cornerstoneTools.mouseWheelInput.enable(element);
197+
cornerstoneTools.wwwc.activate(element, 1); // ww/wc is the default tool for left mouse button
198+
cornerstoneTools.pan.activate(element, 2); // pan is the default tool for middle mouse button
199+
cornerstoneTools.zoom.activate(element, 4); // zoom is the default tool for right mouse button
200+
cornerstoneTools.zoomWheel.activate(element); // zoom is the default tool for middle mouse wheel
201+
loaded = true;
202+
}
203+
204+
function getTransferSyntax() {
205+
const value = image.data.string('x00020010');
206+
return value + ' [' + uids[value] + ']';
207+
}
208+
209+
function getSopClass() {
210+
const value = image.data.string('x00080016');
211+
return value + ' [' + uids[value] + ']';
212+
}
213+
214+
function getPixelRepresentation() {
215+
const value = image.data.uint16('x00280103');
216+
if (value === undefined) {
217+
return;
218+
}
219+
return value + (value === 0 ? ' (unsigned)' : ' (signed)');
220+
}
221+
222+
function getPlanarConfiguration() {
223+
const value = image.data.uint16('x00280006');
224+
if (value === undefined) {
225+
return;
226+
}
227+
return value + (value === 0 ? ' (pixel)' : ' (plane)');
228+
}
229+
230+
231+
// document.getElementById('transferSyntax').textContent = getTransferSyntax();
232+
// document.getElementById('sopClass').textContent = getSopClass();
233+
// document.getElementById('samplesPerPixel').textContent = image.data.uint16('x00280002');
234+
// document.getElementById('photometricInterpretation').textContent = image.data.string('x00280004');
235+
// document.getElementById('numberOfFrames').textContent = image.data.string('x00280008');
236+
// document.getElementById('planarConfiguration').textContent = getPlanarConfiguration();
237+
// document.getElementById('rows').textContent = image.data.uint16('x00280010');
238+
// document.getElementById('columns').textContent = image.data.uint16('x00280011');
239+
// document.getElementById('pixelSpacing').textContent = image.data.string('x00280030');
240+
// document.getElementById('rowPixelSpacing').textContent = image.rowPixelSpacing;
241+
// document.getElementById('columnPixelSpacing').textContent = image.columnPixelSpacing;
242+
// document.getElementById('bitsAllocated').textContent = image.data.uint16('x00280100');
243+
// document.getElementById('bitsStored').textContent = image.data.uint16('x00280101');
244+
// document.getElementById('highBit').textContent = image.data.uint16('x00280102');
245+
// document.getElementById('pixelRepresentation').textContent = getPixelRepresentation();
246+
// document.getElementById('windowCenter').textContent = image.data.string('x00281050');
247+
// document.getElementById('windowWidth').textContent = image.data.string('x00281051');
248+
// document.getElementById('rescaleIntercept').textContent = image.data.string('x00281052');
249+
// document.getElementById('rescaleSlope').textContent = image.data.string('x00281053');
250+
// document.getElementById('basicOffsetTable').textContent = image.data.elements.x7fe00010.basicOffsetTable ? image.data.elements.x7fe00010.basicOffsetTable.length : '';
251+
// document.getElementById('fragments').textContent = image.data.elements.x7fe00010.fragments ? image.data.elements.x7fe00010.fragments.length : '';
252+
// document.getElementById('minStoredPixelValue').textContent = image.minPixelValue;
253+
// document.getElementById('maxStoredPixelValue').textContent = image.maxPixelValue;
254+
// var end = new Date().getTime();
255+
// var time = end - start;
256+
// document.getElementById('totalTime').textContent = time + "ms";
257+
// document.getElementById('loadTime').textContent = image.loadTimeInMS + "ms";
258+
// document.getElementById('decodeTime').textContent = image.decodeTimeInMS + "ms";
259+
260+
}, function (err) {
261+
throw err;
262+
});
263+
}
264+
catch (err) {
265+
throw err;
266+
}
267+
268+
})
269+
270+
cornerstone.events.addEventListener('cornerstoneimageloadprogress', function (event) {
271+
const eventData = event.detail;
272+
const loadProgress = document.getElementById('loadProgress');
273+
loadProgress.textContent = `Image Load Progress: ${eventData.percentComplete}%`;
274+
});
275+
276+
function getUrlWithoutFrame() {
277+
var url = document.getElementById('wadoURL').value;
278+
var frameIndex = url.indexOf('frame=');
279+
if (frameIndex !== -1) {
280+
url = url.substr(0, frameIndex - 1);
281+
}
282+
return url;
283+
}
284+
</script>
285+
286+
</html>

0 commit comments

Comments
 (0)