Skip to content

Commit 766577d

Browse files
authored
Merge pull request #4016 from melissalinkert/dicom-provide-metadata
Add API and DICOM-specific implementations for providing supplemental metadata during conversion
2 parents d519601 + caccc63 commit 766577d

File tree

12 files changed

+1153
-12
lines changed

12 files changed

+1153
-12
lines changed

components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import loci.formats.meta.IPyramidStore;
7979
import loci.formats.out.DicomWriter;
8080
import loci.formats.ome.OMEPyramidStore;
81+
import loci.formats.out.IExtraMetadataWriter;
8182
import loci.formats.out.TiffWriter;
8283
import loci.formats.services.OMEXMLService;
8384
import loci.formats.services.OMEXMLServiceImpl;
@@ -132,6 +133,8 @@ public final class ImageConverter {
132133
private boolean precompressed = false;
133134
private boolean tryPrecompressed = false;
134135

136+
private String extraMetadata = null;
137+
135138
private IFormatReader reader;
136139
private MinMaxCalculator minMax;
137140
private DimensionSwapper dimSwapper;
@@ -267,6 +270,9 @@ else if (args[i].equals("-fill")) {
267270
// allow specifying 0-255
268271
fillColor = (byte) Integer.parseInt(args[++i]);
269272
}
273+
else if (args[i].equals("-extra-metadata")) {
274+
extraMetadata = args[++i];
275+
}
270276
else if (!args[i].equals(CommandLineTools.NO_UPGRADE_CHECK)) {
271277
LOGGER.error("Found unknown command flag: {}; exiting.", args[i]);
272278
return false;
@@ -671,6 +677,15 @@ else if (w instanceof DicomWriter) {
671677
((DicomWriter) w).setBigTiff(bigtiff);
672678
}
673679
}
680+
if (writer instanceof IExtraMetadataWriter) {
681+
((IExtraMetadataWriter) writer).setExtraMetadata(extraMetadata);
682+
}
683+
else if (writer instanceof ImageWriter) {
684+
IFormatWriter w = ((ImageWriter) writer).getWriter(out);
685+
if (w instanceof IExtraMetadataWriter) {
686+
((IExtraMetadataWriter) w).setExtraMetadata(extraMetadata);
687+
}
688+
}
674689

675690
String format = writer.getFormat();
676691
LOGGER.info("[{}] -> {} [{}]",

components/formats-bsd/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@
110110
<version>${jxrlib.version}</version>
111111
</dependency>
112112

113+
<dependency>
114+
<groupId>org.json</groupId>
115+
<artifactId>json</artifactId>
116+
<version>20230227</version>
117+
</dependency>
118+
119+
113120
<!-- xerces is no longer a dependency of metadata-extractor, but it may be required by other BF components -->
114121
<dependency>
115122
<groupId>xerces</groupId>

components/formats-bsd/src/loci/formats/dicom/DicomAttribute.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,8 @@ public enum DicomAttribute {
787787

788788
private static final Map<Integer, String[]> dict =
789789
new HashMap<Integer, String[]>();
790+
private static final Map<String, Integer> nameLookup =
791+
new HashMap<String, Integer>();
790792

791793
static {
792794
try (InputStream stream = DicomAttribute.class.getResourceAsStream("dicom-dictionary.txt")) {
@@ -802,7 +804,9 @@ public enum DicomAttribute {
802804
if (tokens.length > 2) {
803805
tokens[2] = tokens[2].trim();
804806
}
805-
dict.put((int) Long.parseLong(tokens[0], 16), tokens);
807+
int key = (int) Long.parseLong(tokens[0], 16);
808+
dict.put(key, tokens);
809+
nameLookup.put(tokens[1].replaceAll("\\s", "").toLowerCase(), key);
806810
}
807811
}
808812
catch (Exception e) {
@@ -872,6 +876,10 @@ public static String getDescription(int newTag) {
872876
return null;
873877
}
874878

879+
public static Integer getTag(String description) {
880+
return nameLookup.get(description.toLowerCase());
881+
}
882+
875883
/**
876884
* Lookup the attribute for the given tag.
877885
* May return null if the tag is not defined in our dictionary.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* #%L
3+
* BSD implementations of Bio-Formats readers and writers
4+
* %%
5+
* Copyright (C) 2005 - 2023 Open Microscopy Environment:
6+
* - Board of Regents of the University of Wisconsin-Madison
7+
* - Glencoe Software, Inc.
8+
* - University of Dundee
9+
* %%
10+
* Redistribution and use in source and binary forms, with or without
11+
* modification, are permitted provided that the following conditions are met:
12+
*
13+
* 1. Redistributions of source code must retain the above copyright notice,
14+
* this list of conditions and the following disclaimer.
15+
* 2. Redistributions in binary form must reproduce the above copyright notice,
16+
* this list of conditions and the following disclaimer in the documentation
17+
* and/or other materials provided with the distribution.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
23+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
* POSSIBILITY OF SUCH DAMAGE.
30+
* #L%
31+
*/
32+
33+
package loci.formats.dicom;
34+
35+
import java.io.IOException;
36+
import java.util.ArrayList;
37+
import java.util.List;
38+
39+
import loci.common.Constants;
40+
import loci.common.DataTools;
41+
42+
import org.json.JSONArray;
43+
import org.json.JSONException;
44+
import org.json.JSONObject;
45+
46+
import org.slf4j.Logger;
47+
import org.slf4j.LoggerFactory;
48+
49+
/**
50+
* Provide DICOM tags from a file containing JSON.
51+
* Formal JSON schema yet to be determined, but the idea is to accept a hierarchy
52+
* of tags of the form:
53+
*
54+
* {
55+
* "BodyPartExamined": {
56+
* "Value": "BRAIN",
57+
* "VR": "CS",
58+
* "Tag": "(0018,0015)"
59+
* }
60+
* "ContributingEquipmentSequence": {
61+
* "VR": "SQ",
62+
* "Tag": "(0018,a001)",
63+
* "Sequence": {
64+
* "Manufacturer": {
65+
* "Value": "PixelMed",
66+
* "VR": "LO",
67+
* "Tag": "(0008,0070)"
68+
* },
69+
* "ContributionDateTime": {
70+
* "Value": "20210710234601.105+0000",
71+
* "VR": "DT",
72+
* "Tag": "(0018,a002)"
73+
* }
74+
* }
75+
* }
76+
* }
77+
*
78+
* This is similar to JSON examples in https://github.com/QIICR/dcmqi/tree/master/doc/examples,
79+
* but allows for the VR (type) and tag to be explicitly stated, in addition to
80+
* the more human-readable description.
81+
*/
82+
public class DicomJSONProvider implements ITagProvider {
83+
84+
private static final Logger LOGGER =
85+
LoggerFactory.getLogger(DicomJSONProvider.class);
86+
87+
private List<DicomTag> tags = new ArrayList<DicomTag>();
88+
89+
@Override
90+
public void readTagSource(String location) throws IOException {
91+
String rawJSON = DataTools.readFile(location);
92+
try {
93+
JSONObject root = new JSONObject(rawJSON);
94+
95+
for (String tagKey : root.keySet()) {
96+
JSONObject tag = root.getJSONObject(tagKey);
97+
String value = tag.has("Value") ? tag.getString("Value") : null;
98+
99+
Integer intTagCode = lookupTag(tagKey, tag);
100+
DicomVR vrEnum = lookupVR(intTagCode, tag);
101+
102+
DicomTag dicomTag = new DicomTag(intTagCode, vrEnum);
103+
dicomTag.value = value;
104+
LOGGER.debug("Adding tag: {}, VR: {}, value: {}",
105+
DicomAttribute.formatTag(intTagCode), vrEnum, value);
106+
dicomTag.validateValue();
107+
108+
if (vrEnum == DicomVR.SQ && tag.has("Sequence")) {
109+
readSequence(tag, dicomTag);
110+
}
111+
112+
ResolutionStrategy rs = vrEnum == DicomVR.SQ ? ResolutionStrategy.APPEND : ResolutionStrategy.REPLACE;
113+
if (tag.has("ResolutionStrategy")) {
114+
String strategy = tag.getString("ResolutionStrategy");
115+
rs = Enum.valueOf(ResolutionStrategy.class, strategy);
116+
}
117+
dicomTag.strategy = rs;
118+
119+
tags.add(dicomTag);
120+
}
121+
}
122+
catch (JSONException e) {
123+
throw new IOException("Could not parse JSON", e);
124+
}
125+
}
126+
127+
@Override
128+
public List<DicomTag> getTags() {
129+
return tags;
130+
}
131+
132+
private void readSequence(JSONObject rootTag, DicomTag parent) {
133+
JSONObject sequence = rootTag.getJSONObject("Sequence");
134+
for (String key : sequence.keySet()) {
135+
JSONObject tag = sequence.getJSONObject(key);
136+
137+
Integer intTagCode = lookupTag(key, tag);
138+
DicomVR vrEnum = lookupVR(intTagCode, tag);
139+
140+
DicomTag dicomTag = new DicomTag(intTagCode, vrEnum);
141+
142+
if (tag.has("Value")) {
143+
dicomTag.value = tag.get("Value");
144+
}
145+
146+
LOGGER.debug("Adding tag: {}, VR: {}, value: {}", intTagCode, vrEnum, dicomTag.value);
147+
dicomTag.validateValue();
148+
149+
dicomTag.parent = parent;
150+
parent.children.add(dicomTag);
151+
152+
if (vrEnum == DicomVR.SQ && tag.get("Sequence") != null) {
153+
readSequence(tag, dicomTag);
154+
}
155+
}
156+
parent.children.sort(null);
157+
}
158+
159+
/**
160+
* Get the tag code corresponding to the given JSON object.
161+
* If "Tag" is not a defined attribute, lookup the default
162+
* for the object name in the dictionary.
163+
*/
164+
private Integer lookupTag(String tagKey, JSONObject tag) {
165+
Integer intTagCode = null;
166+
167+
if (tag.has("Tag")) {
168+
String[] tagCode = tag.getString("Tag").replaceAll("[()]", "").split(",");
169+
170+
int tagUpper = Integer.parseInt(tagCode[0], 16);
171+
int tagLower = Integer.parseInt(tagCode[1], 16);
172+
173+
intTagCode = tagUpper << 16 | tagLower;
174+
}
175+
else {
176+
intTagCode = DicomAttribute.getTag(tagKey);
177+
178+
if (intTagCode == null) {
179+
throw new IllegalArgumentException(
180+
"Tag not defined and could not be determined from description '" +
181+
tagKey + "'");
182+
}
183+
}
184+
185+
return intTagCode;
186+
}
187+
188+
/**
189+
* Get the VR associated with the given tag code and JSON object.
190+
* If "VR" is not a defined attribute, lookup the default for the
191+
* given tag code in the dictionary.
192+
*/
193+
private DicomVR lookupVR(Integer intTagCode, JSONObject tag) {
194+
DicomVR vrEnum = DicomAttribute.getDefaultVR(intTagCode);
195+
if (tag.has("VR")) {
196+
DicomVR userEnum = DicomVR.valueOf(DicomVR.class, tag.getString("VR"));
197+
if (!vrEnum.equals(userEnum)) {
198+
LOGGER.warn("User-defined VR ({}) for {} does not match expected VR ({})",
199+
userEnum, DicomAttribute.formatTag(intTagCode), vrEnum);
200+
if (userEnum != null) {
201+
vrEnum = userEnum;
202+
}
203+
}
204+
}
205+
206+
return vrEnum;
207+
}
208+
209+
210+
211+
}

0 commit comments

Comments
 (0)