dicom-curate 0.6.1 → 0.7.0

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 CHANGED
@@ -97,8 +97,7 @@ import type { TCurationSpecification } from 'dicom-curate'
97
97
  * Curation specification for batch-curating DICOM files.
98
98
  */
99
99
  export function sampleBatchCurationSpecification(): TCurationSpecification {
100
- // Confirm allowed identifiers for this transfer.
101
- const identifiers = {
100
+ const hostProps = {
102
101
  protocolNumber: 'Sample_Protocol_Number',
103
102
  activityProviderName: 'Sample_CRO',
104
103
  centerSubjectId: /^[A-Z]{2}\d{2}-\d{3}$/,
@@ -117,8 +116,8 @@ export function sampleBatchCurationSpecification(): TCurationSpecification {
117
116
  // collect from a csv file. A client can use regex to validate the input.
118
117
  type: 'load',
119
118
  collect: {
120
- CURR_ID: identifiers.centerSubjectId,
121
- StudyDescription: identifiers.timepointNames,
119
+ CURR_ID: hostProps.centerSubjectId,
120
+ StudyDescription: hostProps.timepointNames,
122
121
  MAPPED_ID: /BLIND_\d+/,
123
122
  },
124
123
  // With this, can refer to mappings as parser.getMapping('blindedId')
@@ -132,8 +131,8 @@ export function sampleBatchCurationSpecification(): TCurationSpecification {
132
131
  },
133
132
  },
134
133
 
135
- version: '2.0',
136
- identifiers,
134
+ version: '3.0',
135
+ hostProps,
137
136
 
138
137
  // This specifies the standardized DICOM de-identification
139
138
  dicomPS315EOptions: {
@@ -141,10 +140,10 @@ export function sampleBatchCurationSpecification(): TCurationSpecification {
141
140
  cleanDescriptorsExceptions: ['SeriesDescription'],
142
141
  retainLongitudinalTemporalInformationOptions: 'Full',
143
142
  retainPatientCharacteristicsOption: [
144
- 'PatientsWeight',
145
- 'PatientsSize',
146
- 'PatientsAge',
147
- 'PatientsSex',
143
+ 'PatientWeight',
144
+ 'PatientSize',
145
+ 'PatientAge',
146
+ 'PatientSex',
148
147
  'SelectorASValue',
149
148
  ],
150
149
  retainDeviceIdentityOption: true,
@@ -153,60 +152,53 @@ export function sampleBatchCurationSpecification(): TCurationSpecification {
153
152
  retainInstitutionIdentityOption: true,
154
153
  },
155
154
 
156
- // This section defines the output folder structure and alignment of DICOM headers
157
- modifications(parser) {
155
+ modifyDicomHeader(parser) {
158
156
  const scan = parser.getFilePathComp('scan')
159
157
  const centerSubjectId = parser.getFilePathComp('centerSubjectId')
160
158
 
161
159
  return {
162
- dicomHeader: {
163
- // Align the PatientID DICOM header with the centerSubjectId folder name.
164
- PatientID: centerSubjectId,
165
- // This example maps PatientIDs based on the mapping CSV file.
166
- // PatientID: parser.getMapping('blindedId'),
167
- PatientName: centerSubjectId,
168
- // Align the StudyDescription DICOM header with the timepoint folder name.
169
- StudyDescription: parser.getFilePathComp('timepoint'),
170
- // The party responsible for assigning a standard ClinicalTrialSeriesDescription
171
- ClinicalTrialCoordinatingCenterName: identifiers.activityProviderName,
172
- // Align the ClinicalTrialSeriesDescription DICOM header with the scan folder name.
173
- ClinicalTrialSeriesDescription: scan,
174
- },
175
-
176
- // This defines the output folder structure.
177
- outputFilePathComponents: [
178
- parser.getFilePathComp('protocolNumber'),
179
- parser.getFilePathComp('activityProvider'),
180
- centerSubjectId,
181
- parser.getFilePathComp('timepoint'),
182
- parser.getFilePathComp('scan') +
183
- '=' +
184
- parser.getDicom('SeriesNumber'),
185
- parser.getFilePathComp(parser.FILEBASENAME) + '.dcm',
186
- ],
160
+ // Align the PatientID DICOM header with the centerSubjectId folder name.
161
+ PatientID: centerSubjectId,
162
+ // This example maps PatientIDs based on the mapping CSV file.
163
+ // PatientID: parser.getMapping('blindedId'),
164
+ PatientName: centerSubjectId,
165
+ // Align the StudyDescription DICOM header with the timepoint folder name.
166
+ StudyDescription: parser.getFilePathComp('timepoint'),
167
+ // The party responsible for assigning a standard ClinicalTrialSeriesDescription
168
+ ClinicalTrialCoordinatingCenterName: hostProps.activityProviderName,
169
+ // Align the ClinicalTrialSeriesDescription DICOM header with the scan folder name.
170
+ ClinicalTrialSeriesDescription: scan,
187
171
  }
188
172
  },
189
173
 
174
+ outputFilePathComponents(parser) {
175
+ const scan = parser.getFilePathComp('scan')
176
+ const centerSubjectId = parser.getFilePathComp('centerSubjectId')
177
+
178
+ return [
179
+ parser.getFilePathComp('protocolNumber'),
180
+ parser.getFilePathComp('activityProvider'),
181
+ centerSubjectId,
182
+ parser.getFilePathComp('timepoint'),
183
+ scan + '=' + parser.getDicom('SeriesNumber'),
184
+ parser.getFilePathComp(parser.FILEBASENAME) + '.dcm',
185
+ ]
186
+ },
187
+
190
188
  // This section defines the validation rules for the input DICOMs.
191
189
  // The processing continues on errors, but errors will have to be fixed
192
190
  // or reviewed between the parties.
193
- validation(parser) {
194
- const modality = parser.getDicom('Modality')
195
- const filename = parser.getFilePathComp(parser.FILEBASENAME)
196
- const seriesUid = parser.getDicom('SeriesInstanceUID')
197
-
198
- return {
199
- errors: [
200
- // File path
201
- [
202
- 'Invalid study folder name',
203
- parser.getFilePathComp('protocolNumber') !==
204
- identifiers.protocolNumber,
205
- ],
206
- // DICOM header
207
- ['Missing Modality', parser.missingDicom('Modality')],
191
+ errors(parser) {
192
+ return [
193
+ // File path
194
+ [
195
+ 'Invalid study folder name',
196
+ parser.getFilePathComp('protocolNumber') !== hostProps.protocolNumber,
208
197
  ],
209
- }
198
+ // DICOM header
199
+ ['Missing Modality', parser.missingDicom('Modality')],
200
+ ['Missing SOP Class UID', parser.missingDicom('SOPClassUID')],
201
+ ]
210
202
  },
211
203
  }
212
204
  }
@@ -36670,7 +36670,7 @@ function getParser(inputPathPattern, inputFilePath, naturalData, dicomPS315EOpti
36670
36670
  }
36671
36671
 
36672
36672
  // src/config/specVersion.ts
36673
- var specVersion = "2.0";
36673
+ var specVersion = "3.0";
36674
36674
 
36675
36675
  // src/collectMappings.ts
36676
36676
  var import_lodash2 = __toESM(require_lodash(), 1);
@@ -36693,18 +36693,14 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
36693
36693
  let finalSpec = {
36694
36694
  dicomPS315EOptions: defaultPs315Options,
36695
36695
  inputPathPattern: "",
36696
- modifications(parser2) {
36697
- return {
36698
- dicomHeader: {},
36699
- outputFilePathComponents: [
36700
- parser2.protectUid(parser2.getDicom("SeriesInstanceUID")),
36701
- parser2.getFilePathComp(parser2.FILENAME)
36702
- ]
36703
- };
36704
- },
36705
- validation: () => ({ errors: [] })
36696
+ modifyDicomHeader: () => ({}),
36697
+ outputFilePathComponents: (parser2) => [
36698
+ parser2.protectUid(parser2.getDicom("SeriesInstanceUID")),
36699
+ parser2.getFilePathComp(parser2.FILENAME)
36700
+ ],
36701
+ errors: () => []
36706
36702
  };
36707
- const { modifications, validation, ...restSpec } = mappingOptions.curationSpec();
36703
+ const { modifyDicomHeader, errors, ...restSpec } = mappingOptions.curationSpec();
36708
36704
  if (restSpec.version !== specVersion) {
36709
36705
  throw new Error(
36710
36706
  `Only version ${specVersion} supported in curationSpecification`
@@ -36712,10 +36708,10 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
36712
36708
  }
36713
36709
  Object.assign(finalSpec, restSpec);
36714
36710
  if (!mappingOptions.skipModifications) {
36715
- finalSpec.modifications = modifications;
36711
+ finalSpec.modifyDicomHeader = modifyDicomHeader;
36716
36712
  }
36717
36713
  if (!mappingOptions.skipValidation) {
36718
- finalSpec.validation = validation;
36714
+ finalSpec.errors = errors;
36719
36715
  }
36720
36716
  const finalFilePath = finalSpec.dicomPS315EOptions === "Off" ? inputFilePath : inputFilePath.slice(0, inputFilePath.lastIndexOf("/") + 1) + `${String(inputFileIndex + 1).padStart(5, "0")}.dcm`;
36721
36717
  const parser = getParser(
@@ -36726,8 +36722,7 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
36726
36722
  mappingOptions.columnMappings,
36727
36723
  finalSpec.additionalData
36728
36724
  );
36729
- let modificationMap = finalSpec.modifications(parser);
36730
- mapResults.errors = finalSpec.validation(parser).errors.filter(([, failure]) => failure).map(([message]) => message);
36725
+ mapResults.errors = finalSpec.errors(parser).filter(([, failure]) => failure).map(([message]) => message);
36731
36726
  if (finalSpec.additionalData?.type === "listing") {
36732
36727
  const { lookups, info, collect } = finalSpec.additionalData.collect(parser);
36733
36728
  const collectByValue = collect.map((item) => {
@@ -36744,7 +36739,7 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
36744
36739
  });
36745
36740
  mapResults.listing = { info: cleanedInfo, collectByValue };
36746
36741
  }
36747
- mapResults.outputFilePath = modificationMap.outputFilePathComponents.join("/");
36742
+ mapResults.outputFilePath = finalSpec.outputFilePathComponents(parser).join("/");
36748
36743
  if (finalSpec.dicomPS315EOptions !== "Off") {
36749
36744
  deidentifyPS315E({
36750
36745
  naturalData,
@@ -36754,7 +36749,7 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
36754
36749
  originalDicomDict: dicomData.dict
36755
36750
  });
36756
36751
  }
36757
- const dicomMap = modificationMap.dicomHeader;
36752
+ const dicomMap = finalSpec.modifyDicomHeader(parser);
36758
36753
  for (let attrPath in dicomMap) {
36759
36754
  mapResults.mappings[attrPath] = [
36760
36755
  (0, import_lodash2.get)(naturalData, attrPath),
@@ -30341,7 +30341,7 @@ function getParser(inputPathPattern, inputFilePath, naturalData, dicomPS315EOpti
30341
30341
  }
30342
30342
 
30343
30343
  // src/config/specVersion.ts
30344
- var specVersion = "2.0";
30344
+ var specVersion = "3.0";
30345
30345
 
30346
30346
  // src/collectMappings.ts
30347
30347
  var import_lodash2 = __toESM(require_lodash(), 1);
@@ -30364,18 +30364,14 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
30364
30364
  let finalSpec = {
30365
30365
  dicomPS315EOptions: defaultPs315Options,
30366
30366
  inputPathPattern: "",
30367
- modifications(parser2) {
30368
- return {
30369
- dicomHeader: {},
30370
- outputFilePathComponents: [
30371
- parser2.protectUid(parser2.getDicom("SeriesInstanceUID")),
30372
- parser2.getFilePathComp(parser2.FILENAME)
30373
- ]
30374
- };
30375
- },
30376
- validation: () => ({ errors: [] })
30367
+ modifyDicomHeader: () => ({}),
30368
+ outputFilePathComponents: (parser2) => [
30369
+ parser2.protectUid(parser2.getDicom("SeriesInstanceUID")),
30370
+ parser2.getFilePathComp(parser2.FILENAME)
30371
+ ],
30372
+ errors: () => []
30377
30373
  };
30378
- const { modifications, validation, ...restSpec } = mappingOptions.curationSpec();
30374
+ const { modifyDicomHeader, errors, ...restSpec } = mappingOptions.curationSpec();
30379
30375
  if (restSpec.version !== specVersion) {
30380
30376
  throw new Error(
30381
30377
  `Only version ${specVersion} supported in curationSpecification`
@@ -30383,10 +30379,10 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
30383
30379
  }
30384
30380
  Object.assign(finalSpec, restSpec);
30385
30381
  if (!mappingOptions.skipModifications) {
30386
- finalSpec.modifications = modifications;
30382
+ finalSpec.modifyDicomHeader = modifyDicomHeader;
30387
30383
  }
30388
30384
  if (!mappingOptions.skipValidation) {
30389
- finalSpec.validation = validation;
30385
+ finalSpec.errors = errors;
30390
30386
  }
30391
30387
  const finalFilePath = finalSpec.dicomPS315EOptions === "Off" ? inputFilePath : inputFilePath.slice(0, inputFilePath.lastIndexOf("/") + 1) + `${String(inputFileIndex + 1).padStart(5, "0")}.dcm`;
30392
30388
  const parser = getParser(
@@ -30397,8 +30393,7 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
30397
30393
  mappingOptions.columnMappings,
30398
30394
  finalSpec.additionalData
30399
30395
  );
30400
- let modificationMap = finalSpec.modifications(parser);
30401
- mapResults.errors = finalSpec.validation(parser).errors.filter(([, failure]) => failure).map(([message]) => message);
30396
+ mapResults.errors = finalSpec.errors(parser).filter(([, failure]) => failure).map(([message]) => message);
30402
30397
  if (finalSpec.additionalData?.type === "listing") {
30403
30398
  const { lookups, info, collect } = finalSpec.additionalData.collect(parser);
30404
30399
  const collectByValue = collect.map((item) => {
@@ -30415,7 +30410,7 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
30415
30410
  });
30416
30411
  mapResults.listing = { info: cleanedInfo, collectByValue };
30417
30412
  }
30418
- mapResults.outputFilePath = modificationMap.outputFilePathComponents.join("/");
30413
+ mapResults.outputFilePath = finalSpec.outputFilePathComponents(parser).join("/");
30419
30414
  if (finalSpec.dicomPS315EOptions !== "Off") {
30420
30415
  deidentifyPS315E({
30421
30416
  naturalData,
@@ -30425,7 +30420,7 @@ function collectMappings(inputFilePath, inputFileIndex, dicomData, mappingOption
30425
30420
  originalDicomDict: dicomData.dict
30426
30421
  });
30427
30422
  }
30428
- const dicomMap = modificationMap.dicomHeader;
30423
+ const dicomMap = finalSpec.modifyDicomHeader(parser);
30429
30424
  for (let attrPath in dicomMap) {
30430
30425
  mapResults.mappings[attrPath] = [
30431
30426
  (0, import_lodash2.get)(naturalData, attrPath),
@@ -1,6 +1,6 @@
1
1
  // src/config/sample2PassCurationSpecification.ts
2
2
  function sample2PassCurationSpecification() {
3
- const identifiers = {
3
+ const hostProps = {
4
4
  protocolNumber: "Sample_Protocol_Number",
5
5
  activityProviderName: "Sample_CRO",
6
6
  centerSubjectId: /^[A-Z]{2}\d{2}-\d{3}$/,
@@ -24,7 +24,7 @@ function sample2PassCurationSpecification() {
24
24
  lookups,
25
25
  info: [
26
26
  // [label, value]
27
- ["Protocol Number", identifiers.protocolNumber],
27
+ ["Protocol Number", hostProps.protocolNumber],
28
28
  ["Patient Name", parser.getDicom("PatientName")],
29
29
  ["Patient ID", parser.getDicom("PatientID")],
30
30
  ["Study Date", parser.getDicom("StudyDate")],
@@ -37,12 +37,12 @@ function sample2PassCurationSpecification() {
37
37
  // PatNamePatId | PatNameIDSeriesDesc | CenterSubjectId | Timepoint | Comment
38
38
  // [lookup header, format, lookup value]
39
39
  collect: [
40
- ["CenterSubjectId", identifiers.centerSubjectId, "PatNamePatId"],
40
+ ["CenterSubjectId", hostProps.centerSubjectId, "PatNamePatId"],
41
41
  // For now, collecting this on Series not Study level.
42
42
  // Trade-off is re-use (study level) vs avoiding confusion due to
43
43
  // any mismatch on study vs series level (e.g. 2 studies one visit)
44
- ["Timepoint", identifiers.timepointNames, "PatNameIDSeriesDesc"],
45
- ["ScanName", identifiers.scanNames, "PatNameIDSeriesDesc"],
44
+ ["Timepoint", hostProps.timepointNames, "PatNameIDSeriesDesc"],
45
+ ["ScanName", hostProps.scanNames, "PatNameIDSeriesDesc"],
46
46
  ["Comment", /.*/, "PatNameIDSeriesDesc"]
47
47
  ]
48
48
  };
@@ -67,18 +67,18 @@ function sample2PassCurationSpecification() {
67
67
  }
68
68
  }
69
69
  },
70
- version: "2.0",
71
- identifiers,
70
+ version: "3.0",
71
+ hostProps,
72
72
  // This specifies the standardized DICOM de-identification
73
73
  dicomPS315EOptions: {
74
74
  cleanDescriptorsOption: true,
75
75
  cleanDescriptorsExceptions: ["SeriesDescription"],
76
76
  retainLongitudinalTemporalInformationOptions: "Full",
77
77
  retainPatientCharacteristicsOption: [
78
- "PatientsWeight",
79
- "PatientsSize",
80
- "PatientsAge",
81
- "PatientsSex",
78
+ "PatientWeight",
79
+ "PatientSize",
80
+ "PatientAge",
81
+ "PatientSex",
82
82
  "SelectorASValue"
83
83
  ],
84
84
  retainDeviceIdentityOption: true,
@@ -86,77 +86,70 @@ function sample2PassCurationSpecification() {
86
86
  retainSafePrivateOption: "Quarantine",
87
87
  retainInstitutionIdentityOption: true
88
88
  },
89
- // This section defines the output folder structure and alignment of DICOM headers
90
- modifications(parser) {
89
+ modifyDicomHeader(parser) {
91
90
  const centerSubjectId = String(parser.getMapping("centerSubjectId"));
92
91
  const timepoint = String(parser.getMapping("timepoint"));
93
92
  const scanName = String(parser.getMapping("scanName"));
94
93
  return {
95
- dicomHeader: {
96
- // Align the PatientID DICOM header with the centerSubjectId folder name.
97
- PatientID: centerSubjectId,
98
- // This example maps PatientIDs based on the mapping CSV file.
99
- // PatientID: parser.getMapping('blindedId'),
100
- PatientName: centerSubjectId,
101
- // Align the StudyDescription DICOM header with the timepoint folder name.
102
- StudyDescription: timepoint,
103
- // The party responsible for assigning a standard ClinicalTrialSeriesDescription
104
- // ClinicalTrialCoordinatingCenterName: identifiers.activityProviderName,
105
- // Align the ClinicalTrialSeriesDescription DICOM header with the scan folder name.
106
- ClinicalTrialSeriesDescription: scanName
107
- },
108
- // This defines the output folder structure.
109
- outputFilePathComponents: [
110
- identifiers.protocolNumber,
111
- centerSubjectId,
112
- timepoint,
113
- scanName + "=" + parser.getDicom("SeriesNumber"),
114
- parser.getFilePathComp(parser.FILEBASENAME) + ".dcm"
115
- ]
94
+ // Align the PatientID DICOM header with the centerSubjectId folder name.
95
+ PatientID: centerSubjectId,
96
+ // This example maps PatientIDs based on the mapping CSV file.
97
+ // PatientID: parser.getMapping('blindedId'),
98
+ PatientName: centerSubjectId,
99
+ // Align the StudyDescription DICOM header with the timepoint folder name.
100
+ StudyDescription: timepoint,
101
+ // The party responsible for assigning a standard ClinicalTrialSeriesDescription
102
+ // ClinicalTrialCoordinatingCenterName: hostProps.activityProviderName,
103
+ // Align the ClinicalTrialSeriesDescription DICOM header with the scan folder name.
104
+ ClinicalTrialSeriesDescription: scanName
116
105
  };
117
106
  },
107
+ outputFilePathComponents(parser) {
108
+ const centerSubjectId = String(parser.getMapping("centerSubjectId"));
109
+ const timepoint = String(parser.getMapping("timepoint"));
110
+ const scanName = String(parser.getMapping("scanName"));
111
+ return [
112
+ hostProps.protocolNumber,
113
+ centerSubjectId,
114
+ timepoint,
115
+ scanName + "=" + parser.getDicom("SeriesNumber"),
116
+ parser.getFilePathComp(parser.FILEBASENAME) + ".dcm"
117
+ ];
118
+ },
118
119
  // This section defines the validation rules for the input DICOMs.
119
120
  // The processing continues on errors, but errors will have to be fixed
120
121
  // or reviewed between the parties.
121
- validation(parser) {
122
+ errors(parser) {
122
123
  const filename = parser.getFilePathComp(parser.FILENAME);
123
124
  const seriesUid = parser.getDicom("SeriesInstanceUID");
124
125
  const centerSubjectId = String(parser.getMapping("centerSubjectId"));
125
126
  const timepoint = String(parser.getMapping("timepoint"));
126
127
  const scanName = String(parser.getMapping("scanName"));
127
- return {
128
- errors: [
129
- // File path
130
- [
131
- "Invalid site-subject format",
132
- !centerSubjectId.match(identifiers.centerSubjectId)
133
- ],
134
- [
135
- "Invalid timepoint descriptor",
136
- !identifiers.timepointNames.includes(timepoint)
137
- ],
138
- [
139
- "Invalid scan descriptor",
140
- !identifiers.scanNames.includes(scanName)
141
- ],
142
- // DICOM header
143
- ["Missing Modality", parser.missingDicom("Modality")],
144
- ["Missing SOP Class UID", parser.missingDicom("SOPClassUID")],
145
- [
146
- "Duplicate File Name(s) in series",
147
- !parser.isUniqueInGroup(filename, seriesUid)
148
- ],
149
- [
150
- "Missing Series Instance UID",
151
- parser.missingDicom("SeriesInstanceUID")
152
- ],
153
- [
154
- "Missing Study Instance UID",
155
- parser.missingDicom("StudyInstanceUID")
156
- ],
157
- ["Missing SOP Instance UID", parser.missingDicom("SOPInstanceUID")]
158
- ]
159
- };
128
+ return [
129
+ // File path
130
+ [
131
+ "Invalid site-subject format",
132
+ !centerSubjectId.match(hostProps.centerSubjectId)
133
+ ],
134
+ [
135
+ "Invalid timepoint descriptor",
136
+ !hostProps.timepointNames.includes(timepoint)
137
+ ],
138
+ ["Invalid scan descriptor", !hostProps.scanNames.includes(scanName)],
139
+ // DICOM header
140
+ ["Missing Modality", parser.missingDicom("Modality")],
141
+ ["Missing SOP Class UID", parser.missingDicom("SOPClassUID")],
142
+ [
143
+ "Duplicate File Name(s) in series",
144
+ !parser.isUniqueInGroup(filename, seriesUid)
145
+ ],
146
+ [
147
+ "Missing Series Instance UID",
148
+ parser.missingDicom("SeriesInstanceUID")
149
+ ],
150
+ ["Missing Study Instance UID", parser.missingDicom("StudyInstanceUID")],
151
+ ["Missing SOP Instance UID", parser.missingDicom("SOPInstanceUID")]
152
+ ];
160
153
  }
161
154
  };
162
155
  }