Skip to content

Instantly share code, notes, and snippets.

@rherrick
Created September 21, 2020 18:00
Show Gist options
  • Save rherrick/e0f19111c02c62e8cb328e75be2e47ca to your computer and use it in GitHub Desktop.
Save rherrick/e0f19111c02c62e8cb328e75be2e47ca to your computer and use it in GitHub Desktop.
A script that pulls in the XNAT Mizer library and DicomEdit 4 and 6 and applies an anonymization script to DICOM files. Requires Groovy installation. Save this file as Anonymizer.groovy, make it executable (e.g. chmod +x Anonymizer.groovy), then run it.
#!/usr/bin/env groovy
@GrabConfig(systemClassLoader = true)
@GrabResolver(name = "NRG Release Repo", root = "https://nrgxnat.jfrog.io/nrgxnat/libs-release")
@GrabResolver(name = "NRG Snapshot Repo", root = "https://nrgxnat.jfrog.io/nrgxnat/libs-snapshot")
@Grapes([
@GrabExclude("org.slf4j:slf4j-simple"),
@GrabExclude("org.slf4j:slf4j-log4j12"),
@Grab("info.picocli:picocli:4.5.1"),
@Grab("ch.qos.logback:logback-classic:1.2.3"),
@Grab("org.nrg.dicom:mizer:1.2.0-SNAPSHOT"),
@Grab("org.nrg.dicom:dicom-edit4:1.0.5-SNAPSHOT"),
@Grab("org.nrg.dicom:dicom-edit6:1.2.0-SNAPSHOT"),
@Grab("commons-io:commons-io:2.7")
])
import ch.qos.logback.classic.Logger
import groovy.cli.picocli.CliBuilder
import groovy.cli.picocli.OptionAccessor
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.dcm4che2.io.DicomCodingException
import org.nrg.dcm.edit.mizer.DE4Mizer
import org.nrg.dicom.dicomedit.mizer.DE6Mizer
import org.nrg.dicom.mizer.exceptions.MizerException
import org.nrg.dicom.mizer.service.MizerContext
import org.nrg.dicom.mizer.service.MizerService
import org.nrg.dicom.mizer.service.impl.BaseMizerService
import org.nrg.dicom.mizer.service.impl.MizerContextWithScript
import org.nrg.transaction.TransactionException
import java.nio.file.Paths
import static ch.qos.logback.classic.Level.WARN
import static org.slf4j.Logger.ROOT_LOGGER_NAME as ROOT
import static org.slf4j.LoggerFactory.getLogger
((Logger) getLogger(ROOT)).setLevel(WARN)
static void anonymize(final MizerService mizer, final MizerContext context, final File incoming, final File outgoing, final boolean verbose) {
final File output = outgoing == null ? new File(incoming.getAbsolutePath() + ".temp") : outgoing
if (verbose) {
println "Anonymizing incoming file ${incoming} into destination ${output}"
}
FileUtils.copyFile(incoming, output)
try {
mizer.anonymize(output, context)
if (outgoing == null) {
FileUtils.deleteQuietly(incoming)
FileUtils.moveFile(output, incoming)
}
} catch (MizerException e) {
if (e.getCause() != null && e.getCause() instanceof TransactionException && e.getCause().getCause() instanceof IOException && e.getCause().getCause().getCause() instanceof DicomCodingException && e.getCause().getCause().getCause().getMessage().endsWith("Not a DICOM Stream")) {
System.err.println("Skipping file ${incoming.name}, not recognized as valid DICOM")
FileUtils.deleteQuietly(output)
} else {
throw e
}
}
}
static File getOutputFile(final File incoming) {
def parent = incoming.getParentFile()
def parentPath = parent ? parent.toPath() : Paths.get("")
parentPath.resolve(FilenameUtils.getBaseName(incoming.getName()) + "-anon.dcm").toFile()
}
final def cli = new CliBuilder(usage: "groovy Anonymizer [option]")
cli.s(longOpt: "script", args: 1, "Anonymization script to be processed", type: String.class, required: true)
cli.f(longOpt: "file", args: 1, "DICOM file to be anonymized", type: String.class, required: false)
cli.d(longOpt: "directory", args: 1, "Directory containing DICOM files to be anonymized", type: String.class, required: false)
cli.t(longOpt: "target", args: 1, "Directory where anonymized DICOM files should be placed (defaults to naming output file(s) <filename>-anon.dcm unless -i or --in-place option is specified)", type: String.class, required: false)
cli.i(longOpt: "in-place", "Indicates that anonymized DICOM files should overwrite the original source DICOM files", required: false)
cli.v(longOpt: "verbose", "Indicates this script should provide more verbose output", required: false)
cli.h(longOpt: "help", "Display usage", required: false)
final OptionAccessor options = cli.parse(args)
if (!options) {
System.exit(255)
}
if (options.h) {
cli.usage()
System.exit(0)
}
if ((!options.f && !options.d) || (options.f && options.d)) {
System.err.println "You must specify one and only one of -f/--file or -d/--directory options"
cli.usage()
System.exit(254)
}
if (options.t && options.i) {
System.err.println "You can't specify both the -t/--target and -i/--in-place options"
cli.usage()
System.exit(253)
}
final File source = Paths.get(options.f ?: options.d).toFile()
if (!source.exists()) {
System.err.println options.f ? "The file indicated by the -f/--file option does not exist: ${source.toFile()}" : "The directory indicated by the -d/--directory option does not exist: ${source.toFile()}"
cli.usage()
System.exit(252)
}
if (source.isDirectory() && options.f) {
System.err.println "The -f/--file option was specified as ${options.f} but that's a directory, not a file"
cli.usage()
System.exit(251)
}
if (source.isFile() && options.d) {
System.err.println "The -d/--directory option was specified as ${options.d} but that's a file, not a directory"
cli.usage()
System.exit(250)
}
final boolean inPlace = options.i
final boolean verbose = options.v
final MizerService mizer = new BaseMizerService(Arrays.asList(new DE4Mizer(), new DE6Mizer()))
def lines = Paths.get(options.s).toFile().readLines()
if (verbose) {
println("Read script ${options.s}, found ${lines.size()} lines")
}
final MizerContext context = new MizerContextWithScript(lines)
final File target = options.t ? Paths.get(options.t).toFile() : null
if (source.isDirectory()) {
println("Processing DICOM in folder ${source}")
source.eachFileRecurse(FileType.FILES) { file ->
anonymize(mizer, context, file, inPlace ? null : (target ? target.toPath().resolve(source.relativePath(file)).toFile() : getOutputFile(file)), verbose)
}
} else {
println("Processing DICOM in file ${source}")
anonymize(mizer, context, source, inPlace ? null : (target ? target : getOutputFile(source)), verbose)
}
return 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment