From de52639d8a1aaf3c3e498f50fbf4d8816e41f1aa Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Fri, 3 Jul 2020 20:26:17 -0700 Subject: [PATCH] [Rewrite] Native Pixel Data Parsing, handle value multiplicity. (#86) This implements native pixel data parsing on the rewrite branch. This includes fixes done on the mainline branch to parse PixelData properly if it appears multiple times in a DICOM (PR #79). This also includes some fixes to better handle value multiplicity. --- cmd/dicomtest/main.go | 47 ++++++-- dataset.go | 20 +++- element.go | 12 ++ mocks/pkg/dicomio/mock_reader.go | 15 +++ parse.go | 6 +- pkg/dicomio/reader.go | 8 ++ read.go | 195 ++++++++++++++++++++++++++----- 7 files changed, 264 insertions(+), 39 deletions(-) diff --git a/cmd/dicomtest/main.go b/cmd/dicomtest/main.go index abd89979..eb14b296 100644 --- a/cmd/dicomtest/main.go +++ b/cmd/dicomtest/main.go @@ -4,9 +4,10 @@ package main import ( "flag" "fmt" - "io/ioutil" + "image/jpeg" "log" "os" + "strconv" "github.com/suyashkumar/dicom" "github.com/suyashkumar/dicom/pkg/tag" @@ -44,21 +45,49 @@ func main() { return } - for _, elem := range ds.Elements { + for z, elem := range ds.Elements { if elem.Tag != tag.PixelData { log.Println(elem.Tag) log.Println(elem.ValueLength) log.Println(elem.Value) - } else { - imageInfo := elem.Value.GetValue().(dicom.PixelDataInfo) - for i, frame := range imageInfo.Frames { - err := ioutil.WriteFile(fmt.Sprintf("image_%d.jpg", i), frame.EncapsulatedData.Data, - 0644) - if err != nil { - log.Println(err) + // TODO: remove image icon hack after implementing flat iterator + if elem.Tag == tag.IconImageSequence { + for _, item := range elem.Value.GetValue().([]*dicom.SequenceItemValue) { + for _, subElem := range item.GetValue().([]*dicom.Element) { + if subElem.Tag == tag.PixelData { + writePixelDataElement(subElem, strconv.Itoa(z)) + } + } } } + } else { + writePixelDataElement(elem, strconv.Itoa(z)) } + + } + } +} + +func writePixelDataElement(e *dicom.Element, id string) { + imageInfo := e.Value.GetValue().(dicom.PixelDataInfo) + for idx, f := range imageInfo.Frames { + i, err := f.GetImage() + if err != nil { + log.Fatal("Error while getting image") } + + name := fmt.Sprintf("image_%d_%s.jpg", idx, id) + f, err := os.Create(name) + if err != nil { + fmt.Printf("Error while creating file: %s", err.Error()) + } + err = jpeg.Encode(f, i, &jpeg.Options{Quality: 100}) + if err != nil { + log.Println(err) + } + if err = f.Close(); err != nil { + log.Println("ERROR: unable to properly close file: ", f.Name()) + } + } } diff --git a/dataset.go b/dataset.go index 4a9c079e..de0d5ce3 100644 --- a/dataset.go +++ b/dataset.go @@ -1,6 +1,24 @@ package dicom +import ( + "errors" + + "github.com/suyashkumar/dicom/pkg/tag" +) + +var ErrorElementNotFound = errors.New("element not found") + type Dataset struct { Elements []*Element - Size uint64 +} + +// FindElementByTag searches through the dataset and returns a pointer to the matching element. +// It DOES NOT search within Sequences as well. +func (d *Dataset) FindElementByTag(tag tag.Tag) (*Element, error) { + for _, e := range d.Elements { + if e.Tag == tag { + return e, nil + } + } + return nil, ErrorElementNotFound } diff --git a/element.go b/element.go index 958fd2ec..aab94e90 100644 --- a/element.go +++ b/element.go @@ -130,3 +130,15 @@ func (e *PixelDataValue) String() string { // TODO: consider adding more sophisticated formatting return "" } + +func MustGetInt(v Value) int { + return v.GetValue().([]int)[0] +} + +func MustGetInts(v Value) []int { + return v.GetValue().([]int) +} + +func MustGetString(v Value) string { + return v.GetValue().([]string)[0] +} diff --git a/mocks/pkg/dicomio/mock_reader.go b/mocks/pkg/dicomio/mock_reader.go index 6040c345..cee62394 100644 --- a/mocks/pkg/dicomio/mock_reader.go +++ b/mocks/pkg/dicomio/mock_reader.go @@ -47,6 +47,21 @@ func (mr *MockReaderMockRecorder) Read(p interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockReader)(nil).Read), p) } +// ReadUInt8 mocks base method +func (m *MockReader) ReadUInt8() (uint8, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadUInt8") + ret0, _ := ret[0].(uint8) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadUInt8 indicates an expected call of ReadUInt8 +func (mr *MockReaderMockRecorder) ReadUInt8() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadUInt8", reflect.TypeOf((*MockReader)(nil).ReadUInt8)) +} + // ReadUInt16 mocks base method func (m *MockReader) ReadUInt16() (uint16, error) { m.ctrl.T.Helper() diff --git a/parse.go b/parse.go index d030d078..bcff289f 100644 --- a/parse.go +++ b/parse.go @@ -72,7 +72,7 @@ func (p *parser) readHeader() ([]*Element, error) { } // Read the length of the metadata elements: (0002,0000) MetaElementGroupLength - maybeMetaLen, err := readElement(p.reader) + maybeMetaLen, err := readElement(p.reader, nil) if err != nil { log.Println("read element err") return nil, err @@ -93,7 +93,7 @@ func (p *parser) readHeader() ([]*Element, error) { } defer p.reader.PopLimit() for !p.reader.IsLimitExhausted() { - elem, err := readElement(p.reader) + elem, err := readElement(p.reader, nil) if err != nil { // TODO: see if we can skip over malformed elements somehow log.Println("read element err") @@ -109,7 +109,7 @@ func (p *parser) readHeader() ([]*Element, error) { func (p *parser) Parse() (Dataset, error) { for !p.reader.IsLimitExhausted() { // TODO: avoid silent looping - elem, err := readElement(p.reader) + elem, err := readElement(p.reader, &p.dataset) if err != nil { // TODO: tolerate some kinds of errors and continue parsing return Dataset{}, err diff --git a/pkg/dicomio/reader.go b/pkg/dicomio/reader.go index db4edd39..af2ce81d 100644 --- a/pkg/dicomio/reader.go +++ b/pkg/dicomio/reader.go @@ -15,6 +15,8 @@ var ( // Reader provides common functionality for reading underlying DICOM data. type Reader interface { io.Reader + // ReadUInt8 reads a uint16 from the underlying reader + ReadUInt8() (uint8, error) // ReadUInt16 reads a uint16 from the underlying reader ReadUInt16() (uint16, error) // ReadUInt32 reads a uint32 from the underlying reader @@ -80,6 +82,12 @@ func (r *reader) Read(p []byte) (int, error) { return n, err } +func (r *reader) ReadUInt8() (uint8, error) { + var out uint8 + err := binary.Read(r, r.bo, &out) + return out, err +} + func (r *reader) ReadUInt16() (uint16, error) { var out uint16 err := binary.Read(r, r.bo, &out) diff --git a/read.go b/read.go index cedbd228..4421651d 100644 --- a/read.go +++ b/read.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "log" + "strconv" "strings" "github.com/suyashkumar/dicom/pkg/dicomio" @@ -76,7 +77,7 @@ func readVL(r dicomio.Reader, isImplicit bool, t tag.Tag, vr string) (uint32, er } } -func readValue(r dicomio.Reader, t tag.Tag, vr string, vl uint32, isImplicit bool) (Value, error) { +func readValue(r dicomio.Reader, t tag.Tag, vr string, vl uint32, isImplicit bool, d *Dataset) (Value, error) { // TODO: implement vrkind := tag.GetVRKind(t, vr) // TODO: if we keep consistent function signature, consider a static map of VR to func? @@ -94,7 +95,7 @@ func readValue(r dicomio.Reader, t tag.Tag, vr string, vl uint32, isImplicit boo case tag.VRItem: return readSequenceItem(r, t, vr, vl) case tag.VRPixelData: - return readPixelData(r, t, vr, vl) + return readPixelData(r, t, vr, vl, d) default: return readString(r, t, vr, vl) } @@ -102,7 +103,7 @@ func readValue(r dicomio.Reader, t tag.Tag, vr string, vl uint32, isImplicit boo return nil, fmt.Errorf("unsure how to parse this VR") } -func readPixelData(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, error) { +func readPixelData(r dicomio.Reader, t tag.Tag, vr string, vl uint32, d *Dataset) (Value, error) { if vl == tag.VLUndefinedLength { var image PixelDataInfo image.IsEncapsulated = true @@ -135,7 +136,111 @@ func readPixelData(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, er } return &PixelDataValue{PixelDataInfo: image}, nil } - return nil, nil + + // Assume we're reading NativeData data since we have a defined value length as per Part 5 Sec A.4 of DICOM spec. + // We need Elements that have been already parsed (rows, cols, etc) to parse frames out of NativeData Pixel data + if d == nil { + return nil, errors.New("the Dataset context cannot be nil in order to read Native PixelData") + } + + i, _, err := readNativeFrames(r, d, nil) + log.Println("Dataset Size: ", len(d.Elements)) + + if err != nil { + return nil, err + } + + // TODO: avoid this copy + return &PixelDataValue{PixelDataInfo: *i}, nil + +} + +// readNativeFrames reads NativeData frames from a Decoder based on already parsed pixel information +// that should be available in parsedData (elements like NumberOfFrames, rows, columns, etc) +func readNativeFrames(d dicomio.Reader, parsedData *Dataset, frameChan chan *frame.Frame) (pixelData *PixelDataInfo, bytesRead int, err error) { + image := PixelDataInfo{ + IsEncapsulated: false, + } + + // Parse information from previously parsed attributes that are needed to parse NativeData Frames: + rows, err := parsedData.FindElementByTag(tag.Rows) + if err != nil { + return nil, 0, err + } + + cols, err := parsedData.FindElementByTag(tag.Columns) + if err != nil { + return nil, 0, err + } + + nof, err := parsedData.FindElementByTag(tag.NumberOfFrames) + nFrames := 0 + if err == nil { + // No error, so parse number of frames + nFrames, err = strconv.Atoi(MustGetString(nof.Value)) // odd that number of frames is encoded as a string... + if err != nil { + return nil, 0, err + } + } else { + // error fetching NumberOfFrames, so default to 1. TODO: revisit + nFrames = 1 + } + + b, err := parsedData.FindElementByTag(tag.BitsAllocated) + if err != nil { + return nil, 0, err + } + bitsAllocated := MustGetInt(b.Value) + + s, err := parsedData.FindElementByTag(tag.SamplesPerPixel) + if err != nil { + return nil, 0, err + } + samplesPerPixel := MustGetInt(s.Value) + + pixelsPerFrame := MustGetInt(rows.Value) * MustGetInt(cols.Value) + + // Parse the pixels: + image.Frames = make([]frame.Frame, nFrames) + for frameIdx := 0; frameIdx < nFrames; frameIdx++ { + // Init current frame + currentFrame := frame.Frame{ + Encapsulated: false, + NativeData: frame.NativeFrame{ + BitsPerSample: bitsAllocated, + Rows: MustGetInt(rows.Value), + Cols: MustGetInt(cols.Value), + Data: make([][]int, int(pixelsPerFrame)), + }, + } + for pixel := 0; pixel < int(pixelsPerFrame); pixel++ { + currentPixel := make([]int, samplesPerPixel) + for value := 0; value < samplesPerPixel; value++ { + if bitsAllocated == 8 { + val, err := d.ReadUInt8() + if err != nil { + return nil, bytesRead, errors.New("") + } + currentPixel[value] = int(val) + } else if bitsAllocated == 16 { + val, err := d.ReadUInt16() + if err != nil { + return nil, bytesRead, errors.New("") + } + currentPixel[value] = int(val) + } + } + currentFrame.NativeData.Data[pixel] = currentPixel + } + image.Frames[frameIdx] = currentFrame + if frameChan != nil { + frameChan <- ¤tFrame // write the current frame to the frameChan + } + } + + bytesRead = (bitsAllocated / 8) * samplesPerPixel * pixelsPerFrame * nFrames + + return &image, bytesRead, nil } // readSequence reads a sequence element (VR = SQ) that contains a subset of Items. Each item contains @@ -146,7 +251,7 @@ func readSequence(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, err if vl == tag.VLUndefinedLength { for { - subElement, err := readElement(r) + subElement, err := readElement(r, nil) if err != nil { // Stop reading due to error log.Println("error reading subitem, ", err) @@ -172,7 +277,7 @@ func readSequence(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, err return nil, err } for !r.IsLimitExhausted() { - subElement, err := readElement(r) + subElement, err := readElement(r, nil) if err != nil { // TODO: option to ignore errors parsing subelements? return nil, err @@ -192,17 +297,25 @@ func readSequence(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, err func readSequenceItem(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, error) { var sequenceItem SequenceItemValue + // seqElements holds items read so far. + // TODO: deduplicate with sequenceItem above + var seqElements Dataset + + log.Println("readSequenceItem TOP LOOP") + if vl == tag.VLUndefinedLength { for { - subElem, err := readElement(r) + subElem, err := readElement(r, &seqElements) if err != nil { return nil, err } if subElem.Tag == tag.ItemDelimitationItem { break } + log.Println("readSequenceItem: tag: ", subElem.Tag) sequenceItem.elements = append(sequenceItem.elements, subElem) + seqElements.Elements = append(seqElements.Elements, subElem) } } else { err := r.PushLimit(int64(vl)) @@ -211,12 +324,14 @@ func readSequenceItem(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, } for !r.IsLimitExhausted() { - subElem, err := readElement(r) + subElem, err := readElement(r, &seqElements) if err != nil { return nil, err } + log.Println("readSequenceItem: tag: ", subElem.Tag) sequenceItem.elements = append(sequenceItem.elements, subElem) + seqElements.Elements = append(seqElements.Elements, subElem) } } @@ -278,31 +393,57 @@ func readDate(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, error) func readInt(r dicomio.Reader, t tag.Tag, vr string, vl uint32) (Value, error) { // TODO: add other integer types here - switch vr { - case "US": - val, err := r.ReadUInt16() - return &IntsValue{value: []int{int(val)}}, err - case "UL": - val, err := r.ReadUInt32() - return &IntsValue{value: []int{int(val)}}, err - case "SL": - val, err := r.ReadInt32() - return &IntsValue{value: []int{int(val)}}, err - case "SS": - val, err := r.ReadInt16() - return &IntsValue{value: []int{int(val)}}, err + err := r.PushLimit(int64(vl)) + retVal := &IntsValue{value: make([]int, 0, vl/2)} + for !r.IsLimitExhausted() { + switch vr { + case "US": + val, err := r.ReadUInt16() + if err != nil { + return nil, err + } + retVal.value = append(retVal.value, int(val)) + break + case "UL": + val, err := r.ReadUInt32() + if err != nil { + return nil, err + } + retVal.value = append(retVal.value, int(val)) + break + case "SL": + val, err := r.ReadInt32() + if err != nil { + return nil, err + } + retVal.value = append(retVal.value, int(val)) + break + case "SS": + val, err := r.ReadInt16() + if err != nil { + return nil, err + } + retVal.value = append(retVal.value, int(val)) + break + default: + return nil, errors.New("unable to parse integer type") + } } - - return nil, errors.New("could not parse integer type correctly") + r.PopLimit() + return retVal, err } // readElement reads the next element. If the next element is a sequence element, -// it may result in a collection of Elements. -func readElement(r dicomio.Reader) (*Element, error) { +// it may result in a collection of Elements. It takes a pointer to the Dataset of +// elements read so far, since previously read elements may be needed to parse +// certain Elements (like native PixelData). If the Dataset is nil, it is +// treated as an empty Dataset. +func readElement(r dicomio.Reader, d *Dataset) (*Element, error) { t, err := readTag(r) if err != nil { return nil, err } + log.Println("readElement: readTag: ", t) vr, err := readVR(r, false, *t) if err != nil { @@ -314,7 +455,9 @@ func readElement(r dicomio.Reader) (*Element, error) { return nil, err } - val, err := readValue(r, *t, vr, vl, false) + log.Println("readElement: vr, vl", vr, vl) + + val, err := readValue(r, *t, vr, vl, false, d) if err != nil { log.Println("error reading value ", err) return nil, err