@radicalimaging/static-wado-creator 0.6.4
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 +285 -0
- package/bin/mkdicomweb.js +9 -0
- package/lib/DeleteStudy.js +14 -0
- package/lib/RejectInstance.js +17 -0
- package/lib/StaticWado.js +206 -0
- package/lib/createMain.js +14 -0
- package/lib/deleteMain.js +12 -0
- package/lib/dictionary/Tags.js +111 -0
- package/lib/dictionary/dataDictionary.js +22268 -0
- package/lib/groupMain.js +14 -0
- package/lib/index.js +8 -0
- package/lib/instanceMain.js +13 -0
- package/lib/metadataMain.js +14 -0
- package/lib/mkdicomwebConfig.js +149 -0
- package/lib/model/TagLists.js +151 -0
- package/lib/model/uids.js +42 -0
- package/lib/operation/InstanceDeduplicate.js +100 -0
- package/lib/operation/ScanStudy.js +20 -0
- package/lib/operation/StudyData.js +416 -0
- package/lib/operation/ThumbnailService.js +184 -0
- package/lib/operation/adapter/asyncIterableToBuffer.js +112 -0
- package/lib/operation/adapter/decodeImage.js +9 -0
- package/lib/operation/adapter/getImageInfo.js +21 -0
- package/lib/operation/adapter/transcodeImage.js +303 -0
- package/lib/operation/extractImageFrames.js +41 -0
- package/lib/operation/getDataSet.js +60 -0
- package/lib/operation/getEncapsulatedImageFrame.js +23 -0
- package/lib/operation/getNumberOfFrames.js +13 -0
- package/lib/operation/getUncompressedImageFrame.js +7 -0
- package/lib/operation/getVR.js +16 -0
- package/lib/operation/getValue.js +193 -0
- package/lib/program/index.js +15 -0
- package/lib/rejectMain.js +12 -0
- package/lib/util/IdCreator.js +25 -0
- package/lib/util/adaptProgramOpts.js +58 -0
- package/lib/util/compressionOptionParser.js +13 -0
- package/lib/writer/CompleteStudyWriter.js +88 -0
- package/lib/writer/DeduplicateWriter.js +32 -0
- package/lib/writer/ExpandUriPath.js +15 -0
- package/lib/writer/HashDataWriter.js +59 -0
- package/lib/writer/ImageFrameWriter.js +31 -0
- package/lib/writer/JSONWriter.js +18 -0
- package/lib/writer/MultipartAttribute.js +8 -0
- package/lib/writer/MultipartHeader.js +14 -0
- package/lib/writer/ThumbnailWriter.js +21 -0
- package/lib/writer/VideoWriter.js +52 -0
- package/lib/writer/WriteMultipart.js +22 -0
- package/lib/writer/WriteStream.js +51 -0
- package/package.json +75 -0
package/lib/groupMain.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const StaticWado = require("./StaticWado");
|
|
2
|
+
const adaptProgramOpts = require("./util/adaptProgramOpts");
|
|
3
|
+
|
|
4
|
+
module.exports = function createMain(options, program) {
|
|
5
|
+
const finalOptions = adaptProgramOpts(options, {
|
|
6
|
+
...this,
|
|
7
|
+
isInstance: false,
|
|
8
|
+
isGroup: true,
|
|
9
|
+
isStudyData: false,
|
|
10
|
+
});
|
|
11
|
+
const importer = new StaticWado(finalOptions);
|
|
12
|
+
// Either do a scan or on specified studies
|
|
13
|
+
return importer.executeCommand(program.args);
|
|
14
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const mkdicomwebConfig = require("./mkdicomwebConfig");
|
|
2
|
+
const StaticWado = require("./StaticWado");
|
|
3
|
+
|
|
4
|
+
StaticWado.mkdicomwebConfig = mkdicomwebConfig;
|
|
5
|
+
StaticWado.createMain = require("./createMain");
|
|
6
|
+
StaticWado.deleteMain = require("./deleteMain");
|
|
7
|
+
|
|
8
|
+
module.exports = StaticWado;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const StaticWado = require("./StaticWado");
|
|
2
|
+
const adaptProgramOpts = require("./util/adaptProgramOpts");
|
|
3
|
+
|
|
4
|
+
module.exports = function createMain(options, program) {
|
|
5
|
+
const finalOptions = adaptProgramOpts(options, {
|
|
6
|
+
...this,
|
|
7
|
+
isInstance: true,
|
|
8
|
+
isGroup: false,
|
|
9
|
+
isStudyData: false,
|
|
10
|
+
});
|
|
11
|
+
const importer = new StaticWado(finalOptions);
|
|
12
|
+
return importer.executeCommand(program.args);
|
|
13
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const StaticWado = require("./StaticWado");
|
|
2
|
+
const adaptProgramOpts = require("./util/adaptProgramOpts");
|
|
3
|
+
|
|
4
|
+
module.exports = function createMain(options, program) {
|
|
5
|
+
const finalOptions = adaptProgramOpts(options, {
|
|
6
|
+
...this,
|
|
7
|
+
isInstance: false,
|
|
8
|
+
isGroup: false,
|
|
9
|
+
isStudyData: false,
|
|
10
|
+
});
|
|
11
|
+
const importer = new StaticWado(finalOptions);
|
|
12
|
+
// Either do a scan or on specified studies
|
|
13
|
+
return importer.executeCommand(program.args);
|
|
14
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const ConfigPoint = require("config-point");
|
|
2
|
+
const { staticWadoConfig } = require("@radicalimaging/static-wado-util");
|
|
3
|
+
const createMain = require("./createMain");
|
|
4
|
+
const deleteMain = require("./deleteMain");
|
|
5
|
+
const rejectMain = require("./rejectMain");
|
|
6
|
+
const instanceMain = require("./instanceMain");
|
|
7
|
+
const groupMain = require("./groupMain");
|
|
8
|
+
const metadataMain = require("./metadataMain");
|
|
9
|
+
const compressionOptionParser = require("./util/compressionOptionParser");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Defines the basic configuration values for the dicomwebserver component. See the README for more details.
|
|
13
|
+
* TODO - fix the default values so they come from the configuration file. In the meantime, leave the defaults blank.
|
|
14
|
+
*/
|
|
15
|
+
const { mkdicomwebConfig } = ConfigPoint.register({
|
|
16
|
+
mkdicomwebConfig: {
|
|
17
|
+
// This declare the inheritted configuration, don't assume this is directly accessible
|
|
18
|
+
configBase: staticWadoConfig,
|
|
19
|
+
isStudyData: true,
|
|
20
|
+
isGroup: true,
|
|
21
|
+
options: [
|
|
22
|
+
{
|
|
23
|
+
key: "-c, --clean",
|
|
24
|
+
description: "Clean the outputs before generating/starting to write new values.",
|
|
25
|
+
defaultValue: false,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "-v, --verbose",
|
|
29
|
+
description: "Write verbose output",
|
|
30
|
+
defaultValue: false,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: "-t, --content-type <type>",
|
|
34
|
+
description: 'Destination type to compress to (choices: "jls", "lei", "jls-lossy" or DICOM Transfer Syntax UID - default: "jls")',
|
|
35
|
+
defaultValue: "jls",
|
|
36
|
+
customParser: compressionOptionParser,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "-m, --maximum-inline-public-length <value>",
|
|
40
|
+
description: "Maximum length of public binary data",
|
|
41
|
+
defaultValue: 128 * 1024 + 2,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "-M, --maximum-inline-private-length <value>",
|
|
45
|
+
description: "Maximum length of private binary data",
|
|
46
|
+
defaultValue: 64,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "-r, --recompress <listvalue...>",
|
|
50
|
+
description: "List of types to recompress separated by space",
|
|
51
|
+
defaultValue: ["uncompressed", "jp2"],
|
|
52
|
+
choices: ["uncompressed", "jp2", "jpeglossless", "rle", "none"],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "--recompress-thumb <listvalue...>",
|
|
56
|
+
description: "List of types to recompress thumb separated by space",
|
|
57
|
+
defaultValue: ["uncompressed", "jp2"],
|
|
58
|
+
choices: ["uncompressed", "jp2", "jpeglossless", "rle", "none"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "--no-recompress",
|
|
62
|
+
description: "Force no recompression",
|
|
63
|
+
defaultValue: false,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "--no-recompress-thumb",
|
|
67
|
+
description: "Force no recompression thumbnail",
|
|
68
|
+
defaultValue: false,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "-T, --colour-content-type <value>",
|
|
72
|
+
description: "Colour content type",
|
|
73
|
+
defaultValue: null,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "--path-deduplicated <path>",
|
|
77
|
+
description: "Set the deduplicate data directory path (relative to dir)",
|
|
78
|
+
defaultValue: "deduplicated",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "--path-instances <path>",
|
|
82
|
+
description: "Set the instances directory path (relative to dir)",
|
|
83
|
+
defaultValue: "instances",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "--store-multipart-bulk-data",
|
|
87
|
+
description: "Use multipart encoding for non image frame bulkdata",
|
|
88
|
+
defaultValue: false,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "--prepend-bulk-data-uri <value>",
|
|
92
|
+
description: "Prepend bulkdata uri (ex. to use absolute Uri like http://host:3000/dicomweb)",
|
|
93
|
+
defaultValue: "",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: "--expand-bulk-data-uri",
|
|
97
|
+
description: "expand bulkdata relative uri to use full relative path (should also be set when using --prepend-bulk-data-uri)",
|
|
98
|
+
defaultValue: false,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: "-o, --dir <value>",
|
|
102
|
+
description: "Set output directory",
|
|
103
|
+
defaultValue: { configOperation: "reference", source: "staticWadoConfig", reference: "rootDir" },
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
programs: [
|
|
107
|
+
{
|
|
108
|
+
command: "create",
|
|
109
|
+
isDefault: true,
|
|
110
|
+
arguments: ["input"],
|
|
111
|
+
main: createMain,
|
|
112
|
+
helpDescription:
|
|
113
|
+
"Make DICOMweb query and metadata from binary Part 10 DICOM files. Does a full read of\n" +
|
|
114
|
+
"deduplicated files each time a study instance UID is found, and only updates those studies\n" +
|
|
115
|
+
"having at least one ",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
command: "instance",
|
|
119
|
+
arguments: ["input"],
|
|
120
|
+
main: instanceMain,
|
|
121
|
+
helpDescription: "Make instance level DICOMweb metadata and bulkdata, but don't group or write series metadata",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
command: "group",
|
|
125
|
+
arguments: ["input"],
|
|
126
|
+
main: groupMain,
|
|
127
|
+
helpDescription: "Group instance level metadata into deduplicated data.\nDeletes instance level deduplicated information once it is confirmed written.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
command: "metadata",
|
|
131
|
+
arguments: ["input"],
|
|
132
|
+
main: metadataMain,
|
|
133
|
+
helpDescription: "Write the metadata object (series and study details) from the deduplicated data.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
command: "delete",
|
|
137
|
+
main: deleteMain,
|
|
138
|
+
helpDescription: "Delete the given study, series or instance (not yet implemented)",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
command: "reject",
|
|
142
|
+
main: rejectMain,
|
|
143
|
+
helpDescription: "Reject the specified series, specified as studies/<studyUID>/series/<seriesUID>",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
module.exports = mkdicomwebConfig;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const hashFactory = require("node-object-hash");
|
|
2
|
+
const Tags = require("../dictionary/Tags");
|
|
3
|
+
|
|
4
|
+
/* eslint "no-param-reassign": "off" */
|
|
5
|
+
|
|
6
|
+
const hasher = hashFactory();
|
|
7
|
+
|
|
8
|
+
const { PatientID, PatientName, IssuerOfPatientID } = Tags;
|
|
9
|
+
const { StudyDescription, AccessionNumber, StudyInstanceUID, StudyDate, StudyTime } = Tags;
|
|
10
|
+
const { SeriesDescription, SeriesNumber, SeriesInstanceUID, SeriesDate, SeriesTime } = Tags;
|
|
11
|
+
|
|
12
|
+
const { DeduppedHash, DeduppedRef, DeduppedType, } = Tags;
|
|
13
|
+
|
|
14
|
+
const { pushList, getList, setValue, getValue } = Tags;
|
|
15
|
+
|
|
16
|
+
const PatientQuery = [
|
|
17
|
+
PatientID,
|
|
18
|
+
Tags.OtherPatientIDsSequence,
|
|
19
|
+
PatientName,
|
|
20
|
+
IssuerOfPatientID,
|
|
21
|
+
Tags.PatientBirthDate,
|
|
22
|
+
Tags.PatientBirthTime,
|
|
23
|
+
Tags.PatientSex,
|
|
24
|
+
Tags.PatientIdentityRemoved,
|
|
25
|
+
Tags.DeidentificationMethodCodeSequence,
|
|
26
|
+
];
|
|
27
|
+
const StudyQuery = [StudyDescription, AccessionNumber, StudyInstanceUID, StudyDate, StudyTime, Tags.StudyStatusID, Tags.StudyPriorityID, Tags.StudyID];
|
|
28
|
+
|
|
29
|
+
const PatientStudyQuery = [...PatientQuery, ...StudyQuery];
|
|
30
|
+
|
|
31
|
+
// The difference between the extracts and the query is that the query contains the parent query values
|
|
32
|
+
// TODO - make it into a self-cpontained object that can generate either
|
|
33
|
+
const SeriesExtract = [
|
|
34
|
+
SeriesDescription,
|
|
35
|
+
SeriesNumber,
|
|
36
|
+
SeriesInstanceUID,
|
|
37
|
+
Tags.Modality,
|
|
38
|
+
SeriesDate,
|
|
39
|
+
SeriesTime,
|
|
40
|
+
Tags.SpecificCharacterSet,
|
|
41
|
+
Tags.Manufacturer,
|
|
42
|
+
Tags.InstitutionName,
|
|
43
|
+
Tags.ReferringPhysicianName,
|
|
44
|
+
Tags.StationName,
|
|
45
|
+
Tags.OperatorsName,
|
|
46
|
+
Tags.ManufacturerModelName,
|
|
47
|
+
Tags.BodyPartExamined,
|
|
48
|
+
Tags.ProtocolName,
|
|
49
|
+
Tags.RequestedProcedureDescription,
|
|
50
|
+
];
|
|
51
|
+
const SeriesQuery = [StudyInstanceUID, ...SeriesExtract];
|
|
52
|
+
|
|
53
|
+
const InstanceQuery = [
|
|
54
|
+
Tags.SOPInstanceUID,
|
|
55
|
+
Tags.InstanceNumber,
|
|
56
|
+
Tags.SeriesInstanceUID,
|
|
57
|
+
Tags.StudyInstanceUID,
|
|
58
|
+
Tags.ContentDate,
|
|
59
|
+
Tags.ContentTime,
|
|
60
|
+
Tags.AcquisitionDate,
|
|
61
|
+
Tags.AcquisitionTime,
|
|
62
|
+
Tags.AcquisitionNumber,
|
|
63
|
+
Tags.SamplesPerPixel,
|
|
64
|
+
Tags.PhotometricInterpretation,
|
|
65
|
+
Tags.PlanarConfiguration,
|
|
66
|
+
Tags.Rows,
|
|
67
|
+
Tags.Columns,
|
|
68
|
+
Tags.BitsAllocated,
|
|
69
|
+
Tags.BitsStored,
|
|
70
|
+
Tags.HighBit,
|
|
71
|
+
Tags.PixelRepresentation,
|
|
72
|
+
Tags.WindowCenter,
|
|
73
|
+
Tags.WindowWidth,
|
|
74
|
+
Tags.WindowCenterWidthExplanation,
|
|
75
|
+
Tags.InstanceCreationTime,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const ImageExtract = [
|
|
79
|
+
Tags.ImageType,
|
|
80
|
+
Tags.InstanceCreationDate,
|
|
81
|
+
Tags.SOPClassUID,
|
|
82
|
+
Tags.ConversionType,
|
|
83
|
+
Tags.ManufacturerModelName,
|
|
84
|
+
Tags.ContentDate,
|
|
85
|
+
Tags.AcquisitionDate,
|
|
86
|
+
Tags.SecondaryCaptureDeviceID,
|
|
87
|
+
Tags.DateOfSecondaryCapture,
|
|
88
|
+
Tags.SecondaryCaptureDeviceManufacturer,
|
|
89
|
+
Tags.SecondaryCaptureDeviceManufacturerModelName,
|
|
90
|
+
Tags.SecondaryCaptureDeviceSoftwareVersions,
|
|
91
|
+
Tags.SoftwareVersions,
|
|
92
|
+
Tags.SamplesPerPixel,
|
|
93
|
+
Tags.PhotometricInterpretation,
|
|
94
|
+
Tags.PlanarConfiguration,
|
|
95
|
+
Tags.Rows,
|
|
96
|
+
Tags.Columns,
|
|
97
|
+
Tags.BitsAllocated,
|
|
98
|
+
Tags.BitsStored,
|
|
99
|
+
Tags.HighBit,
|
|
100
|
+
Tags.PixelRepresentation,
|
|
101
|
+
Tags.WindowCenter,
|
|
102
|
+
Tags.WindowWidth,
|
|
103
|
+
Tags.WindowCenterWidthExplanation,
|
|
104
|
+
Tags.PatientPosition,
|
|
105
|
+
Tags.PatientOrientation,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const addHash = (data, type) => {
|
|
109
|
+
const existing = getValue(data,DeduppedHash);
|
|
110
|
+
if (existing) {
|
|
111
|
+
return existing;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const hashValue = hasher.hash(data);
|
|
115
|
+
setValue(data, DeduppedHash, hashValue );
|
|
116
|
+
setValue(data, DeduppedType, type );
|
|
117
|
+
return hashValue;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const TagSets = {
|
|
121
|
+
PatientQuery,
|
|
122
|
+
StudyQuery,
|
|
123
|
+
PatientStudyQuery,
|
|
124
|
+
SeriesQuery,
|
|
125
|
+
SeriesExtract,
|
|
126
|
+
InstanceQuery,
|
|
127
|
+
ImageExtract,
|
|
128
|
+
|
|
129
|
+
QueryExtract: { remove: false, hashRef: false },
|
|
130
|
+
RemoveExtract: { remove: true, hashRef: true },
|
|
131
|
+
|
|
132
|
+
extract: (data, type, tagSet, options) => {
|
|
133
|
+
const ret = {};
|
|
134
|
+
const { remove, hashRef } = options || {};
|
|
135
|
+
tagSet.forEach((tag) => {
|
|
136
|
+
const value = data[tag];
|
|
137
|
+
if (value) {
|
|
138
|
+
ret[tag] = value;
|
|
139
|
+
if (remove) delete data[tag];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const hashValue = addHash(ret, type);
|
|
143
|
+
if (hashRef) {
|
|
144
|
+
pushList(data,DeduppedRef,hashValue);
|
|
145
|
+
}
|
|
146
|
+
return ret;
|
|
147
|
+
},
|
|
148
|
+
addHash,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
module.exports = TagSets;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const octetStream = "application/octet-stream";
|
|
2
|
+
const uncompressed = {
|
|
3
|
+
uncompressed: true,
|
|
4
|
+
gzip: true,
|
|
5
|
+
contentType: octetStream,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @see {@link https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.3.3.2.html}
|
|
10
|
+
*/
|
|
11
|
+
const jpeg = "image/jpeg";
|
|
12
|
+
const jls = "image/jls";
|
|
13
|
+
const jll = "image/jll";
|
|
14
|
+
const jxl = "image/x-jxl";
|
|
15
|
+
const htj2k = "image/x-htj2k";
|
|
16
|
+
const jp2 = "image/jp2";
|
|
17
|
+
const rle = "image/dicom-rle";
|
|
18
|
+
|
|
19
|
+
const uids = {
|
|
20
|
+
"1.2.840.10008.1.2": uncompressed,
|
|
21
|
+
"1.2.840.10008.1.2.1": uncompressed,
|
|
22
|
+
"1.2.840.10008.1.2.1.99": uncompressed,
|
|
23
|
+
"1.2.840.10008.1.2.2": uncompressed,
|
|
24
|
+
"1.2.840.10008.1.2.4.50": { contentType: jpeg, lossy: true },
|
|
25
|
+
"1.2.840.10008.1.2.4.51": { contentType: jpeg, lossy: true },
|
|
26
|
+
"1.2.840.10008.1.2.4.57": { contentType: jpeg },
|
|
27
|
+
"1.2.840.10008.1.2.4.70": { contentType: jll },
|
|
28
|
+
"1.2.840.10008.1.2.4.140": { contentType: jxl },
|
|
29
|
+
"1.2.840.10008.1.2.4.141": { contentType: jxl, lossy: true },
|
|
30
|
+
"1.2.840.10008.1.2.4.80": { contentType: jls },
|
|
31
|
+
"1.2.840.10008.1.2.4.81": { contentType: jls, lossy: true },
|
|
32
|
+
"1.2.840.10008.1.2.4.90": { contentType: jp2 },
|
|
33
|
+
"1.2.840.10008.1.2.4.91": { contentType: jp2 },
|
|
34
|
+
// From JPEG original data, lossless to JXL
|
|
35
|
+
"1.2.840.10008.1.2.4.142": { contentType: jxl, lossy: true },
|
|
36
|
+
"1.2.840.10008.1.2.4.150": { contentType: htj2k },
|
|
37
|
+
// **
|
|
38
|
+
"1.2.840.10008.1.2.5": { contentType: rle },
|
|
39
|
+
default: {},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports = uids;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// TODO review returning type of some methods
|
|
2
|
+
const JSONWriter = require("../writer/JSONWriter");
|
|
3
|
+
const TagLists = require("../model/TagLists");
|
|
4
|
+
const Tags = require("../dictionary/Tags");
|
|
5
|
+
|
|
6
|
+
const {getValue, setValue} = Tags;
|
|
7
|
+
|
|
8
|
+
const extractors = {
|
|
9
|
+
patient: TagLists.PatientQuery,
|
|
10
|
+
study: TagLists.StudyQuery,
|
|
11
|
+
series: TagLists.SeriesExtract,
|
|
12
|
+
image: TagLists.ImageExtract,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This is an instance listener - the way this one works is that it listens for instance metadata.
|
|
17
|
+
* Every time an instance metadata event is fired, it deduplicates a bunch of data, firing deduplicate events,
|
|
18
|
+
* and then collects the deduplicate data into a list.
|
|
19
|
+
* Whenever the study UID changes, a new event is fired to indicate the deduplicated data is ready.
|
|
20
|
+
*/
|
|
21
|
+
async function deduplicateSingleInstance(id, imageFrame) {
|
|
22
|
+
if (!imageFrame) return {};
|
|
23
|
+
const studyData = await this.completeStudy.getCurrentStudyData(this, id);
|
|
24
|
+
const seriesUID = getValue(imageFrame,Tags.SeriesInstanceUID);
|
|
25
|
+
const sopUID = getValue(imageFrame,Tags.SOPInstanceUID);
|
|
26
|
+
if (!sopUID) {
|
|
27
|
+
console.warn("No sop instance UID in", imageFrame);
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
if (studyData.sopExists(sopUID)) {
|
|
31
|
+
if( this.verbose ) console.log('SOP Instance UID', sopUID, 'already exists, skipping');
|
|
32
|
+
// TODO - allow replace as an option
|
|
33
|
+
// Null value means skip writing this instance
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if( this.verbose ) console.log('SOP Instance UID', sopUID, 'being added');
|
|
38
|
+
const deduplicated = { ...imageFrame };
|
|
39
|
+
|
|
40
|
+
if (!this.extractors) this.extractors = extractors;
|
|
41
|
+
for (const key of Object.keys(this.extractors)) {
|
|
42
|
+
const extracted = TagLists.extract(deduplicated, key, this.extractors[key], TagLists.RemoveExtract);
|
|
43
|
+
const hashKey = getValue(extracted,Tags.DeduppedHash);
|
|
44
|
+
await studyData.addExtracted(this, hashKey, extracted);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Restore the series and SOP UIDs
|
|
48
|
+
setValue(deduplicated, Tags.SeriesInstanceUID, seriesUID);
|
|
49
|
+
setValue(deduplicated, Tags.SOPInstanceUID, sopUID);
|
|
50
|
+
TagLists.addHash(deduplicated, Tags.InstanceType);
|
|
51
|
+
|
|
52
|
+
return deduplicated;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Canonicalize the JSON data, making Values always arrays, remote "undefined" from the tags etc */
|
|
56
|
+
const canonicalize = (json) => {
|
|
57
|
+
if (!json) return undefined;
|
|
58
|
+
if (Array.isArray(json)) {
|
|
59
|
+
return json.map(canonicalize);
|
|
60
|
+
}
|
|
61
|
+
Object.keys(json).forEach((tag) => {
|
|
62
|
+
if (!tag || tag === "undefined") {
|
|
63
|
+
// console.error('Got an undefined tag:', tag, typeof(tag));
|
|
64
|
+
// TODO check returning value
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const val = json[tag];
|
|
68
|
+
const { Value } = val || {};
|
|
69
|
+
if (Value && !Array.isArray(Value)) {
|
|
70
|
+
val.Value = [Value];
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return json;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const InstanceDeduplicate = (options) =>
|
|
77
|
+
async function run(id, sourceImageFrame) {
|
|
78
|
+
// Notify the existing listeners, if any
|
|
79
|
+
const imageFrame = canonicalize(sourceImageFrame);
|
|
80
|
+
if (options.isInstanceMetadata) {
|
|
81
|
+
// console.log("Writing instance metadata");
|
|
82
|
+
await JSONWriter(id.sopInstanceRootPath, "metadata", imageFrame);
|
|
83
|
+
}
|
|
84
|
+
if (!options.isDeduplicate && !options.isGroup) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!this.deduplicateSingleInstance) {
|
|
89
|
+
this.deduplicateSingleInstance = deduplicateSingleInstance;
|
|
90
|
+
this.verbose = options.verbose;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const deduppedInstance = await this.deduplicateSingleInstance(id, imageFrame);
|
|
94
|
+
if (deduppedInstance) {
|
|
95
|
+
// this refers to callee
|
|
96
|
+
await this.deduplicated(id, deduppedInstance);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
module.exports = InstanceDeduplicate;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
function ScanStudy(options) {
|
|
4
|
+
const { directoryName, deduplicatedRoot, deduplicatedInstancesRoot } = options;
|
|
5
|
+
|
|
6
|
+
return function run(dir, studyInstanceUid) {
|
|
7
|
+
const studyPath = path.join(directoryName, "studies", studyInstanceUid);
|
|
8
|
+
const deduplicatedPath = path.join(deduplicatedRoot, studyInstanceUid);
|
|
9
|
+
const deduplicatedInstancesPath = path.join(deduplicatedInstancesRoot, studyInstanceUid);
|
|
10
|
+
console.log("Scanning", dir, studyInstanceUid);
|
|
11
|
+
return this.completeStudy.getCurrentStudyData(this, {
|
|
12
|
+
studyPath,
|
|
13
|
+
deduplicatedPath,
|
|
14
|
+
deduplicatedInstancesPath,
|
|
15
|
+
studyInstanceUid,
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = ScanStudy;
|