001/* ***** BEGIN LICENSE BLOCK *****
002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
003 *
004 * The contents of this file are subject to the Mozilla Public License Version
005 * 1.1 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 * http://www.mozilla.org/MPL/
008 *
009 * Software distributed under the License is distributed on an "AS IS" basis,
010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
011 * for the specific language governing rights and limitations under the
012 * License.
013 *
014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
015 * Java(TM), hosted at https://github.com/gunterze/dcm4che.
016 *
017 * The Initial Developer of the Original Code is
018 * Agfa Healthcare.
019 * Portions created by the Initial Developer are Copyright (C) 2013
020 * the Initial Developer. All Rights Reserved.
021 *
022 * Contributor(s):
023 * See @authors listed below
024 *
025 * Alternatively, the contents of this file may be used under the terms of
026 * either the GNU General Public License Version 2 or later (the "GPL"), or
027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
028 * in which case the provisions of the GPL or the LGPL are applicable instead
029 * of those above. If you wish to allow use of your version of this file only
030 * under the terms of either the GPL or the LGPL, and not to allow others to
031 * use your version of this file under the terms of the MPL, indicate your
032 * decision by deleting the provisions above and replace them with the notice
033 * and other provisions required by the GPL or the LGPL. If you do not delete
034 * the provisions above, a recipient may use your version of this file under
035 * the terms of any one of the MPL, the GPL or the LGPL.
036 *
037 * ***** END LICENSE BLOCK ***** */
038
039package org.dcm4che3.emf;
040
041import java.util.ArrayList;
042import java.util.HashMap;
043import java.util.Iterator;
044
045import org.dcm4che3.data.Tag;
046import org.dcm4che3.data.UID;
047import org.dcm4che3.data.Attributes;
048import org.dcm4che3.data.BulkData;
049import org.dcm4che3.data.Fragments;
050import org.dcm4che3.data.Sequence;
051import org.dcm4che3.data.VR;
052
053/**
054 * @author Gunter Zeilinger <gunterze@gmail.com>
055 *
056 */
057public class MultiframeExtractor {
058
059    private enum Impl {
060        EnhancedCTImageExtractor(UID.CTImageStorage),
061        EnhancedMRImageExtractor(UID.MRImageStorage) {
062            Attributes extract(MultiframeExtractor mfe, Attributes emf, int frame) {
063                Attributes sf = super.extract(mfe, emf, frame);
064                setEchoTime(sf);
065                setScanningSequence(sf);
066                setSequenceVariant(sf);
067                setScanOptions(sf);
068                return sf;
069            }
070
071            void setEchoTime(Attributes sf) {
072                double echoTime = sf.getDouble(Tag.EffectiveEchoTime, 0);
073                if (echoTime == 0)
074                    sf.setNull(Tag.EchoTime, VR.DS);
075                else
076                    sf.setDouble(Tag.EchoTime, VR.DS, echoTime);
077            }
078
079            void setScanningSequence(Attributes sf) {
080                ArrayList<String> list = new ArrayList<String>(3);
081                
082                String eps = sf.getString(Tag.EchoPulseSequence);
083                if (!"GRADIENT".equals(eps))
084                    list.add("SE");
085                if (!"SPIN".equals(eps))
086                    list.add("GR");
087                if ("YES".equals(sf.getString(Tag.InversionRecovery)))
088                    list.add("IR");
089                if ("YES".equals(sf.getString(Tag.EchoPlanarPulseSequence)))
090                    list.add("EP");
091                sf.setString(Tag.ScanningSequence, VR.CS,
092                        list.toArray(new String[list.size()]));
093            }
094
095            void setSequenceVariant(Attributes sf) {
096                ArrayList<String> list = new ArrayList<String>(5);
097                if (!"SINGLE".equals(sf.getString(Tag.SegmentedKSpaceTraversal)))
098                    list.add("SK");
099                String mf = sf.getString(Tag.MagnetizationTransfer);
100                if (mf != null && !"NONE".equals(mf))
101                    list.add("MTC");
102                String ssps = sf.getString(Tag.SteadyStatePulseSequence);
103                if (ssps != null && !"NONE".equals(ssps))
104                    list.add("TIME_REVERSED".equals(ssps) ? "TRSS" :"SS");
105                String sp = sf.getString(Tag.Spoiling);
106                if (sp != null && !"NONE".equals(sp))
107                    list.add("SP");
108                String op = sf.getString(Tag.OversamplingPhase);
109                if (op != null && !"NONE".equals(op))
110                    list.add("OSP");
111                if (list.isEmpty())
112                    list.add("NONE");
113                sf.setString(Tag.SequenceVariant, VR.CS,
114                        list.toArray(new String[list.size()]));
115            }
116
117            void setScanOptions(Attributes sf) {
118                ArrayList<String> list = new ArrayList<String>(3);
119                String per = sf.getString(Tag.RectilinearPhaseEncodeReordering);
120                if (per != null && !"LINEAR".equals(per))
121                    list.add("PER");
122                String frameType3 = sf.getString(Tag.ImageType, 2);
123                if ("ANGIO".equals(frameType3))
124                    sf.setString(Tag.AngioFlag, VR.CS, "Y");
125                if (frameType3.startsWith("CARD"))
126                    list.add("CG");
127                if (frameType3.endsWith("RESP_GATED"))
128                    list.add("RG");
129                String pfd = sf.getString(Tag.PartialFourierDirection);
130                if ("PHASE".equals(pfd))
131                    list.add("PFP");
132                if ("FREQUENCY".equals(pfd))
133                    list.add("PFF");
134                String sp = sf.getString(Tag.SpatialPresaturation);
135                if (sp != null && !"NONE".equals(sp))
136                    list.add("SP");
137                String sss = sf.getString(Tag.SpectrallySelectedSuppression);
138                if (sss != null && sss.startsWith("FAT"))
139                    list.add("FS");
140                String fc = sf.getString(Tag.FlowCompensation);
141                if (fc != null && !"NONE".equals(fc))
142                    list.add("FC");
143                sf.setString(Tag.ScanOptions, VR.CS,
144                        list.toArray(new String[list.size()]));
145            }
146
147        },
148        EnhancedPETImageExtractor(UID.PositronEmissionTomographyImageStorage);
149
150        private final String sfcuid;
151
152        Impl(String sfcuid) {
153            this.sfcuid = sfcuid;
154        }
155
156        Attributes extract(MultiframeExtractor mfe, Attributes emf, int frame) {
157            return mfe.extract(emf, frame, sfcuid);
158        }
159    }
160
161    private static final HashMap<String,Impl> impls = new HashMap<String,Impl>(8);
162    static {
163        impls.put(UID.EnhancedCTImageStorage, Impl.EnhancedCTImageExtractor);
164        impls.put(UID.EnhancedMRImageStorage, Impl.EnhancedMRImageExtractor);
165        impls.put(UID.EnhancedPETImageStorage, Impl.EnhancedPETImageExtractor);
166    }
167
168    private static final int[] EXCLUDE_TAGS = {
169        Tag.ReferencedImageEvidenceSequence,
170        Tag.SourceImageEvidenceSequence,
171        Tag.DimensionIndexSequence,
172        Tag.NumberOfFrames,
173        Tag.SharedFunctionalGroupsSequence,
174        Tag.PerFrameFunctionalGroupsSequence,
175        Tag.PixelData };
176
177    private boolean preserveSeriesInstanceUID;
178    private String instanceNumberFormat = "%s%04d";
179    private UIDMapper uidMapper = new HashUIDMapper();
180    private NumberOfFramesAccessor nofAccessor = new NumberOfFramesAccessor();
181
182    public static boolean isSupportedSOPClass(String cuid) {
183        return impls.containsKey(cuid);
184    }
185
186    public static String legacySOPClassUID(String mfcuid) {
187        Impl impl = impls.get(mfcuid);
188        return impl != null ? impl.sfcuid : null;
189    }
190
191    public final boolean isPreserveSeriesInstanceUID() {
192        return preserveSeriesInstanceUID;
193    }
194
195    public final void setPreserveSeriesInstanceUID(
196            boolean preserveSeriesInstanceUID) {
197        this.preserveSeriesInstanceUID = preserveSeriesInstanceUID;
198    }
199
200    public final String getInstanceNumberFormat() {
201        return instanceNumberFormat;
202    }
203
204    public final void setInstanceNumberFormat(String instanceNumberFormat) {
205        String.format(instanceNumberFormat, "1", 1);
206        this.instanceNumberFormat = instanceNumberFormat;
207    }
208
209    public final UIDMapper getUIDMapper() {
210        return uidMapper;
211    }
212
213    public final void setUIDMapper(UIDMapper uidMapper) {
214        if (uidMapper == null)
215            throw new NullPointerException();
216        this.uidMapper = uidMapper;
217    }
218
219    public final NumberOfFramesAccessor getNumberOfFramesAccessorr() {
220        return nofAccessor;
221    }
222
223    public final void setNumberOfFramesAccessor(NumberOfFramesAccessor accessor) {
224        if (accessor == null)
225            throw new NullPointerException();
226        this.nofAccessor = accessor;
227    }
228
229    /** Extract specified frame from Enhanced Multi-frame image and return it
230     * as correponding legacy Single-frame image.
231     * 
232     * @param emf Enhanced Multi-frame image
233     * @param frame 0 based frame index
234     * @return legacy Single-frame image
235     */
236    public Attributes extract(Attributes emf, int frame) {
237        return implFor(emf.getString(Tag.SOPClassUID))
238                .extract(this, emf, frame);
239    }
240
241    private static Impl implFor(String mfcuid) {
242        Impl impl = impls.get(mfcuid);
243        if (impl == null)
244            throw new IllegalArgumentException(
245                    "Unsupported SOP Class: " + mfcuid);
246        return impl;
247    }
248
249    private Attributes extract(Attributes emf, int frame, String cuid) {
250        Attributes sfgs = emf.getNestedDataset(Tag.SharedFunctionalGroupsSequence);
251        if (sfgs == null)
252            throw new IllegalArgumentException(
253                    "Missing (5200,9229) Shared Functional Groups Sequence");
254        Attributes fgs = emf.getNestedDataset(Tag.PerFrameFunctionalGroupsSequence, frame);
255        if (fgs == null)
256            throw new IllegalArgumentException(
257                    "Missing (5200,9230) Per-frame Functional Groups Sequence Item for frame #" + (frame + 1));
258        Attributes dest = new Attributes(emf.size() * 2);
259        dest.addNotSelected(emf, EXCLUDE_TAGS);
260        addFunctionGroups(dest, sfgs);
261        addFunctionGroups(dest, fgs);
262        addPixelData(dest, emf, frame);
263        dest.setString(Tag.SOPClassUID, VR.UI, cuid);
264        dest.setString(Tag.SOPInstanceUID, VR.UI, uidMapper.get(
265                dest.getString(Tag.SOPInstanceUID)) + '.' + (frame + 1));
266        dest.setString(Tag.InstanceNumber, VR.IS,
267                createInstanceNumber(dest.getString(Tag.InstanceNumber, ""), frame));
268        dest.setString(Tag.ImageType, VR.CS, dest.getStrings(Tag.FrameType));
269        dest.remove(Tag.FrameType);
270        if (!preserveSeriesInstanceUID)
271            dest.setString(Tag.SeriesInstanceUID, VR.UI, uidMapper.get(
272                    dest.getString(Tag.SeriesInstanceUID)));
273        adjustReferencedImages(dest, Tag.ReferencedImageSequence);
274        adjustReferencedImages(dest, Tag.SourceImageSequence);
275        return dest;
276    }
277
278    private void adjustReferencedImages(Attributes attrs, int sqtag) {
279        Sequence sq = attrs.getSequence(sqtag);
280        if (sq == null)
281            return;
282        
283        ArrayList<Attributes> newRefs = new ArrayList<Attributes>();
284        for (Iterator<Attributes> itr = sq.iterator(); itr.hasNext();) {
285            Attributes ref = (Attributes) itr.next();
286            String cuid = legacySOPClassUID(ref.getString(Tag.ReferencedSOPClassUID));
287            if (cuid == null)
288                continue;
289
290            itr.remove();
291            String iuid = uidMapper.get(ref.getString(Tag.ReferencedSOPInstanceUID));
292            int[] frames = ref.getInts(Tag.ReferencedFrameNumber);
293            int n = frames == null ? nofAccessor.getNumberOfFrames(iuid)
294                                   : frames.length;
295            ref.remove(Tag.ReferencedFrameNumber);
296            ref.setString(Tag.ReferencedSOPClassUID, VR.UI, cuid);
297            for (int i = 0; i < n; i++) {
298                Attributes newRef = new Attributes(ref);
299                newRef.setString(Tag.ReferencedSOPInstanceUID, VR.UI,
300                        iuid + '.' + (frames != null ? frames[i] : (i+1)));
301                newRefs.add(newRef);
302            }
303        }
304        for (Attributes ref : newRefs)
305            sq.add(ref);
306    }
307
308    private void addFunctionGroups(Attributes dest, Attributes fgs) {
309        dest.addSelected(fgs, Tag.ReferencedImageSequence);
310        Attributes fg;
311        for (int sqTag : fgs.tags())
312            if (sqTag != Tag.ReferencedImageSequence
313                    && (fg = fgs.getNestedDataset(sqTag)) != null)
314                dest.addAll(fg);
315    }
316
317    private void addPixelData(Attributes dest, Attributes src, int frame) {
318        VR.Holder vr = new VR.Holder();
319        Object pixelData = src.getValue(Tag.PixelData, vr);
320        if (pixelData instanceof byte[]) {
321            dest.setBytes(Tag.PixelData, vr.vr, extractPixelData(
322                    (byte[]) pixelData, frame, calcFrameLength(src)));
323        } else if (pixelData instanceof BulkData) {
324            dest.setValue(Tag.PixelData, vr.vr, extractPixelData(
325                    (BulkData) pixelData, frame, calcFrameLength(src)));
326        } else {
327            Fragments destFrags = dest.newFragments(Tag.PixelData, vr.vr, 2);
328            destFrags.add(null);
329            destFrags.add(((Fragments) pixelData).get(frame + 1));
330        }
331    }
332
333    private BulkData extractPixelData(BulkData src, int frame,
334            int length) {
335        return new BulkData(src.uriWithoutQuery(),
336                src.offset() + frame * length, length,
337                src.bigEndian);
338    }
339
340    private byte[] extractPixelData(byte[] src, int frame, int length) {
341        byte[] dest = new byte[length];
342        System.arraycopy(src, frame * length, dest, 0, length);
343        return dest;
344    }
345
346    private int calcFrameLength(Attributes src) {
347        return src.getInt(Tag.Rows, 0)
348             * src.getInt(Tag.Columns, 0)
349             * (src.getInt(Tag.BitsAllocated, 8) >> 3)
350             * src.getInt(Tag.NumberOfSamples, 1);
351    }
352
353    private String createInstanceNumber(String mfinstno, int frame) {
354        String s = String.format(instanceNumberFormat, mfinstno, frame + 1);
355        return s.length() > 16 ? s.substring(s.length() - 16) : s;
356    }
357
358}