@radicalimaging/static-wado-creator 1.2.8 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -260
- package/lib/StaticWado.js +19 -2
- package/lib/createPart10.js +113 -0
- package/lib/mkdicomwebConfig.js +34 -3
- package/lib/model/uids.js +5 -1
- package/lib/operation/StudyData.js +6 -1
- package/lib/operation/ThumbnailService.js +15 -17
- package/lib/operation/adapter/transcodeImage.js +132 -15
- package/lib/operation/getValue.js +7 -0
- package/lib/writer/HashDataWriter.js +1 -3
- package/lib/writer/ImageFrameWriter.js +1 -1
- package/lib/writer/VideoWriter.js +2 -2
- package/license.md +21 -0
- package/package.json +7 -6
- package/CHANGELOG.md +0 -67
package/README.md
CHANGED
|
@@ -1,287 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Static DICOMweb Creator
|
|
2
|
+
The Static DICOMweb Creator package converts DICOM Part 10 files to and from files in a DICOMweb like format.
|
|
2
3
|
|
|
3
|
-
#
|
|
4
|
+
# Installation
|
|
5
|
+
Using npm, install the package:
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
The scope of this project is to convert DICOM P10 into a format into a DICOMweb compatible format - specifically JSON metadata, frames and bulkdata. This library will enable the following:
|
|
8
|
-
* Building of DICOMweb compliant services
|
|
9
|
-
* Ability to stream the data for "on the fly" use cases
|
|
10
|
-
* Ability to pregenerate DICOMweb responses and store them so they can be served up with a standard HTTP server to implement a subset of DICOMweb WADO-RS, specifically:
|
|
11
|
-
* Retrieve instance metadata
|
|
12
|
-
* Retrieve series level metadata
|
|
13
|
-
* Retrieve frames in an instance
|
|
14
|
-
* Retrieve bulk data
|
|
15
|
-
* Retrieve study, series or instance query
|
|
16
|
-
* To explore an alternative archive format that is web friendly (no need for a DICOM parser)
|
|
17
|
-
* Should be able to recreate DICOM P10 file from this data (semantic equivalence or possibly bit for bit equivalent)
|
|
18
|
-
* Will make updates more efficient (e.g. updating patient name just requires updating metadata)
|
|
19
|
-
* More efficient image access (no need to scan through DICOM P10 to access frames, or store the offsets of each frame separately and seek to them)
|
|
20
|
-
|
|
21
|
-
The scope of this library is limited to:
|
|
22
|
-
* Taking one or more DICOM files as input, writing Bulkdata and Pixeldata
|
|
23
|
-
* Creating either deduplicated representations or instance level metadata
|
|
24
|
-
* Combining deduplicated representations into sets of deduplicated representations
|
|
25
|
-
* Using the deduplicated representations to generate study/series/instance query results and series metadata
|
|
26
|
-
|
|
27
|
-
View the [design rationale](docs/design.md) for more information on the scope
|
|
28
|
-
|
|
29
|
-
View [frequently asked questions](docs/faq.md)
|
|
30
|
-
|
|
31
|
-
View [specification](docs/spec.md)
|
|
32
|
-
|
|
33
|
-
## Pre-requisites
|
|
34
|
-
View root pre-requisites section [pre-requisites](../../README.md#pre-requisites)
|
|
35
|
-
|
|
36
|
-
## Development
|
|
37
|
-
View root development section [development](../../README.md#development)
|
|
38
|
-
|
|
39
|
-
## Running the CLI
|
|
40
|
-
Install the CLI with either npm install -g . or npm install -g @radicalimaging/static-wado-js
|
|
41
|
-
|
|
42
|
-
## Usage
|
|
43
|
-
Once you install the package the executables (i.e static-wado commands) are installed into to your PATH system, then you can use anywhere on your command line program.
|
|
44
|
-
|
|
45
|
-
### Api
|
|
46
|
-
There are 5 different commands: mkdicomweb, mkdicomwebinstances, mkdicomwebstudy, mkdicomwebdeduplicated, mkdicomwebdeduplicatedgroup that can be used (see usage bellow). It can be run as:
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
commnadName [options] [<input...>]
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Arguments are:
|
|
53
|
-
|
|
54
|
-
input
|
|
55
|
-
: List of directories/files separated by space.
|
|
56
|
-
Required for commands: **mkdicomweb**, **mkdicomwebdeduplicated**, **mkdicomwebinstances**. Optional for the others. This argument should be placed at end of command invoking.
|
|
57
|
-
|
|
58
|
-
The options are:
|
|
59
|
-
|
|
60
|
-
-c, --clean
|
|
61
|
-
: Clean the study output directory for these instances (default: false)
|
|
62
|
-
|
|
63
|
-
-C, --remove-deduplicated-instances
|
|
64
|
-
: Remove single instance deduplicated files after writing group files (default: false)
|
|
65
|
-
|
|
66
|
-
-d, --deduplicate
|
|
67
|
-
: Write deduplicate instance level data (default: false)
|
|
68
|
-
|
|
69
|
-
-g, --group
|
|
70
|
-
: Write combined deduplicate data (default: true)
|
|
71
|
-
|
|
72
|
-
-i, --instances
|
|
73
|
-
: Write instance metadata (default: false)
|
|
74
|
-
|
|
75
|
-
-m, --maximum-inline-public-length <value>
|
|
76
|
-
: Maximum length of public binary data (default: 131074)
|
|
77
|
-
|
|
78
|
-
-M, --maximum-inline-private-length <value>
|
|
79
|
-
: Maximum length of private binary data (default: 64)
|
|
80
|
-
|
|
81
|
-
--no-recompress
|
|
82
|
-
: Force no recompression
|
|
83
|
-
[see](#to-recompress)
|
|
84
|
-
|
|
85
|
-
--no-recompress-thumb
|
|
86
|
-
: Force no recompression for thumbnail
|
|
87
|
-
[see](#to-recompress-thumb)
|
|
88
|
-
|
|
89
|
-
-o, --dir <value>
|
|
90
|
-
: Set output directory (default: "~/dicomweb")
|
|
91
|
-
|
|
92
|
-
--path-deduplicated <path>
|
|
93
|
-
: Set the deduplicate data directory path (relative to dir) (default: "deduplicated")
|
|
94
|
-
|
|
95
|
-
--path-instances <path>
|
|
96
|
-
: Set the instances directory path (relative to dir) (default: "instances")
|
|
97
|
-
|
|
98
|
-
-r, --recompress <listvalue...>
|
|
99
|
-
: List of types to recompress separated by space (choices: "uncompressed", "jp2", "jpeglossless", "rle", default: "uncompressed jp2")
|
|
100
|
-
[see](#to-recompress)
|
|
101
|
-
|
|
102
|
-
--recompress-thumb <listvalue...>
|
|
103
|
-
: List of types to recompress thumb separated by space (choices: "uncompressed", "jp2", "jpeglossless", "rle", default: "uncompressed jp2")
|
|
104
|
-
[see](#to-recompress-thumb)
|
|
105
|
-
|
|
106
|
-
-s, --study
|
|
107
|
-
: Write study metadata - on provided instances only (TO FIX), (default: true)
|
|
108
|
-
|
|
109
|
-
-t, --content-type <type>
|
|
110
|
-
: Destination type to compress to (choices: "jls", "lei", "jls-lossy" or DICOM Transfer Syntax UID - default: "jls")
|
|
111
|
-
|
|
112
|
-
-T, --colour-content-type <type>
|
|
113
|
-
: Colour content type (default: null)
|
|
114
|
-
|
|
115
|
-
-v, --verbose
|
|
116
|
-
: Write verbose output (default: false)
|
|
117
|
-
|
|
118
|
-
-V, --version
|
|
119
|
-
: output the version number
|
|
120
|
-
|
|
121
|
-
### Help command
|
|
122
|
-
You can output help information by:
|
|
123
|
-
```
|
|
124
|
-
commandName help
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
```
|
|
128
|
-
commandName -h
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
commandName --help
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### To create instances
|
|
136
|
-
Run the tool:
|
|
137
|
-
```
|
|
138
|
-
mkdicomwebinstances <directoryOfP10Files>
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### To create a full DICOMweb output structure
|
|
142
|
-
Run the tool:
|
|
143
|
-
```
|
|
144
|
-
mkdicomweb <directoryOfP10Files>
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### To run separated stages
|
|
148
|
-
The mkdicomweb tool runs the three stages all together, on just the studies references. This can instead be done on separate files by running:
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @radicalimaging/static-wado-creator
|
|
149
9
|
```
|
|
150
|
-
mkdicomwebdeduplicated <directoryOfP10Files>
|
|
151
|
-
mkdicomwebdeduplicatedgroup
|
|
152
|
-
mkdicomwebstudy
|
|
153
|
-
```
|
|
154
|
-
which creates a full study directory. The first stage writes to ~/dicomweb/instances/<studyUID>/ data about each instance as it is read in. The second stage then groups these files for more efficient compression into the ~/dicomweb/deduplicated/<studyUID>/ The last stage then creates the actual DICOMweb files.
|
|
155
10
|
|
|
156
|
-
There is currently no notification of what studies have been updated between stages. The intent is to write notifications to ~/dicomweb/notifications/<studyUID> of what operations need to be applied/updated.
|
|
157
11
|
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
cd ~/dicomweb
|
|
161
|
-
npx http-server -p 5000 -g --cors -e json
|
|
162
|
-
```
|
|
12
|
+
## Additional Installs:
|
|
163
13
|
|
|
164
|
-
|
|
14
|
+
* To render thumbnail/rendered responses, install the dcm2jpg command from [dcm4che](www.dcm4che.org)
|
|
15
|
+
* To generate thumbnails for video, instal ffmpeg.
|
|
165
16
|
|
|
166
|
-
The -e option will mark json files as the default extension, so when requesting `GET dicomweb/studies/` the `./studies/index.json.gz` file will be served.
|
|
167
17
|
|
|
168
|
-
|
|
169
|
-
|
|
18
|
+
# Tools Usage
|
|
19
|
+
The tools will write to a dicomweb folder created by default in the users home directory (~) in the `~/dicomweb/` location in the locations as specified in [File Structure](../../file-structure.md).
|
|
170
20
|
|
|
171
|
-
|
|
21
|
+
## Converting DICOM Part 10 to DICOMweb
|
|
172
22
|
|
|
173
|
-
|
|
174
|
-
TODO
|
|
23
|
+
DICOM part 10 files can be converted to DICOMweb. By default, LEI grayscale images will be converted to JPEG-LS compressed images and a thumbnail will be generated. This can be done with:
|
|
175
24
|
|
|
176
|
-
|
|
25
|
+
```bash
|
|
26
|
+
mkdicomweb create <DICOM-FILES-DIR>
|
|
177
27
|
```
|
|
178
|
-
mkdicomwebupdate -<delete/anonymize/patient/study/series/instance> <studyInstanceUID> (tag=newValue)*
|
|
179
|
-
```
|
|
180
|
-
to delete the given item or to update the specified attribute contained in the given level. Multiple mkdicomwebupdate commands may be run to perform updates on different attribute sets, or they may be grouped into a single file for bulk application.
|
|
181
28
|
|
|
182
|
-
|
|
183
|
-
It allows commands to recompress data/metadata prior writing it to local. Currently, if recompress is active the designed data types will be transcoded [see](#recompress-aliases-and-input-transfer-syntaxes).
|
|
29
|
+
Note there is no glob support on the input parameter, so either directories or files need to be fully specified.
|
|
184
30
|
|
|
185
|
-
|
|
186
|
-
```
|
|
187
|
-
mkdicomweb ./folderName
|
|
188
|
-
```
|
|
189
|
-
It will recompress any existing data whose transfer syntaxes are 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1 and 1.2.840.10008.1.2.2.
|
|
190
|
-
|
|
191
|
-
Define incoming types for recompression of: uncompressed,jp2,rle
|
|
192
|
-
```
|
|
193
|
-
mkdicomweb -r uncompressed jp2 rle ./folderName
|
|
194
|
-
```
|
|
195
|
-
It will recompress any existing data whose transfer syntaxes: 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1, 1.2.840.10008.1.2.2 and 1.2.840.10008.1.2.5 to transfer syntax 1.2.840.10008.1.2.4.80.
|
|
31
|
+
## Converting Static DICOMweb to Part 10
|
|
196
32
|
|
|
197
|
-
|
|
198
|
-
```
|
|
199
|
-
mkdicomweb ./folderName
|
|
200
|
-
```
|
|
201
|
-
It will recompress any existing data whose transfer syntaxes are 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1 and 1.2.840.10008.1.2.2 to jls (transfer syntax 1.2.840.10008.1.2.4.80)
|
|
33
|
+
Static DICOMweb files can be converted back into Part 10, provided:
|
|
202
34
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
mkdicomweb -t lei ./folderName
|
|
206
|
-
```
|
|
207
|
-
It will decode any existing data whose transfer syntaxes are 1.2.840.10008.1.2.4.90, 1.2.840.10008.1.2.4.91, 1.2.840.10008.1.2, 1.2.840.10008.1.2.1 and 1.2.840.10008.1.2.2 and store uncompressed as LEI (transfer syntax 1.2.840.10008.1.2.1)
|
|
35
|
+
* Total DICOM Part 10 size is under 2 gb
|
|
36
|
+
* DICOMweb data is stored in a compressed transfer syntax
|
|
208
37
|
|
|
209
|
-
|
|
210
|
-
```
|
|
211
|
-
mkdicomweb -r uncompressed jp2 rle --no-recompress ./folderName
|
|
212
|
-
```
|
|
213
|
-
It will NOT recompress any existing data but instead it will keep them in the original encoding.
|
|
38
|
+
The commands is:
|
|
214
39
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
mkdicomweb --no-recompress ./folderName
|
|
40
|
+
```bash
|
|
41
|
+
mkdicomweb part10 <studyUID>
|
|
218
42
|
```
|
|
219
|
-
It will NOT recompress any existing data but instead it will keep them in the original encoding.
|
|
220
|
-
|
|
221
|
-
Obs: --no-recompress forces no compression at all. Use it if you want to disable even the default behavior.
|
|
222
|
-
|
|
223
43
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
| TransferSyntaxUID | Name | Recompress alias | Target TransferSyntaxUID |
|
|
228
|
-
|------------------------ |--------------------------------------------------------- |------------------ |-------------------------- |
|
|
229
|
-
| 1.2.840.10008.1.2 | Implicit VR Endian | uncompressed | 1.2.840.10008.1.2 |
|
|
230
|
-
| 1.2.840.10008.1.2.1 | Explicit VR Little Endian | uncompressed | 1.2.840.10008.1.2.1 |
|
|
231
|
-
| 1.2.840.10008.1.2.2 | Explicit VR Big Endian | uncompressed | 1.2.840.10008.1.2.1 |
|
|
232
|
-
| 1.2.840.10008.1.2.4.57 | JPEG Lossless, Nonhierarchical (Processes 14) | jpeglossless | 1.2.840.10008.1.2.4.80 |
|
|
233
|
-
| 1.2.840.10008.1.2.4.70 | JPEG Lossless, Nonhierarchical, First- Order Prediction | jpeglossless | 1.2.840.10008.1.2.4.80 |
|
|
234
|
-
| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | jp2 | 1.2.840.10008.1.2.4.80 |
|
|
235
|
-
| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | jp2 | 1.2.840.10008.1.2.4.80 |
|
|
236
|
-
| 1.2.840.10008.1.2.5 | RLE Lossless | rle | 1.2.840.10008.1.2.4.80 |
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
### To recompress thumb
|
|
240
|
-
It tells thumbnail creation to use recompressed data or not.
|
|
241
|
-
The list of types MUST be necessarily a subset of recompress list.
|
|
242
|
-
|
|
243
|
-
Obs: --no-recompress-thumb forces to not use compression at all. Use it if you want to disable even the default behavior.
|
|
244
|
-
|
|
245
|
-
By default the recompression occurs for incoming types: uncompressed, jp2. This will recompress the types mentioned and will generate the thumbnails with the result of data recompression.
|
|
44
|
+
## Delete A Study
|
|
45
|
+
A study can be deleted using:
|
|
246
46
|
|
|
247
47
|
```
|
|
248
|
-
mkdicomweb
|
|
48
|
+
mkdicomweb delete <studyUID>
|
|
249
49
|
```
|
|
250
50
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
mkdicomweb -r uncompressed jp2 rle --recompress-thumb jp2 ./folderName
|
|
254
|
-
```
|
|
51
|
+
## Reject A Series
|
|
52
|
+
A series can be "deleted", that is rejected, which removed it only from the indices by running:
|
|
255
53
|
|
|
256
|
-
Skipping not recompressed data. This will recompress the types: uncompressed jp2, thumbnails creation will use original data for all types (since recompress-thumb list does not intersect with recompress list).
|
|
257
54
|
```
|
|
258
|
-
mkdicomweb
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Force no recompression thumb. This will recompress the types: uncompressed jp2 rle, but for thumbnails creation will use original data.
|
|
55
|
+
mkdicomweb reject <studyUID>/seres/<seriesUID>
|
|
262
56
|
```
|
|
263
|
-
mkdicomweb -r uncompressed jp2 rle --no-recompress-thumb ./folderName
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
## Development
|
|
270
|
-
|
|
271
|
-
## TODO (Looking for help here!!)
|
|
272
|
-
|
|
273
|
-
* Create docker container to encapsulate build environment
|
|
274
|
-
* Create test data (DICOM P10 and expected DICOMweb results) - Jordan working on this
|
|
275
|
-
* Document API
|
|
276
|
-
* Create CI + Publish to NPM
|
|
277
|
-
* Fix Bugs
|
|
278
|
-
* Get bulkdata refs written properly
|
|
279
|
-
* Enhance cli
|
|
280
|
-
* Add support for specifying bulkDataMinSize
|
|
281
|
-
* Add support for writing out DICOM P10 file
|
|
282
|
-
* Enhance library
|
|
283
|
-
* Write out "info" file
|
|
284
|
-
* P10 Header
|
|
285
|
-
* Data needed to recreate original P10 instance
|
|
286
|
-
* Create DICOMweb -> DICOM P10 tool
|
|
287
|
-
|
package/lib/StaticWado.js
CHANGED
|
@@ -9,12 +9,13 @@ const getDataSet = require("./operation/getDataSet");
|
|
|
9
9
|
const InstanceDeduplicate = require("./operation/InstanceDeduplicate");
|
|
10
10
|
const DeduplicateWriter = require("./writer/DeduplicateWriter");
|
|
11
11
|
const ImageFrameWriter = require("./writer/ImageFrameWriter");
|
|
12
|
+
const WriteStream = require("./writer/WriteStream");
|
|
12
13
|
const CompleteStudyWriter = require("./writer/CompleteStudyWriter");
|
|
13
14
|
const IdCreator = require("./util/IdCreator");
|
|
14
15
|
const ScanStudy = require("./operation/ScanStudy");
|
|
15
16
|
const HashDataWriter = require("./writer/HashDataWriter");
|
|
16
17
|
const VideoWriter = require("./writer/VideoWriter");
|
|
17
|
-
const { transcodeImageFrame, transcodeId, transcodeMetadata } = require("./operation/adapter/transcodeImage");
|
|
18
|
+
const { transcodeImageFrame, generateLossyImage, transcodeId, transcodeMetadata } = require("./operation/adapter/transcodeImage");
|
|
18
19
|
const ThumbnailWriter = require("./writer/ThumbnailWriter");
|
|
19
20
|
const decodeImage = require("./operation/adapter/decodeImage");
|
|
20
21
|
const ThumbnailService = require("./operation/ThumbnailService");
|
|
@@ -168,14 +169,30 @@ class StaticWado {
|
|
|
168
169
|
bulkdata: async (bulkData, options) => {
|
|
169
170
|
const _bulkDataIndex = bulkDataIndex;
|
|
170
171
|
bulkDataIndex += 1;
|
|
172
|
+
// TODO - handle other types here too as single part rendered
|
|
173
|
+
if (options?.mimeType === "application/pdf") {
|
|
174
|
+
console.log("Writing rendered mimeType", options.mimeType);
|
|
175
|
+
const writeStream = WriteStream(id.sopInstanceRootPath, "rendered.pdf", {
|
|
176
|
+
gzip: false,
|
|
177
|
+
mkdir: true,
|
|
178
|
+
});
|
|
179
|
+
await writeStream.write(bulkData);
|
|
180
|
+
await writeStream.close();
|
|
181
|
+
}
|
|
171
182
|
return this.callback.bulkdata(targetId, _bulkDataIndex, bulkData, options);
|
|
172
183
|
},
|
|
173
184
|
imageFrame: async (originalImageFrame) => {
|
|
174
|
-
const { imageFrame: transcodedImageFrame, id: transcodedId } = await transcodeImageFrame(id, targetId, originalImageFrame, dataSet, this.options);
|
|
185
|
+
const { imageFrame: transcodedImageFrame, decoded, id: transcodedId } = await transcodeImageFrame(id, targetId, originalImageFrame, dataSet, this.options);
|
|
186
|
+
|
|
187
|
+
const lossyImage = await generateLossyImage(id, decoded, this.options);
|
|
175
188
|
|
|
176
189
|
const currentImageFrameIndex = imageFrameIndex;
|
|
177
190
|
imageFrameIndex += 1;
|
|
178
191
|
|
|
192
|
+
if (lossyImage) {
|
|
193
|
+
await this.callback.imageFrame(lossyImage.id, currentImageFrameIndex, lossyImage.imageFrame);
|
|
194
|
+
}
|
|
195
|
+
|
|
179
196
|
thumbnailService.queueThumbnail(
|
|
180
197
|
{
|
|
181
198
|
imageFrame: originalImageFrame,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
const { Tags, readBulkData } = require("@radicalimaging/static-wado-util");
|
|
3
|
+
const dcmjs = require("dcmjs");
|
|
4
|
+
|
|
5
|
+
const StaticWado = require("./StaticWado");
|
|
6
|
+
const adaptProgramOpts = require("./util/adaptProgramOpts");
|
|
7
|
+
const WriteStream = require("./writer/WriteStream");
|
|
8
|
+
|
|
9
|
+
const UncompressedLEIExplicit = "1.2.840.10008.1.2.1";
|
|
10
|
+
const { DicomDict, DicomMetaDictionary } = dcmjs.data;
|
|
11
|
+
|
|
12
|
+
const fileMetaInformationVersionArray = new Uint8Array(2);
|
|
13
|
+
fileMetaInformationVersionArray[1] = 1;
|
|
14
|
+
|
|
15
|
+
const createFmi = (instance) => {
|
|
16
|
+
// Assume the TSUID is in the value 0
|
|
17
|
+
const TransferSyntaxUID = Tags.getValue(instance, Tags.AvailableTransferSyntaxUID) || UncompressedLEIExplicit;
|
|
18
|
+
const MediaStorageSOPClassUID = Tags.getValue(instance, Tags.MediaStorageSOPClassUID);
|
|
19
|
+
const SOPInstanceUID = Tags.getValue(instance, Tags.SOPInstanceUID);
|
|
20
|
+
const naturalFmi = {
|
|
21
|
+
MediaStorageSOPClassUID,
|
|
22
|
+
MediaStorageSOPInstanceUID: SOPInstanceUID,
|
|
23
|
+
ImplementationVersionName: "mkdicomweb",
|
|
24
|
+
TransferSyntaxUID,
|
|
25
|
+
ImplementationClassUID: "2.25.984723498557234098.001",
|
|
26
|
+
FileMetaInformationVersion: fileMetaInformationVersionArray.buffer,
|
|
27
|
+
};
|
|
28
|
+
const denaturalized = DicomMetaDictionary.denaturalizeDataset(naturalFmi);
|
|
29
|
+
return denaturalized;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const readBulkDataValue = async (studyDir, instance, value) => {
|
|
33
|
+
const { BulkDataURI } = value;
|
|
34
|
+
value.vr = "OB";
|
|
35
|
+
const seriesUid = Tags.getValue(instance, Tags.SeriesInstanceUID);
|
|
36
|
+
const numberOfFrames = Tags.getValue(instance, Tags.NumberOfFrames) || 1;
|
|
37
|
+
const dir = `${studyDir}/series/${seriesUid}`;
|
|
38
|
+
if (BulkDataURI.indexOf("/frames") !== -1) {
|
|
39
|
+
// Really only support a limited number of frames based on memory size
|
|
40
|
+
value.Value = [];
|
|
41
|
+
for (let frame = 1; frame <= numberOfFrames; frame++) {
|
|
42
|
+
const bulk = await readBulkData(dir, BulkDataURI, frame);
|
|
43
|
+
if (!bulk) break;
|
|
44
|
+
value.Value.push(bulk);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
value.Value = [new ArrayBuffer(await readBulkData(dir, BulkDataURI))];
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const readBinaryData = async (dir, instance) => {
|
|
52
|
+
for (const tag of Object.keys(instance)) {
|
|
53
|
+
const v = instance[tag];
|
|
54
|
+
if (v.BulkDataURI) {
|
|
55
|
+
await readBulkDataValue(dir, instance, v);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!v.vr) {
|
|
59
|
+
const value0 = v.Value?.[0];
|
|
60
|
+
if (typeof value0 === "string") {
|
|
61
|
+
v.vr = "LT";
|
|
62
|
+
} else {
|
|
63
|
+
console.log("Deleting", tag, v.Value, v);
|
|
64
|
+
delete instance[tag];
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (v.vr === "SQ") {
|
|
69
|
+
await Promise.all(v.Values.map(readBinaryData));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const writeBuffer = (dir, fileName, buffer) => {
|
|
76
|
+
const writeStream = WriteStream(dir, fileName, {
|
|
77
|
+
mkdir: true,
|
|
78
|
+
gzip: false,
|
|
79
|
+
});
|
|
80
|
+
writeStream.write(buffer);
|
|
81
|
+
return writeStream.close();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
module.exports = async function createMain(options, program) {
|
|
85
|
+
const finalOptions = adaptProgramOpts(options, {
|
|
86
|
+
...this,
|
|
87
|
+
// Instance metadata is the instances/<sopUID>/metadata.gz files
|
|
88
|
+
isInstance: false,
|
|
89
|
+
// Deduplicated data is single instance deduplicated data
|
|
90
|
+
isDeduplicate: false,
|
|
91
|
+
// Group data is the group file directories
|
|
92
|
+
isGroup: false,
|
|
93
|
+
isStudyData: false,
|
|
94
|
+
isDeleteInstances: false,
|
|
95
|
+
});
|
|
96
|
+
const importer = new StaticWado(finalOptions);
|
|
97
|
+
const studyInstanceUid = program.args[0];
|
|
98
|
+
const dir = `${importer.options.rootDir}/studies/${studyInstanceUid}`;
|
|
99
|
+
console.log("Creating part 10 for", studyInstanceUid, dir);
|
|
100
|
+
const study = await importer.callback.scanStudy(studyInstanceUid);
|
|
101
|
+
const outputPath = `./studies/${studyInstanceUid}`;
|
|
102
|
+
for (const sop of study.getSopUids()) {
|
|
103
|
+
const instance = await study.recombine(sop);
|
|
104
|
+
const fmi = createFmi(instance);
|
|
105
|
+
|
|
106
|
+
console.log("Processing sop", sop);
|
|
107
|
+
const dicomDict = new DicomDict(fmi);
|
|
108
|
+
await readBinaryData(dir, instance);
|
|
109
|
+
dicomDict.dict = instance;
|
|
110
|
+
const buffer = Buffer.from(dicomDict.write());
|
|
111
|
+
writeBuffer(outputPath, sop, buffer);
|
|
112
|
+
}
|
|
113
|
+
};
|
package/lib/mkdicomwebConfig.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const ConfigPoint = require("config-point");
|
|
2
2
|
const { staticWadoConfig } = require("@radicalimaging/static-wado-util");
|
|
3
3
|
const createMain = require("./createMain");
|
|
4
|
+
const createPart10 = require("./createPart10");
|
|
4
5
|
const deleteMain = require("./deleteMain");
|
|
5
6
|
const rejectMain = require("./rejectMain");
|
|
6
7
|
const instanceMain = require("./instanceMain");
|
|
@@ -21,7 +22,12 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
21
22
|
isGroup: true,
|
|
22
23
|
options: [
|
|
23
24
|
{
|
|
24
|
-
key: "-c
|
|
25
|
+
key: "-c <configFile.json5>",
|
|
26
|
+
description: "Use an alternate configuration file",
|
|
27
|
+
defaultValue: "~/static-wado.json5",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "--clean",
|
|
25
31
|
description: "Clean the outputs before generating/starting to write new values.",
|
|
26
32
|
defaultValue: false,
|
|
27
33
|
},
|
|
@@ -39,6 +45,25 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
39
45
|
description: "Delete the imported instances",
|
|
40
46
|
defaultValue: false,
|
|
41
47
|
},
|
|
48
|
+
{
|
|
49
|
+
key: "--lossy",
|
|
50
|
+
description: "Generate lossy instances instead of lossless",
|
|
51
|
+
defaultValue: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: "--alternate <type>",
|
|
55
|
+
description: "Generates an alternate representaton of the image generally in the /lossy sub-directory",
|
|
56
|
+
choices: ["jhc", "jls", "jhcLossless", "jlsLossless"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "--alternate-thumbnail",
|
|
60
|
+
description: "Generates a thumbnail for the alternate representation",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "--alternate-name <dir>",
|
|
64
|
+
description: "Uses a given sub directory name",
|
|
65
|
+
defaultValue: "lossy",
|
|
66
|
+
},
|
|
42
67
|
{
|
|
43
68
|
key: "-v, --verbose",
|
|
44
69
|
description: "Write verbose output",
|
|
@@ -110,8 +135,8 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
110
135
|
defaultValue: false,
|
|
111
136
|
},
|
|
112
137
|
{
|
|
113
|
-
key: "--
|
|
114
|
-
description: "
|
|
138
|
+
key: "--thumb",
|
|
139
|
+
description: "Generate thumbnails",
|
|
115
140
|
},
|
|
116
141
|
{
|
|
117
142
|
key: "--dcm2jpg",
|
|
@@ -170,6 +195,12 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
170
195
|
"of deduplicated files each time a study instance UID is found, and only updates\n" +
|
|
171
196
|
"those studies having at least one ",
|
|
172
197
|
},
|
|
198
|
+
{
|
|
199
|
+
command: "part10",
|
|
200
|
+
arguments: ["studyUID"],
|
|
201
|
+
main: createPart10,
|
|
202
|
+
helpDescription: "Store the specified study as part 10 data",
|
|
203
|
+
},
|
|
173
204
|
{
|
|
174
205
|
command: "index",
|
|
175
206
|
main: indexMain,
|
package/lib/model/uids.js
CHANGED
|
@@ -33,7 +33,11 @@ const uids = {
|
|
|
33
33
|
"1.2.840.10008.1.2.4.81": { contentType: jls, lossy: true },
|
|
34
34
|
"1.2.840.10008.1.2.4.90": { contentType: jp2, extension: ".jp2" },
|
|
35
35
|
"1.2.840.10008.1.2.4.91": { contentType: jp2 },
|
|
36
|
-
|
|
36
|
+
// Private/test transfer syntax for HTJ2K
|
|
37
|
+
"3.2.840.10008.1.2.4.96": { contentType: htj2k },
|
|
38
|
+
"1.2.840.10008.1.2.4.201": { contentType: htj2k },
|
|
39
|
+
"1.2.840.10008.1.2.4.202": { contentType: htj2k, extension: ".jhc" },
|
|
40
|
+
"1.2.840.10008.1.2.4.203": { contentType: htj2k, lossy: true },
|
|
37
41
|
"1.2.840.10008.1.2.5": { contentType: rle },
|
|
38
42
|
default: {},
|
|
39
43
|
};
|
|
@@ -158,7 +158,8 @@ class StudyData {
|
|
|
158
158
|
/**
|
|
159
159
|
* Create a full study instance data for instance at index
|
|
160
160
|
*/
|
|
161
|
-
async recombine(
|
|
161
|
+
async recombine(indexOrSop) {
|
|
162
|
+
const index = typeof indexOrSop === "string" ? this.sopInstances[indexOrSop] : indexOrSop;
|
|
162
163
|
const deduplicated = this.deduplicated[index];
|
|
163
164
|
if (index < 0 || index >= this.deduplicated.length) {
|
|
164
165
|
throw new Error(`Can't read index ${index}, out of bounds [0..${this.deduplicated.length})`);
|
|
@@ -461,6 +462,10 @@ class StudyData {
|
|
|
461
462
|
await JSONWriter(this.deduplicatedPath, hashValue, deduplicatedList, { gzip: true, index: false });
|
|
462
463
|
console.log("Wrote naturalized dataset");
|
|
463
464
|
}
|
|
465
|
+
|
|
466
|
+
getSopUids() {
|
|
467
|
+
return Object.keys(this.sopInstances);
|
|
468
|
+
}
|
|
464
469
|
}
|
|
465
470
|
|
|
466
471
|
module.exports = StudyData;
|
|
@@ -120,9 +120,10 @@ class ThumbnailService {
|
|
|
120
120
|
if (pixelData) {
|
|
121
121
|
const { BulkDataURI } = pixelData;
|
|
122
122
|
if (BulkDataURI?.indexOf("mp4")) {
|
|
123
|
+
fs.mkdirSync(`${itemId.sopInstanceRootPath}/rendered`, { recursive: true });
|
|
123
124
|
const mp4Path = path.join(itemId.sopInstanceRootPath, "rendered/index.mp4");
|
|
124
125
|
// Generate as rendered, as more back ends support that.
|
|
125
|
-
const thumbPath = path.join(itemId.sopInstanceRootPath, "rendered");
|
|
126
|
+
const thumbPath = path.join(itemId.sopInstanceRootPath, "rendered/1.jpg");
|
|
126
127
|
console.log("MP4 - converting video format", mp4Path);
|
|
127
128
|
this.ffmpeg(mp4Path, thumbPath);
|
|
128
129
|
return thumbPath;
|
|
@@ -132,26 +133,23 @@ class ThumbnailService {
|
|
|
132
133
|
}
|
|
133
134
|
return null;
|
|
134
135
|
}
|
|
135
|
-
if(
|
|
136
|
-
return this.dcm2jpg(id.filename, id.imageFrameRootPath.replace(/frames/,"thumbnail"), {});
|
|
136
|
+
if (options.dcm2jpg) {
|
|
137
|
+
return this.dcm2jpg(id.filename, id.imageFrameRootPath.replace(/frames/, "thumbnail"), {});
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
console.log("Using CS2 to generate thumbnails", id, options);
|
|
140
|
-
|
|
141
140
|
await callback.internalGenerateImage(imageFrame, dataSet, metadata, id.transferSyntaxUid, async (thumbBuffer) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
|
|
147
|
-
this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
|
|
148
|
-
Stats.StudyStats.add("Thumbnail Write", `Write thumbnail ${this.thumbFileName}`, 100);
|
|
149
|
-
}
|
|
150
|
-
return this.thumbFileName;
|
|
151
|
-
} catch(e) {
|
|
152
|
-
console.log("Couldn't generate thumbnail", this.thumbFileName, e);
|
|
153
|
-
}
|
|
141
|
+
try {
|
|
142
|
+
if (thumbBuffer) {
|
|
143
|
+
await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);
|
|
154
144
|
|
|
145
|
+
this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath);
|
|
146
|
+
this.copySyncThumbnail(id.seriesRootPath, id.studyPath);
|
|
147
|
+
Stats.StudyStats.add("Thumbnail Write", `Write thumbnail ${this.thumbFileName}`, 100);
|
|
148
|
+
}
|
|
149
|
+
return this.thumbFileName;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.log("Couldn't generate thumbnail", this.thumbFileName, e);
|
|
152
|
+
}
|
|
155
153
|
});
|
|
156
154
|
}
|
|
157
155
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
2
|
-
const { Tags } = require("@radicalimaging/static-wado-util");
|
|
2
|
+
const { Tags, replicate } = require("@radicalimaging/static-wado-util");
|
|
3
3
|
const getImageInfo = require("./getImageInfo");
|
|
4
4
|
|
|
5
5
|
dicomCodec.setConfig("verbose: false");
|
|
@@ -36,7 +36,7 @@ const transcodeDestinationMap = {
|
|
|
36
36
|
transcodeOp: transcodeOp.encode,
|
|
37
37
|
},
|
|
38
38
|
jhc: {
|
|
39
|
-
transferSyntaxUid: "
|
|
39
|
+
transferSyntaxUid: "1.2.840.10008.1.2.4.202",
|
|
40
40
|
transcodeOp: transcodeOp.encode,
|
|
41
41
|
},
|
|
42
42
|
jpeg: {
|
|
@@ -67,6 +67,10 @@ const transcodeSourceMap = {
|
|
|
67
67
|
alias: "jls",
|
|
68
68
|
},
|
|
69
69
|
"1.2.840.10008.1.2.4.70": {
|
|
70
|
+
transcodeOp: transcodeOp.decode,
|
|
71
|
+
alias: "jpll",
|
|
72
|
+
},
|
|
73
|
+
"1.2.840.10008.1.2.4.80": {
|
|
70
74
|
transcodeOp: transcodeOp.decode,
|
|
71
75
|
alias: "jls",
|
|
72
76
|
},
|
|
@@ -82,6 +86,10 @@ const transcodeSourceMap = {
|
|
|
82
86
|
transcodeOp: transcodeOp.decode,
|
|
83
87
|
alias: "jhc",
|
|
84
88
|
},
|
|
89
|
+
"1.2.840.10008.1.2.4.202": {
|
|
90
|
+
transcodeOp: transcodeOp.decode,
|
|
91
|
+
alias: "jhc",
|
|
92
|
+
},
|
|
85
93
|
"1.2.840.10008.1.2.5": {
|
|
86
94
|
transcodeOp: transcodeOp.decode,
|
|
87
95
|
alias: "rle",
|
|
@@ -136,17 +144,20 @@ function shouldTranscodeImageFrame(id, options, samplesPerPixel) {
|
|
|
136
144
|
return false;
|
|
137
145
|
}
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
const { transferSyntaxUid } = id;
|
|
148
|
+
const transcoder = getTranscoder(transferSyntaxUid, options, samplesPerPixel);
|
|
149
|
+
const validTranscoder = transcoder && transcoder.transferSyntaxUid;
|
|
150
|
+
if (!validTranscoder) {
|
|
151
|
+
console.verbose("Not transcoding because no decoder found for", transferSyntaxUid, samplesPerPixel);
|
|
152
|
+
return false;
|
|
143
153
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
const validRecompress = recompress.includes("true") || recompress.includes(transcoder.alias);
|
|
155
|
+
if (!validRecompress) {
|
|
156
|
+
console.verbose("Not transcoding because recompress", recompress, "does not include", transcoder.alias);
|
|
157
|
+
return false;
|
|
148
158
|
}
|
|
149
|
-
|
|
159
|
+
|
|
160
|
+
return true;
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
/**
|
|
@@ -179,6 +190,102 @@ function transcodeLog(options, msg, error = "") {
|
|
|
179
190
|
}
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
const beforeEncode = (options, encoder) => {
|
|
194
|
+
const lossy = !!options.lossy;
|
|
195
|
+
const quality = lossy ? 0.002 : -1;
|
|
196
|
+
const delta = lossy ? 3 : 0;
|
|
197
|
+
// First value is to encode as reversible lossless colour
|
|
198
|
+
encoder.setQuality?.(!lossy, quality);
|
|
199
|
+
encoder.setNearLossless?.(delta);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
function scale(imageFrame, imageInfo) {
|
|
203
|
+
const { rows, columns, bitsPerPixel, pixelRepresentation, samplesPerPixel } = imageInfo;
|
|
204
|
+
let ArrayConstructor = Float32Array;
|
|
205
|
+
if (bitsPerPixel === 8) {
|
|
206
|
+
ArrayConstructor = pixelRepresentation ? Int8Array : Uint8Array;
|
|
207
|
+
} else if (bitsPerPixel > 8 && bitsPerPixel <= 16) {
|
|
208
|
+
ArrayConstructor = pixelRepresentation ? Int16Array : Uint16Array;
|
|
209
|
+
}
|
|
210
|
+
const src = {
|
|
211
|
+
pixelData: new ArrayConstructor(imageFrame.buffer),
|
|
212
|
+
rows,
|
|
213
|
+
columns,
|
|
214
|
+
samplesPerPixel,
|
|
215
|
+
};
|
|
216
|
+
const dest = {
|
|
217
|
+
rows: Math.round(rows / 4),
|
|
218
|
+
columns: Math.round(columns / 4),
|
|
219
|
+
samplesPerPixel,
|
|
220
|
+
};
|
|
221
|
+
dest.pixelData = new ArrayConstructor(dest.rows * dest.columns * samplesPerPixel);
|
|
222
|
+
replicate(src, dest);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
imageFrame: dest.pixelData,
|
|
226
|
+
imageInfo: {
|
|
227
|
+
...imageInfo,
|
|
228
|
+
rows: dest.rows,
|
|
229
|
+
columns: dest.columns,
|
|
230
|
+
width: dest.columns,
|
|
231
|
+
height: dest.rows,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Generates a lossy version of the image.
|
|
237
|
+
* Options for lossy are:
|
|
238
|
+
* * htj2k - generates a full resolution but high loss htj2k representation of the frames
|
|
239
|
+
* * jls - generates a +/- 2 value, maximum 128 on an edge JLS representation
|
|
240
|
+
*
|
|
241
|
+
* In either case, saves it to <sopUID>/fsiz/frameNo as multipart related
|
|
242
|
+
*/
|
|
243
|
+
async function generateLossyImage(id, decoded, options) {
|
|
244
|
+
if (!options.alternate) return;
|
|
245
|
+
if (!decoded?.imageFrame) return;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
let { imageFrame, imageInfo } = decoded;
|
|
249
|
+
const lossyId = {
|
|
250
|
+
...id,
|
|
251
|
+
imageFrameRootPath: id.imageFrameRootPath.replace("frames", options.alternateName),
|
|
252
|
+
transferSyntaxUid: transcodeDestinationMap.jhc.transferSyntaxUid,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
let lossy = true;
|
|
256
|
+
if (options.alternateThumbnail && imageInfo.rows >= 512) {
|
|
257
|
+
const scaled = scale(imageFrame, imageInfo);
|
|
258
|
+
if (!scaled) {
|
|
259
|
+
console.log("Couldn't scale");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
imageFrame = Buffer.from(scaled.imageFrame.buffer);
|
|
263
|
+
imageInfo = scaled.imageInfo;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (options.alternate === "jls") {
|
|
267
|
+
lossyId.transferSyntaxUid = transcodeDestinationMap["jls-lossy"].transferSyntaxUid;
|
|
268
|
+
} else if (options.alternate === "jlsLossless") {
|
|
269
|
+
lossyId.transferSyntaxUid = transcodeDestinationMap.jls.transferSyntaxUid;
|
|
270
|
+
lossy = false;
|
|
271
|
+
} else if (options.alternate === "jhcLossless") {
|
|
272
|
+
lossy = false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const encodeOptions = {
|
|
276
|
+
beforeEncode: beforeEncode.bind(null, {
|
|
277
|
+
lossy,
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
const lossyEncoding = await dicomCodec.encode(imageFrame, imageInfo, lossyId.transferSyntaxUid, encodeOptions);
|
|
281
|
+
console.log("Encoded alternate", lossyId.transferSyntaxUid, "of size", lossyEncoding.imageFrame.length);
|
|
282
|
+
// eslint-disable-next-line consistent-return
|
|
283
|
+
return { id: lossyId, imageFrame: lossyEncoding.imageFrame };
|
|
284
|
+
} catch (e) {
|
|
285
|
+
console.warn("Unable to create alternate:", e);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
182
289
|
/**
|
|
183
290
|
* Transcode imageFrame. It uses transcodeMap to define the target encoding based on source encoding.
|
|
184
291
|
*
|
|
@@ -206,8 +313,8 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
206
313
|
|
|
207
314
|
const transcoder = getTranscoder(id.transferSyntaxUid, options, samplesPerPixel);
|
|
208
315
|
|
|
209
|
-
//
|
|
210
|
-
if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid) {
|
|
316
|
+
// Don't transcode if not required
|
|
317
|
+
if (targetId.transferSyntaxUid !== transcoder.transferSyntaxUid && !options.forceTranscode) {
|
|
211
318
|
console.verbose("Image is already in", targetId.transferSyntaxUid, "not transcoding");
|
|
212
319
|
return {
|
|
213
320
|
id,
|
|
@@ -221,20 +328,28 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
221
328
|
const imageInfo = getImageInfo(dataSet);
|
|
222
329
|
let done = false;
|
|
223
330
|
let processResultMsg = "";
|
|
331
|
+
const encodeOptions = {
|
|
332
|
+
beforeEncode: beforeEncode.bind(null, {
|
|
333
|
+
lossy: options.lossy,
|
|
334
|
+
}),
|
|
335
|
+
};
|
|
336
|
+
let decoded;
|
|
224
337
|
|
|
225
338
|
try {
|
|
226
339
|
switch (transcoder.transcodeOp) {
|
|
227
340
|
case transcodeOp.transcode:
|
|
228
341
|
transcodeLog(options, `Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
|
|
229
342
|
|
|
230
|
-
|
|
343
|
+
decoded = await dicomCodec.decode(imageFrame, imageInfo, id.transferSyntaxUid);
|
|
344
|
+
result = await dicomCodec.encode(decoded.imageFrame, decoded.imageInfo, targetId.transferSyntaxUid, encodeOptions);
|
|
231
345
|
|
|
346
|
+
console.log("transcoded image to", targetId.transferSyntaxUid, "of size", result.imageFrame.length);
|
|
232
347
|
processResultMsg = `Transcoding finished`;
|
|
233
348
|
break;
|
|
234
349
|
case transcodeOp.encode:
|
|
235
350
|
transcodeLog(options, `Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`);
|
|
236
351
|
|
|
237
|
-
result = await dicomCodec.encode(imageFrame, imageInfo, targetId.transferSyntaxUid);
|
|
352
|
+
result = await dicomCodec.encode(imageFrame, imageInfo, targetId.transferSyntaxUid, encodeOptions);
|
|
238
353
|
|
|
239
354
|
processResultMsg = `Encoding finished`;
|
|
240
355
|
break;
|
|
@@ -270,6 +385,7 @@ async function transcodeImageFrame(id, targetIdSrc, imageFrame, dataSet, options
|
|
|
270
385
|
id: targetId,
|
|
271
386
|
imageFrame: _imageFrame,
|
|
272
387
|
done,
|
|
388
|
+
decoded,
|
|
273
389
|
};
|
|
274
390
|
}
|
|
275
391
|
|
|
@@ -329,3 +445,4 @@ exports.transcodeId = transcodeId;
|
|
|
329
445
|
exports.getDestinationTranscoder = getDestinationTranscoder;
|
|
330
446
|
exports.transcodeImageFrame = transcodeImageFrame;
|
|
331
447
|
exports.transcodeMetadata = transcodeMetadata;
|
|
448
|
+
exports.generateLossyImage = generateLossyImage;
|
|
@@ -151,10 +151,16 @@ const isPrivate = (attr) => {
|
|
|
151
151
|
return chHex % 2 == 1;
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
+
const RawEncapsulatedDocument = "x00420011";
|
|
155
|
+
|
|
154
156
|
const isValueInline = (attr, options) => {
|
|
155
157
|
if (isPrivate(attr)) {
|
|
156
158
|
return attr.length <= options.maximumInlinePrivateLength;
|
|
157
159
|
}
|
|
160
|
+
// For encapsulated documents, almost always have them be external
|
|
161
|
+
if (attr.tag === RawEncapsulatedDocument && attr.length > 4) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
158
164
|
return !attr.length || attr.length <= options.maximumInlinePublicLength;
|
|
159
165
|
};
|
|
160
166
|
|
|
@@ -198,6 +204,7 @@ const getValue = async (dataSet, attr, vr, getDataSet, callback, options, parent
|
|
|
198
204
|
}
|
|
199
205
|
const binaryValue = dataSet.byteArray.slice(attr.dataOffset, attr.dataOffset + attr.length);
|
|
200
206
|
const mimeType = attr.tag == "x00420011" && dataSet.string("x00420012");
|
|
207
|
+
|
|
201
208
|
const BulkDataURI = await callback.bulkdata(binaryValue, { mimeType });
|
|
202
209
|
return { BulkDataURI, vr };
|
|
203
210
|
};
|
|
@@ -16,19 +16,17 @@ const extensions = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const baseHasher = hashFactory.hasher();
|
|
19
|
-
const MAX_HASH = 1024*1024;
|
|
20
19
|
|
|
21
20
|
const hasher = {
|
|
22
21
|
hash: (v) => {
|
|
23
22
|
const type = v.constructor?.name;
|
|
24
23
|
if (type === "Buffer") {
|
|
25
|
-
return createHash(
|
|
24
|
+
return createHash("sha256").update(v).digest("hex");
|
|
26
25
|
}
|
|
27
26
|
return baseHasher.hash(v);
|
|
28
27
|
},
|
|
29
28
|
};
|
|
30
29
|
|
|
31
|
-
|
|
32
30
|
/** Writes out JSON files to the given file name. Automatically GZips them, and adds the extension */
|
|
33
31
|
const HashDataWriter =
|
|
34
32
|
(options) =>
|
|
@@ -18,7 +18,7 @@ const ImageFrameWriter = (options) => {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (encapsulatedImage || !extension) {
|
|
21
|
-
const writeStream = WriteStream(id.imageFrameRootPath, `${1 + index}`, {
|
|
21
|
+
const writeStream = WriteStream(id.imageFrameRootPath, `${1 + index}.mht`, {
|
|
22
22
|
gzip: type.gzip,
|
|
23
23
|
mkdir: true,
|
|
24
24
|
});
|
|
@@ -25,8 +25,8 @@ const VideoWriter = () =>
|
|
|
25
25
|
async function run(id, dataSet) {
|
|
26
26
|
console.log(`Writing video ${id.sopInstanceUid}`);
|
|
27
27
|
const extension = VIDEO_TYPES[dataSet.string(Tags.RawTransferSyntaxUID)];
|
|
28
|
-
const filename = `
|
|
29
|
-
const writeStream = WriteStream(id.sopInstanceRootPath
|
|
28
|
+
const filename = `index.${extension}`;
|
|
29
|
+
const writeStream = WriteStream(`${id.sopInstanceRootPath}/rendered`, filename, {
|
|
30
30
|
mkdir: true,
|
|
31
31
|
});
|
|
32
32
|
let length = 0;
|
package/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Radical Imaging Inc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radicalimaging/static-wado-creator",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Library to convert DICOMP10 to DICOMweb format. Original upstream: https://github.com/chafey/dicomp10-to-dicomweb-js",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"verbose": true
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@cornerstonejs/dicom-codec": ">=0.
|
|
59
|
-
"@radicalimaging/static-cs-lite": "
|
|
60
|
-
"@radicalimaging/static-wado-util": "
|
|
61
|
-
"canvas": "^2.11.
|
|
58
|
+
"@cornerstonejs/dicom-codec": ">=1.0.5",
|
|
59
|
+
"@radicalimaging/static-cs-lite": "^1.3.2",
|
|
60
|
+
"@radicalimaging/static-wado-util": "^1.3.2",
|
|
61
|
+
"canvas": "^2.11.1",
|
|
62
62
|
"config-point": ">=0.5.1",
|
|
63
63
|
"dcmjs": "^0.29.5",
|
|
64
64
|
"dicom-parser": "^1.8.13",
|
|
@@ -68,5 +68,6 @@
|
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"must": "^0.13.4"
|
|
71
|
-
}
|
|
71
|
+
},
|
|
72
|
+
"gitHead": "202e8673a2c9618f441023c8382187ae7fdf5b1a"
|
|
72
73
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# Change Log
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
-
|
|
6
|
-
## [0.6.4](https://github.com/OHIF/static-wado/compare/@radicalimaging/static-wado-creator@0.6.3...@radicalimaging/static-wado-creator@0.6.4) (2022-04-04)
|
|
7
|
-
|
|
8
|
-
**Note:** Version bump only for package @radicalimaging/static-wado-creator
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## [0.6.3](https://github.com/OHIF/static-wado/compare/@radicalimaging/static-wado-creator@0.6.2...@radicalimaging/static-wado-creator@0.6.3) (2022-03-14)
|
|
15
|
-
|
|
16
|
-
**Note:** Version bump only for package @radicalimaging/static-wado-creator
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## [0.6.2](https://github.com/OHIF/static-wado/compare/@radicalimaging/static-wado-creator@0.6.1...@radicalimaging/static-wado-creator@0.6.2) (2022-03-04)
|
|
23
|
-
|
|
24
|
-
**Note:** Version bump only for package @radicalimaging/static-wado-creator
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## [0.6.1](https://github.com/OHIF/static-wado/compare/@radicalimaging/static-wado-creator@0.6.0...@radicalimaging/static-wado-creator@0.6.1) (2022-03-04)
|
|
31
|
-
|
|
32
|
-
**Note:** Version bump only for package @radicalimaging/static-wado-creator
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# [0.6.0](https://github.com/OHIF/static-wado/compare/@radicalimaging/static-wado-creator@0.5.1...@radicalimaging/static-wado-creator@0.6.0) (2022-03-03)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
### Bug Fixes
|
|
42
|
-
|
|
43
|
-
* **static-wado:** make the various constructors use a straight configuration without using the program data ([1997d9f](https://github.com/OHIF/static-wado/commit/1997d9f0fe2e0a084d31edeb475494bcec78fd77))
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
### Features
|
|
47
|
-
|
|
48
|
-
* **static-wado:** qido series fallback to retrieve from proxyAe ([#8](https://github.com/OHIF/static-wado/issues/8)) ([47b5550](https://github.com/OHIF/static-wado/commit/47b55503732e25be08b215bdc201593f64de52e6))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## 0.5.1 (2022-02-16)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
### Bug Fixes
|
|
58
|
-
|
|
59
|
-
* **pencil:** add more files to lint, increase test config and README ([89b667c](https://github.com/OHIF/static-wado/commit/89b667c83d324ab9fa540cda0c037af8fe088f72))
|
|
60
|
-
* **pencil:** adding must package and minor test changes ([9194f3f](https://github.com/OHIF/static-wado/commit/9194f3f1bb52da57e20bb8bb9f07262bcebdffbf))
|
|
61
|
-
* **pencil:** code review.lint ([fa1a58a](https://github.com/OHIF/static-wado/commit/fa1a58a0c62015503575fbeb172b71810c6833de))
|
|
62
|
-
* **pencil:** fix format ([68fbc9b](https://github.com/OHIF/static-wado/commit/68fbc9bf5a3e9bf85e3fbcddfb3e0759e79b769d))
|
|
63
|
-
* **pencil:** fix lint and add pre-commit restr ([deec345](https://github.com/OHIF/static-wado/commit/deec34524531d5a8595a775bac414f63f60e9f23))
|
|
64
|
-
* **pencil:** lint/prettier ([d6ecbc6](https://github.com/OHIF/static-wado/commit/d6ecbc6b5961e03e8a557f4fcc78af53549132cd))
|
|
65
|
-
* **pencil:** moving from wado-js to wado repo ([5916aec](https://github.com/OHIF/static-wado/commit/5916aecd7c77dbc4882681877e2b51210976427f))
|
|
66
|
-
* **pencil:** readme link name ([40408ab](https://github.com/OHIF/static-wado/commit/40408ab6b4e97c8656e30b1dd2c30b92440b9f90))
|
|
67
|
-
* **pencil:** unit tests migration ([d9d56dd](https://github.com/OHIF/static-wado/commit/d9d56dd619fb1f41ed1d5ad85f7f804e013c6527))
|