Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API and DICOM-specific implementations for providing supplemental metadata during conversion #4016

Merged
merged 19 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
07d8014
Initial version of interface and writer API for accepting extra DICOM…
melissalinkert May 30, 2023
280b43d
Add initial implementation of ITagProvider that parses dcdump
melissalinkert May 30, 2023
2c2998e
Add -extra-metadata option for bfconvert
melissalinkert May 31, 2023
85433d3
Add basic JSON provider for DICOM tags
melissalinkert May 31, 2023
c71011b
Fix up some string parsing
melissalinkert Jun 9, 2023
c55f807
Allow tag hierarchies in dcdump provider
melissalinkert Jun 9, 2023
c5b3427
Allow tag hierarchies in DICOM JSON provider
melissalinkert Jun 9, 2023
c03a28d
Remove dcdump metadata provider
melissalinkert Jul 18, 2023
cc3549e
Add configurable tag resolution strategy
melissalinkert Jul 20, 2023
fb647dd
Fix some sorting issues and allow appending to existing sequences
melissalinkert Aug 24, 2023
72831ff
Merge branch 'develop' of github.com:openmicroscopy/bioformats into d…
melissalinkert Aug 24, 2023
52dfc64
Make sure trailing padding has even length
melissalinkert Aug 24, 2023
2159cd4
Attempt to lookup tags by name if not specified in JSON
melissalinkert Aug 28, 2023
afcce2b
Add VR lookup
melissalinkert Aug 31, 2023
7ece5a4
Add some unit tests for supplemental metadata
melissalinkert Aug 31, 2023
c7fd528
Expand unit tests to cover sequences and ResolutionStrategy
melissalinkert Sep 1, 2023
09f65ff
Temporarily comment out all but one test, for build failure investiga…
melissalinkert Sep 11, 2023
5a787f6
Revert "Temporarily comment out all but one test, for build failure i…
melissalinkert Sep 11, 2023
caccc63
Turn off file grouping in test step that reads converted DICOM files
melissalinkert Sep 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
import loci.formats.meta.IPyramidStore;
import loci.formats.out.DicomWriter;
import loci.formats.ome.OMEPyramidStore;
import loci.formats.out.IExtraMetadataWriter;
import loci.formats.out.TiffWriter;
import loci.formats.services.OMEXMLService;
import loci.formats.services.OMEXMLServiceImpl;
Expand Down Expand Up @@ -130,6 +131,8 @@ public final class ImageConverter {
private String swapOrder = null;
private Byte fillColor = null;

private String extraMetadata = null;

private IFormatReader reader;
private MinMaxCalculator minMax;
private DimensionSwapper dimSwapper;
Expand Down Expand Up @@ -264,6 +267,9 @@ else if (args[i].equals("-fill")) {
// allow specifying 0-255
fillColor = (byte) Integer.parseInt(args[++i]);
}
else if (args[i].equals("-extra-metadata")) {
extraMetadata = args[++i];
}
else if (!args[i].equals(CommandLineTools.NO_UPGRADE_CHECK)) {
LOGGER.error("Found unknown command flag: {}; exiting.", args[i]);
return false;
Expand Down Expand Up @@ -651,6 +657,15 @@ else if (w instanceof DicomWriter) {
((DicomWriter) w).setBigTiff(bigtiff);
}
}
if (writer instanceof IExtraMetadataWriter) {
((IExtraMetadataWriter) writer).setExtraMetadata(extraMetadata);
}
else if (writer instanceof ImageWriter) {
IFormatWriter w = ((ImageWriter) writer).getWriter(out);
if (w instanceof IExtraMetadataWriter) {
((IExtraMetadataWriter) w).setExtraMetadata(extraMetadata);
}
}

String format = writer.getFormat();
LOGGER.info("[{}] -> {} [{}]",
Expand Down
7 changes: 7 additions & 0 deletions components/formats-bsd/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@
<version>${jxrlib.version}</version>
</dependency>

<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20230227</version>
</dependency>


<!-- xerces is no longer a dependency of metadata-extractor, but it may be required by other BF components -->
<dependency>
<groupId>xerces</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,8 @@ public enum DicomAttribute {

private static final Map<Integer, String[]> dict =
new HashMap<Integer, String[]>();
private static final Map<String, Integer> nameLookup =
new HashMap<String, Integer>();

static {
try (InputStream stream = DicomAttribute.class.getResourceAsStream("dicom-dictionary.txt")) {
Expand All @@ -802,7 +804,9 @@ public enum DicomAttribute {
if (tokens.length > 2) {
tokens[2] = tokens[2].trim();
}
dict.put((int) Long.parseLong(tokens[0], 16), tokens);
int key = (int) Long.parseLong(tokens[0], 16);
dict.put(key, tokens);
nameLookup.put(tokens[1].replaceAll("\\s", "").toLowerCase(), key);
}
}
catch (Exception e) {
Expand Down Expand Up @@ -872,6 +876,10 @@ public static String getDescription(int newTag) {
return null;
}

public static Integer getTag(String description) {
return nameLookup.get(description.toLowerCase());
}

/**
* Lookup the attribute for the given tag.
* May return null if the tag is not defined in our dictionary.
Expand Down
211 changes: 211 additions & 0 deletions components/formats-bsd/src/loci/formats/dicom/DicomJSONProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* #%L
* BSD implementations of Bio-Formats readers and writers
* %%
* Copyright (C) 2005 - 2023 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/

package loci.formats.dicom;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import loci.common.Constants;
import loci.common.DataTools;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Provide DICOM tags from a file containing JSON.
* Formal JSON schema yet to be determined, but the idea is to accept a hierarchy
* of tags of the form:
*
* {
* "BodyPartExamined": {
* "Value": "BRAIN",
* "VR": "CS",
* "Tag": "(0018,0015)"
* }
* "ContributingEquipmentSequence": {
* "VR": "SQ",
* "Tag": "(0018,a001)",
* "Sequence": {
* "Manufacturer": {
* "Value": "PixelMed",
* "VR": "LO",
* "Tag": "(0008,0070)"
* },
* "ContributionDateTime": {
* "Value": "20210710234601.105+0000",
* "VR": "DT",
* "Tag": "(0018,a002)"
* }
* }
* }
* }
*
* This is similar to JSON examples in https://github.com/QIICR/dcmqi/tree/master/doc/examples,
* but allows for the VR (type) and tag to be explicitly stated, in addition to
* the more human-readable description.
*/
public class DicomJSONProvider implements ITagProvider {

private static final Logger LOGGER =
LoggerFactory.getLogger(DicomJSONProvider.class);

private List<DicomTag> tags = new ArrayList<DicomTag>();

@Override
public void readTagSource(String location) throws IOException {
String rawJSON = DataTools.readFile(location);
try {
JSONObject root = new JSONObject(rawJSON);

for (String tagKey : root.keySet()) {
JSONObject tag = root.getJSONObject(tagKey);
String value = tag.has("Value") ? tag.getString("Value") : null;

Integer intTagCode = lookupTag(tagKey, tag);
DicomVR vrEnum = lookupVR(intTagCode, tag);

DicomTag dicomTag = new DicomTag(intTagCode, vrEnum);
dicomTag.value = value;
LOGGER.debug("Adding tag: {}, VR: {}, value: {}",
DicomAttribute.formatTag(intTagCode), vrEnum, value);
dicomTag.validateValue();

if (vrEnum == DicomVR.SQ && tag.has("Sequence")) {
readSequence(tag, dicomTag);
}

ResolutionStrategy rs = vrEnum == DicomVR.SQ ? ResolutionStrategy.APPEND : ResolutionStrategy.REPLACE;
if (tag.has("ResolutionStrategy")) {
String strategy = tag.getString("ResolutionStrategy");
rs = Enum.valueOf(ResolutionStrategy.class, strategy);
}
dicomTag.strategy = rs;

tags.add(dicomTag);
}
}
catch (JSONException e) {
throw new IOException("Could not parse JSON", e);
}
}

@Override
public List<DicomTag> getTags() {
return tags;
}

private void readSequence(JSONObject rootTag, DicomTag parent) {
JSONObject sequence = rootTag.getJSONObject("Sequence");
for (String key : sequence.keySet()) {
JSONObject tag = sequence.getJSONObject(key);

Integer intTagCode = lookupTag(key, tag);
DicomVR vrEnum = lookupVR(intTagCode, tag);

DicomTag dicomTag = new DicomTag(intTagCode, vrEnum);

if (tag.has("Value")) {
dicomTag.value = tag.get("Value");
}

LOGGER.debug("Adding tag: {}, VR: {}, value: {}", intTagCode, vrEnum, dicomTag.value);
dicomTag.validateValue();

dicomTag.parent = parent;
parent.children.add(dicomTag);

if (vrEnum == DicomVR.SQ && tag.get("Sequence") != null) {
readSequence(tag, dicomTag);
}
}
parent.children.sort(null);
}

/**
* Get the tag code corresponding to the given JSON object.
* If "Tag" is not a defined attribute, lookup the default
* for the object name in the dictionary.
*/
private Integer lookupTag(String tagKey, JSONObject tag) {
Integer intTagCode = null;

if (tag.has("Tag")) {
String[] tagCode = tag.getString("Tag").replaceAll("[()]", "").split(",");

int tagUpper = Integer.parseInt(tagCode[0], 16);
int tagLower = Integer.parseInt(tagCode[1], 16);

intTagCode = tagUpper << 16 | tagLower;
}
else {
intTagCode = DicomAttribute.getTag(tagKey);

if (intTagCode == null) {
throw new IllegalArgumentException(
"Tag not defined and could not be determined from description '" +
tagKey + "'");
}
}

return intTagCode;
}

/**
* Get the VR associated with the given tag code and JSON object.
* If "VR" is not a defined attribute, lookup the default for the
* given tag code in the dictionary.
*/
private DicomVR lookupVR(Integer intTagCode, JSONObject tag) {
DicomVR vrEnum = DicomAttribute.getDefaultVR(intTagCode);
if (tag.has("VR")) {
DicomVR userEnum = DicomVR.valueOf(DicomVR.class, tag.getString("VR"));
if (!vrEnum.equals(userEnum)) {
LOGGER.warn("User-defined VR ({}) for {} does not match expected VR ({})",
userEnum, DicomAttribute.formatTag(intTagCode), vrEnum);
if (userEnum != null) {
vrEnum = userEnum;
}
}
}

return vrEnum;
}



}
Loading
Loading