From 77468ebc9e4bbfdc67e28ef579d1db43da121d61 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Sun, 26 May 2024 23:59:30 -0400 Subject: [PATCH 01/11] Initial hacking to explore multi-value native pixel data--still a lot of cleanup required and things to explore, tests to fix, things to rename, etc --- element.go | 8 ++- element_test.go | 40 +++++------ go.mod | 7 +- go.sum | 4 ++ pkg/frame/encapsulated.go | 4 +- pkg/frame/frame.go | 14 ++-- pkg/frame/native.go | 85 +++++++++++++++++------ pkg/frame/native_test.go | 26 +++---- read.go | 84 +++++++++++++++-------- read_test.go | 140 +++++++++++++++++++------------------- write.go | 12 ++-- write_test.go | 80 +++++++++++----------- 12 files changed, 295 insertions(+), 209 deletions(-) diff --git a/element.go b/element.go index 2682a311..abff0552 100644 --- a/element.go +++ b/element.go @@ -424,8 +424,10 @@ type PixelDataInfo struct { // IntentionallyUnprocessed indicates that the PixelData Value was actually // read (as opposed to skipped over, as in IntentionallySkipped above) and - // blindly placed into RawData (if possible). Writing this element back out - // should work. This will be true if the + // blindly placed into UnprocessedValueData (if possible). Writing this + // element back out using the dicom.Writer API should work. + // + // IntentionallyUnprocessed will be true if the // dicom.SkipProcessingPixelDataValue flag is set with a PixelData tag. IntentionallyUnprocessed bool `json:"intentionallyUnprocessed"` // UnprocessedValueData holds the unprocessed Element value data if @@ -451,7 +453,7 @@ func (p *pixelDataValue) String() string { if p.ParseErr != nil { return fmt.Sprintf("parseErr err=%s FramesLength=%d Frame[0] size=%d", p.ParseErr.Error(), len(p.Frames), len(p.Frames[0].EncapsulatedData.Data)) } - return fmt.Sprintf("FramesLength=%d FrameSize rows=%d cols=%d", len(p.Frames), p.Frames[0].NativeData.Rows, p.Frames[0].NativeData.Cols) + return fmt.Sprintf("FramesLength=%d FrameSize rows=%d cols=%d", len(p.Frames), p.Frames[0].NativeData.Rows(), p.Frames[0].NativeData.Cols()) } func (p *pixelDataValue) MarshalJSON() ([]byte, error) { diff --git a/element_test.go b/element_test.go index 3d825a8c..a57c96cd 100644 --- a/element_test.go +++ b/element_test.go @@ -245,11 +245,11 @@ func TestElement_Equals(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {4}}, }, }, }, @@ -259,11 +259,11 @@ func TestElement_Equals(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {4}}, }, }, }, @@ -277,11 +277,11 @@ func TestElement_Equals(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {6}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {6}}, }, }, }, @@ -291,11 +291,11 @@ func TestElement_Equals(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {4}}, }, }, }, diff --git a/go.mod b/go.mod index cc32e7da..420db23e 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,11 @@ go 1.18 require ( github.com/golang/mock v1.4.4 - github.com/google/go-cmp v0.5.2 + github.com/google/go-cmp v0.6.0 golang.org/x/text v0.3.8 ) -require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect +require ( + golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect +) diff --git a/go.sum b/go.sum index 6cf56017..31f70fe0 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,11 @@ github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/frame/encapsulated.go b/pkg/frame/encapsulated.go index 32517740..177bd787 100644 --- a/pkg/frame/encapsulated.go +++ b/pkg/frame/encapsulated.go @@ -22,13 +22,13 @@ func (e *EncapsulatedFrame) GetEncapsulatedFrame() (*EncapsulatedFrame, error) { // GetNativeFrame returns ErrorFrameTypeNotPresent, because this struct does // not hold a NativeFrame. -func (e *EncapsulatedFrame) GetNativeFrame() (*NativeFrame, error) { +func (e *EncapsulatedFrame) GetNativeFrame() (INativeFrame, error) { return nil, ErrorFrameTypeNotPresent } // GetImage returns a Go image.Image from the underlying frame. func (e *EncapsulatedFrame) GetImage() (image.Image, error) { - // Decoding the data to only re-encode it as a JPEG *without* modifications + // Decoding the Data to only re-encode it as a JPEG *without* modifications // is very inefficient. If all you want to do is write the JPEG to disk, // you should fetch the EncapsulatedFrame and grab the []byte Data from // there. diff --git a/pkg/frame/frame.go b/pkg/frame/frame.go index 5532de1a..9deb58bf 100644 --- a/pkg/frame/frame.go +++ b/pkg/frame/frame.go @@ -16,12 +16,12 @@ var ErrorFrameTypeNotPresent = errors.New("the frame type you requested is not p type CommonFrame interface { // GetImage gets this frame as an image.Image. Beware that the underlying frame may perform // some default rendering and conversions. Operate on the raw NativeFrame or EncapsulatedFrame - // if you need to do some custom rendering work or want the data from the dicom. + // if you need to do some custom rendering work or want the Data from the dicom. GetImage() (image.Image, error) // IsEncapsulated indicates if the underlying Frame is an EncapsulatedFrame. IsEncapsulated() bool // GetNativeFrame attempts to get the underlying NativeFrame (or returns an error) - GetNativeFrame() (*NativeFrame, error) + GetNativeFrame() (INativeFrame, error) // GetEncapsulatedFrame attempts to get the underlying EncapsulatedFrame (or returns an error) GetEncapsulatedFrame() (*EncapsulatedFrame, error) } @@ -33,12 +33,12 @@ type Frame struct { // Encapsulated indicates whether the underlying frame is encapsulated or // not. Encapsulated bool - // EncapsulatedData holds the encapsulated data for this frame if + // EncapsulatedData holds the encapsulated Data for this frame if // Encapsulated is set to true. EncapsulatedData EncapsulatedFrame - // NativeData holds the native data for this frame if Encapsulated is set + // NativeData holds the native Data for this frame if Encapsulated is set // to false. - NativeData NativeFrame + NativeData INativeFrame } // IsEncapsulated indicates if the frame is encapsulated or not. @@ -46,7 +46,7 @@ func (f *Frame) IsEncapsulated() bool { return f.Encapsulated } // GetNativeFrame returns a NativeFrame from this frame. If the underlying frame // is not a NativeFrame, ErrorFrameTypeNotPresent will be returned. -func (f *Frame) GetNativeFrame() (*NativeFrame, error) { +func (f *Frame) GetNativeFrame() (INativeFrame, error) { if f.Encapsulated { return f.EncapsulatedData.GetNativeFrame() } @@ -84,7 +84,7 @@ func (f *Frame) Equals(target *Frame) bool { if f.Encapsulated && !f.EncapsulatedData.Equals(&target.EncapsulatedData) { return false } - if !f.Encapsulated && !f.NativeData.Equals(&target.NativeData) { + if !f.Encapsulated && !f.NativeData.Equals(target.NativeData) { return false } return true diff --git a/pkg/frame/native.go b/pkg/frame/native.go index 0c43ada8..805c52a1 100644 --- a/pkg/frame/native.go +++ b/pkg/frame/native.go @@ -3,57 +3,104 @@ package frame import ( "image" "image/color" + + "golang.org/x/exp/constraints" ) +type INativeFrame interface { + Rows() int + Cols() int + BitsPerSample() int + GetPixel(x, y int) []int + GetPixelAtIdx(idx int) []int + RawDataSlice() any + Equals(frame INativeFrame) bool + CommonFrame +} + // NativeFrame represents a native image frame -type NativeFrame struct { +type NativeFrame[I constraints.Integer] struct { // Data is a slice of pixels, where each pixel can have multiple values - Data [][]int - Rows int - Cols int - BitsPerSample int + Data [][]I + InternalRows int + InternalCols int + InternalBitsPerSample int +} + +func NewNativeFrame[I constraints.Integer](bitsPerSample, rows, cols, pixelsPerFrame int) *NativeFrame[I] { + return &NativeFrame[I]{ + InternalBitsPerSample: bitsPerSample, + InternalRows: rows, + InternalCols: cols, + Data: make([][]I, pixelsPerFrame), + } +} + +func (n *NativeFrame[I]) Rows() int { return n.InternalRows } +func (n *NativeFrame[I]) Cols() int { return n.InternalCols } +func (n *NativeFrame[I]) BitsPerSample() int { return n.InternalBitsPerSample } +func (n *NativeFrame[I]) GetPixelAtIdx(idx int) []int { + rawPixel := n.Data[idx] + vals := make([]int, len(rawPixel)) + for i, val := range rawPixel { + vals[i] = int(val) + } + return vals } +func (n *NativeFrame[I]) GetPixel(x, y int) []int { + dataIdx := x + (y * n.Cols()) + return n.GetPixelAtIdx(dataIdx) +} +func (n *NativeFrame[I]) RawDataSlice() any { return n.Data } // IsEncapsulated indicates if the frame is encapsulated or not. -func (n *NativeFrame) IsEncapsulated() bool { return false } +func (n *NativeFrame[I]) IsEncapsulated() bool { return false } // GetNativeFrame returns a NativeFrame from this frame. If the underlying frame // is not a NativeFrame, ErrorFrameTypeNotPresent will be returned. -func (n *NativeFrame) GetNativeFrame() (*NativeFrame, error) { +func (n *NativeFrame[I]) GetNativeFrame() (INativeFrame, error) { return n, nil } // GetEncapsulatedFrame returns ErrorFrameTypeNotPresent, because this struct -// does not hold encapsulated frame data. -func (n *NativeFrame) GetEncapsulatedFrame() (*EncapsulatedFrame, error) { +// does not hold encapsulated frame Data. +func (n *NativeFrame[I]) GetEncapsulatedFrame() (*EncapsulatedFrame, error) { return nil, ErrorFrameTypeNotPresent } // GetImage returns an image.Image representation the frame, using default // processing. This default processing is basic at the moment, and does not // autoscale pixel values or use window width or level info. -func (n *NativeFrame) GetImage() (image.Image, error) { - i := image.NewGray16(image.Rect(0, 0, n.Cols, n.Rows)) +func (n *NativeFrame[I]) GetImage() (image.Image, error) { + i := image.NewGray16(image.Rect(0, 0, n.Cols(), n.Rows())) for j := 0; j < len(n.Data); j++ { - i.SetGray16(j%n.Cols, j/n.Cols, color.Gray16{Y: uint16(n.Data[j][0])}) // for now, assume we're not overflowing uint16, assume gray image + i.SetGray16(j%n.Cols(), j/n.Cols(), color.Gray16{Y: uint16(n.Data[j][0])}) // for now, assume we're not overflowing uint16, assume gray image } return i, nil } // Equals returns true if this frame equals the provided target frame, otherwise -// false. -func (n *NativeFrame) Equals(target *NativeFrame) bool { +// false. This may be expensive. +func (n *NativeFrame[I]) Equals(target INativeFrame) bool { if target == nil || n == nil { - return n == target + return INativeFrame(n) == target } - if n.Rows != target.Rows || - n.Cols != target.Cols || - n.BitsPerSample != n.BitsPerSample { + if n.Rows() != target.Rows() || + n.Cols() != target.Cols() || + n.BitsPerSample() != n.BitsPerSample() { return false } + + // If BitsPerSample are equal, we assume the target is of type + // *NativeFrame[I] + rawTarget, ok := target.(*NativeFrame[I]) + if !ok { + + } + for pixIdx, pix := range n.Data { for valIdx, val := range pix { - if val != target.Data[pixIdx][valIdx] { + if val != rawTarget.Data[pixIdx][valIdx] { return false } } diff --git a/pkg/frame/native_test.go b/pkg/frame/native_test.go index 3d3ed3ba..95c40c47 100644 --- a/pkg/frame/native_test.go +++ b/pkg/frame/native_test.go @@ -16,33 +16,33 @@ type point struct { func TestNativeFrame_GetImage(t *testing.T) { cases := []struct { Name string - NativeFrame frame.NativeFrame + NativeFrame frame.NativeFrame[int] SetPoints []point }{ { Name: "Square", - NativeFrame: frame.NativeFrame{ - Rows: 2, - Cols: 2, - Data: [][]int{{0}, {0}, {1}, {0}}, + NativeFrame: frame.NativeFrame[int]{ + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{0}, {0}, {1}, {0}}, }, SetPoints: []point{{0, 1}}, }, { Name: "Rectangle", - NativeFrame: frame.NativeFrame{ - Rows: 3, - Cols: 2, - Data: [][]int{{0}, {0}, {0}, {0}, {1}, {0}}, + NativeFrame: frame.NativeFrame[int]{ + InternalRows: 3, + InternalCols: 2, + Data: [][]int{{0}, {0}, {0}, {0}, {1}, {0}}, }, SetPoints: []point{{0, 2}}, }, { Name: "Rectangle - multiple points", - NativeFrame: frame.NativeFrame{ - Rows: 5, - Cols: 3, - Data: [][]int{{0}, {0}, {0}, {0}, {1}, {1}, {0}, {0}, {0}, {0}, {1}, {0}, {0}, {0}, {0}}, + NativeFrame: frame.NativeFrame[int]{ + InternalRows: 5, + InternalCols: 3, + Data: [][]int{{0}, {0}, {0}, {0}, {1}, {1}, {0}, {0}, {0}, {0}, {1}, {0}, {0}, {0}, {0}}, }, SetPoints: []point{{1, 1}, {2, 1}, {1, 3}}, }, diff --git a/read.go b/read.go index 337735f9..b878cf66 100644 --- a/read.go +++ b/read.go @@ -17,6 +17,7 @@ import ( "github.com/suyashkumar/dicom/pkg/dicomio" "github.com/suyashkumar/dicom/pkg/frame" "github.com/suyashkumar/dicom/pkg/tag" + "golang.org/x/exp/constraints" ) var ( @@ -444,42 +445,28 @@ func (r *reader) readNativeFrames(parsedData *Dataset, fc chan<- *frame.Frame, v // Init current frame currentFrame := frame.Frame{ Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: bitsAllocated, - Rows: MustGetInts(rows.Value)[0], - Cols: MustGetInts(cols.Value)[0], - Data: make([][]int, pixelsPerFrame), - }, - } - buf := make([]int, pixelsPerFrame*samplesPerPixel) + } + if bitsAllocated == 1 { + buf := make([]int, pixelsPerFrame*samplesPerPixel) // override buf for now if err := fillBufferSingleBitAllocated(buf, r.rawReader, bo); err != nil { return nil, bytesToRead, err } + nativeFrame := frame.NewNativeFrame[int](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], pixelsPerFrame) for pixel := 0; pixel < pixelsPerFrame; pixel++ { for value := 0; value < samplesPerPixel; value++ { - currentFrame.NativeData.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] + nativeFrame.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] } } + currentFrame.NativeData = nativeFrame } else { - for pixel := 0; pixel < pixelsPerFrame; pixel++ { - for value := 0; value < samplesPerPixel; value++ { - _, err := io.ReadFull(r.rawReader, pixelBuf) - if err != nil { - return nil, bytesToRead, - fmt.Errorf("could not read uint%d from input: %w", bitsAllocated, err) - } - if bitsAllocated == 8 { - buf[(pixel*samplesPerPixel)+value] = int(pixelBuf[0]) - } else if bitsAllocated == 16 { - buf[(pixel*samplesPerPixel)+value] = int(bo.Uint16(pixelBuf)) - } else if bitsAllocated == 32 { - buf[(pixel*samplesPerPixel)+value] = int(bo.Uint32(pixelBuf)) - } else { - return nil, bytesToRead, fmt.Errorf("bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) - } - } - currentFrame.NativeData.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] + switch bitsAllocated { + case 8: + currentFrame, _, err = readNativeFrame[uint8](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], bytesToRead, samplesPerPixel, pixelsPerFrame, pixelBuf, r.rawReader) + case 16: + currentFrame, _, err = readNativeFrame[uint16](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], bytesToRead, samplesPerPixel, pixelsPerFrame, pixelBuf, r.rawReader) + case 32: + currentFrame, _, err = readNativeFrame[uint32](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], bytesToRead, samplesPerPixel, pixelsPerFrame, pixelBuf, r.rawReader) } } image.Frames[frameIdx] = ¤tFrame @@ -497,6 +484,49 @@ func (r *reader) readNativeFrames(parsedData *Dataset, fc chan<- *frame.Frame, v return &image, bytesToRead, nil } +func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRead, samplesPerPixel, pixelsPerFrame int, pixelBuf []byte, rawReader dicomio.Reader) (frame.Frame, int, error) { + // Init current frame + nativeFrame := frame.NewNativeFrame[I](bitsAllocated, rows, cols, pixelsPerFrame) + currentFrame := frame.Frame{ + Encapsulated: false, + NativeData: nativeFrame, + } + buf := make([]I, pixelsPerFrame*samplesPerPixel) + bo := rawReader.ByteOrder() + for pixel := 0; pixel < pixelsPerFrame; pixel++ { + for value := 0; value < samplesPerPixel; value++ { + _, err := io.ReadFull(rawReader, pixelBuf) + if err != nil { + return frame.Frame{}, bytesToRead, + fmt.Errorf("could not read uint%d from input: %w", bitsAllocated, err) + } + if bitsAllocated == 8 { + v, ok := any(pixelBuf[0]).(I) + if !ok { + + } + buf[(pixel*samplesPerPixel)+value] = v + } else if bitsAllocated == 16 { + v, ok := any(bo.Uint16(pixelBuf)).(I) + if !ok { + + } + buf[(pixel*samplesPerPixel)+value] = v + } else if bitsAllocated == 32 { + v, ok := any(bo.Uint32(pixelBuf)).(I) + if !ok { + + } + buf[(pixel*samplesPerPixel)+value] = v + } else { + return frame.Frame{}, bytesToRead, fmt.Errorf("bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) + } + } + nativeFrame.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] + } + return currentFrame, bytesToRead, nil +} + // readSequence reads a sequence element (VR = SQ) that contains a subset of Items. Each item contains // a set of Elements. // See https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.5.2.html#table_7.5-1 diff --git a/read_test.go b/read_test.go index b8fe8002..734957e7 100644 --- a/read_test.go +++ b/read_test.go @@ -232,11 +232,11 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 5, - Cols: 5, - Data: [][]int{{1}, {2}, {3}, {4}, {5}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 16, + InternalRows: 5, + InternalCols: 5, + Data: [][]int{{1}, {2}, {3}, {4}, {5}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}}, }, }, }, @@ -258,29 +258,29 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {2}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {2}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {2}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {2}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {0}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {0}}, }, }, }, @@ -302,20 +302,20 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, }, }, }, @@ -411,30 +411,30 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 3, - Cols: 3, - Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 3, - Cols: 3, - Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 3, - Cols: 3, - Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, }, }, }, @@ -456,29 +456,29 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 1, - Cols: 1, - Data: [][]int{{1, 2, 3}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + Data: [][]int{{1, 2, 3}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 1, - Cols: 1, - Data: [][]int{{1, 2, 3}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + Data: [][]int{{1, 2, 3}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 1, - Cols: 1, - Data: [][]int{{1, 2, 3}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + Data: [][]int{{1, 2, 3}}, }, }, }, @@ -798,11 +798,11 @@ func TestReadNativeFrames_OneBitAllocated(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 1, - Rows: 4, - Cols: 4, - Data: [][]int{{0}, {0}, {0}, {1}, {0}, {1}, {1}, {1}, {1}, {0}, {0}, {1}, {0}, {1}, {1}, {1}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 1, + InternalRows: 4, + InternalCols: 4, + Data: [][]int{{0}, {0}, {0}, {1}, {0}, {1}, {1}, {1}, {1}, {0}, {0}, {1}, {0}, {1}, {1}, {1}}, }, }, }, @@ -825,11 +825,11 @@ func TestReadNativeFrames_OneBitAllocated(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 1, - Rows: 4, - Cols: 4, - Data: [][]int{{0}, {0}, {0}, {1}, {0}, {1}, {1}, {1}, {1}, {0}, {0}, {1}, {0}, {1}, {1}, {1}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 1, + InternalRows: 4, + InternalCols: 4, + Data: [][]int{{0}, {0}, {0}, {1}, {0}, {1}, {1}, {1}, {1}, {0}, {0}, {1}, {0}, {1}, {1}, {1}}, }, }, }, diff --git a/write.go b/write.go index d58aca1e..9c6182b2 100644 --- a/write.go +++ b/write.go @@ -573,19 +573,19 @@ func writePixelData(w dicomio.Writer, t tag.Tag, value Value, vr string, vl uint return nil } numFrames := len(image.Frames) - numPixels := len(image.Frames[0].NativeData.Data) - numValues := len(image.Frames[0].NativeData.Data[0]) + numPixels := image.Frames[0].NativeData.Rows() * image.Frames[0].NativeData.Cols() + numValues := len(image.Frames[0].NativeData.GetPixelAtIdx(0)) // Total required buffer length in bytes: - length := numFrames * numPixels * numValues * image.Frames[0].NativeData.BitsPerSample / 8 + length := numFrames * numPixels * numValues * image.Frames[0].NativeData.BitsPerSample() / 8 buf := &bytes.Buffer{} buf.Grow(length) bo, _ := w.GetTransferSyntax() for frame := 0; frame < numFrames; frame++ { for pixel := 0; pixel < numPixels; pixel++ { - for value := 0; value < numValues; value++ { - pixelValue := image.Frames[frame].NativeData.Data[pixel][value] - switch image.Frames[frame].NativeData.BitsPerSample { + pixelSlice := image.Frames[frame].NativeData.GetPixelAtIdx(pixel) + for _, pixelValue := range pixelSlice { + switch image.Frames[frame].NativeData.BitsPerSample() { case 8: if err := binary.Write(buf, bo, uint8(pixelValue)); err != nil { return err diff --git a/write_test.go b/write_test.go index 2c134c8a..49c31978 100644 --- a/write_test.go +++ b/write_test.go @@ -289,11 +289,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {4}}, }, }, }, @@ -319,11 +319,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {4}}, }, }, }, @@ -347,11 +347,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 32, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1}, {2}, {3}, {4}}, }, }, }, @@ -375,20 +375,20 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 32, - Rows: 2, - Cols: 2, - Data: [][]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 32, - Rows: 2, - Cols: 2, - Data: [][]int{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, }, }, }, @@ -458,20 +458,20 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 32, - Rows: 2, - Cols: 2, - Data: [][]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 32, - Rows: 2, - Cols: 2, - Data: [][]int{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + Data: [][]int{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, }, }, }, @@ -495,11 +495,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 1, - Cols: 3, - Data: [][]int{{1}, {2}, {3}}, + NativeData: &frame.NativeFrame[int]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 3, + Data: [][]int{{1}, {2}, {3}}, }, }, }, From a69d754e273e2cd42185a2e8f5339f3b71113dcd Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Mon, 27 May 2024 00:13:31 -0400 Subject: [PATCH 02/11] passing tests (though more needed) --- pkg/frame/native_test.go | 4 ++-- read.go | 5 +++++ read_test.go | 48 ++++++++++++++++++++-------------------- write_test.go | 32 +++++++++++++-------------- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/pkg/frame/native_test.go b/pkg/frame/native_test.go index 95c40c47..b4520464 100644 --- a/pkg/frame/native_test.go +++ b/pkg/frame/native_test.go @@ -61,8 +61,8 @@ func TestNativeFrame_GetImage(t *testing.T) { // Check that all pixels are zero except at the // (ExpectedSetPointX, ExpectedSetPointY) point. - for x := 0; x < tc.NativeFrame.Cols; x++ { - for y := 0; y < tc.NativeFrame.Rows; y++ { + for x := 0; x < tc.NativeFrame.Cols(); x++ { + for y := 0; y < tc.NativeFrame.Rows(); y++ { currValue := imgGray.Gray16At(x, y).Y if within(point{x, y}, tc.SetPoints) { if currValue != 1 { diff --git a/read.go b/read.go index b878cf66..9448bb57 100644 --- a/read.go +++ b/read.go @@ -467,8 +467,13 @@ func (r *reader) readNativeFrames(parsedData *Dataset, fc chan<- *frame.Frame, v currentFrame, _, err = readNativeFrame[uint16](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], bytesToRead, samplesPerPixel, pixelsPerFrame, pixelBuf, r.rawReader) case 32: currentFrame, _, err = readNativeFrame[uint32](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], bytesToRead, samplesPerPixel, pixelsPerFrame, pixelBuf, r.rawReader) + default: + return nil, bytesToRead, fmt.Errorf("unsupported bitsAllocated, got: %v, %w", bitsAllocated, ErrorUnsupportedBitsAllocated) } } + if err != nil { + return nil, bytesToRead, err + } image.Frames[frameIdx] = ¤tFrame if fc != nil { fc <- ¤tFrame // write the current frame to the frame channel diff --git a/read_test.go b/read_test.go index 734957e7..bb918908 100644 --- a/read_test.go +++ b/read_test.go @@ -232,11 +232,11 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint16]{ InternalBitsPerSample: 16, InternalRows: 5, InternalCols: 5, - Data: [][]int{{1}, {2}, {3}, {4}, {5}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}}, + Data: [][]uint16{{1}, {2}, {3}, {4}, {5}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}}, }, }, }, @@ -258,29 +258,29 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint16]{ InternalBitsPerSample: 16, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {2}}, + Data: [][]uint16{{1}, {2}, {3}, {2}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint16]{ InternalBitsPerSample: 16, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {2}}, + Data: [][]uint16{{1}, {2}, {3}, {2}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint16]{ InternalBitsPerSample: 16, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {0}}, + Data: [][]uint16{{1}, {2}, {3}, {0}}, }, }, }, @@ -302,20 +302,20 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint16]{ InternalBitsPerSample: 16, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, + Data: [][]uint16{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint16]{ InternalBitsPerSample: 16, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, + Data: [][]uint16{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, }, }, }, @@ -411,30 +411,30 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 3, InternalCols: 3, - Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + Data: [][]uint8{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 3, InternalCols: 3, - Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + Data: [][]uint8{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 3, InternalCols: 3, - Data: [][]int{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + Data: [][]uint8{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, }, }, }, @@ -456,29 +456,29 @@ func TestReadNativeFrames(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 1, InternalCols: 1, - Data: [][]int{{1, 2, 3}}, + Data: [][]uint8{{1, 2, 3}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 1, InternalCols: 1, - Data: [][]int{{1, 2, 3}}, + Data: [][]uint8{{1, 2, 3}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 1, InternalCols: 1, - Data: [][]int{{1, 2, 3}}, + Data: [][]uint8{{1, 2, 3}}, }, }, }, diff --git a/write_test.go b/write_test.go index 49c31978..5f260e50 100644 --- a/write_test.go +++ b/write_test.go @@ -289,11 +289,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + Data: [][]uint8{{1}, {2}, {3}, {4}}, }, }, }, @@ -319,11 +319,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint16]{ InternalBitsPerSample: 16, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + Data: [][]uint16{{1}, {2}, {3}, {4}}, }, }, }, @@ -347,11 +347,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint32]{ InternalBitsPerSample: 32, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + Data: [][]uint32{{1}, {2}, {3}, {4}}, }, }, }, @@ -375,20 +375,20 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint32]{ InternalBitsPerSample: 32, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + Data: [][]uint32{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint32]{ InternalBitsPerSample: 32, InternalRows: 2, InternalCols: 2, - Data: [][]int{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, + Data: [][]uint32{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, }, }, }, @@ -458,20 +458,20 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint32]{ InternalBitsPerSample: 32, InternalRows: 2, InternalCols: 2, - Data: [][]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + Data: [][]uint32{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, }, }, { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint32]{ InternalBitsPerSample: 32, InternalRows: 2, InternalCols: 2, - Data: [][]int{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, + Data: [][]uint32{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, }, }, }, @@ -495,11 +495,11 @@ func TestWrite(t *testing.T) { Frames: []*frame.Frame{ { Encapsulated: false, - NativeData: &frame.NativeFrame[int]{ + NativeData: &frame.NativeFrame[uint8]{ InternalBitsPerSample: 8, InternalRows: 1, InternalCols: 3, - Data: [][]int{{1}, {2}, {3}}, + Data: [][]uint8{{1}, {2}, {3}}, }, }, }, From 01880ceb4298140eefc66c7cce8502e1fa3fb913 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Mon, 27 May 2024 00:14:22 -0400 Subject: [PATCH 03/11] go mod tidy --- go.mod | 7 +------ go.sum | 12 ------------ 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 420db23e..235a8131 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,7 @@ module github.com/suyashkumar/dicom go 1.18 require ( - github.com/golang/mock v1.4.4 github.com/google/go-cmp v0.6.0 + golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d golang.org/x/text v0.3.8 ) - -require ( - golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect -) diff --git a/go.sum b/go.sum index 31f70fe0..6ac7ec39 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,6 @@ -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 7e8d5f77771f2e71a5df62247bc4344e0c249eef Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Sat, 1 Jun 2024 19:13:16 -0400 Subject: [PATCH 04/11] try populating slice inline --- read.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/read.go b/read.go index 9448bb57..91cfcea3 100644 --- a/read.go +++ b/read.go @@ -496,9 +496,10 @@ func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRe Encapsulated: false, NativeData: nativeFrame, } - buf := make([]I, pixelsPerFrame*samplesPerPixel) + // buf := make([]I, pixelsPerFrame*samplesPerPixel) bo := rawReader.ByteOrder() for pixel := 0; pixel < pixelsPerFrame; pixel++ { + nativeFrame.Data[pixel] = make([]I, samplesPerPixel) for value := 0; value < samplesPerPixel; value++ { _, err := io.ReadFull(rawReader, pixelBuf) if err != nil { @@ -510,24 +511,24 @@ func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRe if !ok { } - buf[(pixel*samplesPerPixel)+value] = v + nativeFrame.Data[pixel][value] = v } else if bitsAllocated == 16 { v, ok := any(bo.Uint16(pixelBuf)).(I) if !ok { } - buf[(pixel*samplesPerPixel)+value] = v + nativeFrame.Data[pixel][value] = v } else if bitsAllocated == 32 { v, ok := any(bo.Uint32(pixelBuf)).(I) if !ok { } - buf[(pixel*samplesPerPixel)+value] = v + nativeFrame.Data[pixel][value] = v } else { return frame.Frame{}, bytesToRead, fmt.Errorf("bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) } } - nativeFrame.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] + // nativeFrame.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] } return currentFrame, bytesToRead, nil } From eb8fb2bd365d0f54c6c578c506d18ab105d04d37 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Wed, 7 Aug 2024 22:10:23 -0400 Subject: [PATCH 05/11] Store Native pixel data in flat slice --- element_test.go | 36 ++++++----- pkg/frame/native.go | 79 +++++++++++++++-------- pkg/frame/native_test.go | 47 +++++++++++--- pkg/tag/tag_definitions.go | 2 +- read.go | 15 +++-- read_test.go | 126 ++++++++++++++++++++----------------- write.go | 22 +++++-- write_test.go | 74 ++++++++++++---------- 8 files changed, 248 insertions(+), 153 deletions(-) diff --git a/element_test.go b/element_test.go index a57c96cd..1280db7c 100644 --- a/element_test.go +++ b/element_test.go @@ -246,10 +246,11 @@ func TestElement_Equals(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[int]{ - InternalBitsPerSample: 8, - InternalRows: 2, - InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 4}, }, }, }, @@ -260,10 +261,11 @@ func TestElement_Equals(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[int]{ - InternalBitsPerSample: 8, - InternalRows: 2, - InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 4}, }, }, }, @@ -278,10 +280,11 @@ func TestElement_Equals(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[int]{ - InternalBitsPerSample: 8, - InternalRows: 2, - InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {6}}, + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 6}, }, }, }, @@ -292,10 +295,11 @@ func TestElement_Equals(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[int]{ - InternalBitsPerSample: 8, - InternalRows: 2, - InternalCols: 2, - Data: [][]int{{1}, {2}, {3}, {4}}, + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 4}, }, }, }, diff --git a/pkg/frame/native.go b/pkg/frame/native.go index 805c52a1..f882fc21 100644 --- a/pkg/frame/native.go +++ b/pkg/frame/native.go @@ -1,15 +1,20 @@ package frame import ( + "errors" + "fmt" "image" "image/color" "golang.org/x/exp/constraints" ) +var UnsupportedSamplesPerPixel = errors.New("unsupported samples per pixel") + type INativeFrame interface { Rows() int Cols() int + SamplesPerPixel() int BitsPerSample() int GetPixel(x, y int) []int GetPixelAtIdx(idx int) []int @@ -20,38 +25,59 @@ type INativeFrame interface { // NativeFrame represents a native image frame type NativeFrame[I constraints.Integer] struct { - // Data is a slice of pixels, where each pixel can have multiple values - Data [][]I - InternalRows int - InternalCols int - InternalBitsPerSample int + // RawData is a slice of pixel values. For each pixel, each sample for the + // pixel is unrolled per pixel. For example, consider 2 pixels that have 3 + // samples per Pixel: [[1,2,3], [4,5,6]]. This would be unrolled like + // [1,2,3,4,5,6]. The pixels themselves are arranged in row order, so the + // first row of pixels would be unrolled in order, followed by the next row, + // and so on in this flattened array. + // A flattened slice is used instead of a nested 2D slice because there is + // significant overhead to creating nested slices in Go discussed here: + // https://github.com/suyashkumar/dicom/issues/161#issuecomment-2143627792. + RawData []I + InternalSamplesPerPixel int + InternalRows int + InternalCols int + InternalBitsPerSample int } -func NewNativeFrame[I constraints.Integer](bitsPerSample, rows, cols, pixelsPerFrame int) *NativeFrame[I] { +func NewNativeFrame[I constraints.Integer](bitsPerSample, rows, cols, pixelsPerFrame, samplesPerPixel int) *NativeFrame[I] { return &NativeFrame[I]{ - InternalBitsPerSample: bitsPerSample, - InternalRows: rows, - InternalCols: cols, - Data: make([][]I, pixelsPerFrame), + InternalBitsPerSample: bitsPerSample, + InternalRows: rows, + InternalCols: cols, + RawData: make([]I, pixelsPerFrame*samplesPerPixel), + InternalSamplesPerPixel: samplesPerPixel, } } func (n *NativeFrame[I]) Rows() int { return n.InternalRows } func (n *NativeFrame[I]) Cols() int { return n.InternalCols } func (n *NativeFrame[I]) BitsPerSample() int { return n.InternalBitsPerSample } + +func (n *NativeFrame[I]) SamplesPerPixel() int { return n.InternalSamplesPerPixel } func (n *NativeFrame[I]) GetPixelAtIdx(idx int) []int { - rawPixel := n.Data[idx] - vals := make([]int, len(rawPixel)) - for i, val := range rawPixel { - vals[i] = int(val) + vals := make([]int, n.InternalSamplesPerPixel) + for i := 0; i < n.InternalSamplesPerPixel; i++ { + vals[i] = int(n.RawData[idx+i]) } return vals } func (n *NativeFrame[I]) GetPixel(x, y int) []int { - dataIdx := x + (y * n.Cols()) - return n.GetPixelAtIdx(dataIdx) + pixelIdx := (x * n.InternalSamplesPerPixel) + (y * (n.Cols() * n.InternalSamplesPerPixel)) + vals := make([]int, n.InternalSamplesPerPixel) + for i := 0; i < n.InternalSamplesPerPixel; i++ { + vals[i] = int(n.RawData[pixelIdx+i]) + } + return vals +} + +func (n *NativeFrame[I]) GetSample(x, y, sampleIdx int) int { + dataSampleIdx := (x * n.InternalSamplesPerPixel) + (y * (n.Cols() * n.InternalSamplesPerPixel)) + sampleIdx + return int(n.RawData[dataSampleIdx]) } -func (n *NativeFrame[I]) RawDataSlice() any { return n.Data } + +func (n *NativeFrame[I]) RawDataSlice() any { return n.RawData } // IsEncapsulated indicates if the frame is encapsulated or not. func (n *NativeFrame[I]) IsEncapsulated() bool { return false } @@ -72,9 +98,12 @@ func (n *NativeFrame[I]) GetEncapsulatedFrame() (*EncapsulatedFrame, error) { // processing. This default processing is basic at the moment, and does not // autoscale pixel values or use window width or level info. func (n *NativeFrame[I]) GetImage() (image.Image, error) { + if n.InternalSamplesPerPixel != 1 { + return nil, fmt.Errorf("GetImage(): unexpected InternalSamplesPerPixel got %v, expected 1 since only grayscale images are supported for now %w", n.InternalSamplesPerPixel, UnsupportedSamplesPerPixel) + } i := image.NewGray16(image.Rect(0, 0, n.Cols(), n.Rows())) - for j := 0; j < len(n.Data); j++ { - i.SetGray16(j%n.Cols(), j/n.Cols(), color.Gray16{Y: uint16(n.Data[j][0])}) // for now, assume we're not overflowing uint16, assume gray image + for idx := 0; idx < len(n.RawData); idx++ { + i.SetGray16(idx%n.Cols(), idx/n.Cols(), color.Gray16{Y: uint16(n.RawData[idx])}) // for now, assume we're not overflowing uint16, assume gray image, we can check BitsAllocated if we want to be conservative. } return i, nil } @@ -95,15 +124,15 @@ func (n *NativeFrame[I]) Equals(target INativeFrame) bool { // *NativeFrame[I] rawTarget, ok := target.(*NativeFrame[I]) if !ok { - + return false // in reality, this is kind of an error, unless folks are implementing this interface themselves. } - for pixIdx, pix := range n.Data { - for valIdx, val := range pix { - if val != rawTarget.Data[pixIdx][valIdx] { - return false - } + // TODO: check this using the interface only, which might be more expensive. + for sampleIdx, sample := range n.RawData { + if sample != rawTarget.RawData[sampleIdx] { + return false } } + return true } diff --git a/pkg/frame/native_test.go b/pkg/frame/native_test.go index b4520464..2d11eaa7 100644 --- a/pkg/frame/native_test.go +++ b/pkg/frame/native_test.go @@ -1,6 +1,7 @@ package frame_test import ( + "errors" "image" "testing" @@ -22,27 +23,30 @@ func TestNativeFrame_GetImage(t *testing.T) { { Name: "Square", NativeFrame: frame.NativeFrame[int]{ - InternalRows: 2, - InternalCols: 2, - Data: [][]int{{0}, {0}, {1}, {0}}, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []int{0, 0, 1, 0}, }, SetPoints: []point{{0, 1}}, }, { Name: "Rectangle", NativeFrame: frame.NativeFrame[int]{ - InternalRows: 3, - InternalCols: 2, - Data: [][]int{{0}, {0}, {0}, {0}, {1}, {0}}, + InternalRows: 3, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []int{0, 0, 0, 0, 1, 0}, }, SetPoints: []point{{0, 2}}, }, { Name: "Rectangle - multiple points", NativeFrame: frame.NativeFrame[int]{ - InternalRows: 5, - InternalCols: 3, - Data: [][]int{{0}, {0}, {0}, {0}, {1}, {1}, {0}, {0}, {0}, {0}, {1}, {0}, {0}, {0}, {0}}, + InternalRows: 5, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []int{0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0}, }, SetPoints: []point{{1, 1}, {2, 1}, {1, 3}}, }, @@ -82,6 +86,31 @@ func TestNativeFrame_GetImage(t *testing.T) { } } +func TestNativeFrame_GetImage_Errors(t *testing.T) { + cases := []struct { + name string + nativeFrame frame.NativeFrame[int] + wantErr error + }{ + { + name: "InternalSamplesPerPixel is not 1", + nativeFrame: frame.NativeFrame[int]{ + InternalSamplesPerPixel: 2, + }, + wantErr: frame.UnsupportedSamplesPerPixel, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.nativeFrame.GetImage() + if !errors.Is(err, tc.wantErr) { + t.Errorf("GetImage unexpected error. got: %v, want: %v", err, tc.wantErr) + } + }) + } +} + // within returns true if pt is in the []point func within(pt point, set []point) bool { for _, item := range set { diff --git a/pkg/tag/tag_definitions.go b/pkg/tag/tag_definitions.go index ee29ecec..a50e0990 100644 --- a/pkg/tag/tag_definitions.go +++ b/pkg/tag/tag_definitions.go @@ -4727,7 +4727,7 @@ func maybeInitTagDict() { tagDict[Tag{0x0024, 0x0338}] = Info{Tag{0x0024, 0x0338}, "CS", "IndexNormalsFlag", "1"} tagDict[Tag{0x0024, 0x0341}] = Info{Tag{0x0024, 0x0341}, "FL", "IndexProbability", "1"} tagDict[Tag{0x0024, 0x0344}] = Info{Tag{0x0024, 0x0344}, "SQ", "IndexProbabilitySequence", "1"} - tagDict[Tag{0x0028, 0x0002}] = Info{Tag{0x0028, 0x0002}, "US", "SamplesPerPixel", "1"} + tagDict[Tag{0x0028, 0x0002}] = Info{Tag{0x0028, 0x0002}, "US", "InternalSamplesPerPixel", "1"} tagDict[Tag{0x0028, 0x0003}] = Info{Tag{0x0028, 0x0003}, "US", "SamplesPerPixelUsed", "1"} tagDict[Tag{0x0028, 0x0004}] = Info{Tag{0x0028, 0x0004}, "CS", "PhotometricInterpretation", "1"} tagDict[Tag{0x0028, 0x0006}] = Info{Tag{0x0028, 0x0006}, "US", "PlanarConfiguration", "1"} diff --git a/read.go b/read.go index 91cfcea3..4b820c2f 100644 --- a/read.go +++ b/read.go @@ -452,10 +452,10 @@ func (r *reader) readNativeFrames(parsedData *Dataset, fc chan<- *frame.Frame, v if err := fillBufferSingleBitAllocated(buf, r.rawReader, bo); err != nil { return nil, bytesToRead, err } - nativeFrame := frame.NewNativeFrame[int](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], pixelsPerFrame) + nativeFrame := frame.NewNativeFrame[int](bitsAllocated, MustGetInts(rows.Value)[0], MustGetInts(cols.Value)[0], pixelsPerFrame, samplesPerPixel) for pixel := 0; pixel < pixelsPerFrame; pixel++ { for value := 0; value < samplesPerPixel; value++ { - nativeFrame.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] + nativeFrame.RawData[(pixel*samplesPerPixel)+value] = buf[pixel*samplesPerPixel+value] } } currentFrame.NativeData = nativeFrame @@ -491,15 +491,14 @@ func (r *reader) readNativeFrames(parsedData *Dataset, fc chan<- *frame.Frame, v func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRead, samplesPerPixel, pixelsPerFrame int, pixelBuf []byte, rawReader dicomio.Reader) (frame.Frame, int, error) { // Init current frame - nativeFrame := frame.NewNativeFrame[I](bitsAllocated, rows, cols, pixelsPerFrame) + nativeFrame := frame.NewNativeFrame[I](bitsAllocated, rows, cols, pixelsPerFrame, samplesPerPixel) currentFrame := frame.Frame{ Encapsulated: false, NativeData: nativeFrame, } - // buf := make([]I, pixelsPerFrame*samplesPerPixel) + bo := rawReader.ByteOrder() for pixel := 0; pixel < pixelsPerFrame; pixel++ { - nativeFrame.Data[pixel] = make([]I, samplesPerPixel) for value := 0; value < samplesPerPixel; value++ { _, err := io.ReadFull(rawReader, pixelBuf) if err != nil { @@ -511,19 +510,19 @@ func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRe if !ok { } - nativeFrame.Data[pixel][value] = v + nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v } else if bitsAllocated == 16 { v, ok := any(bo.Uint16(pixelBuf)).(I) if !ok { } - nativeFrame.Data[pixel][value] = v + nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v } else if bitsAllocated == 32 { v, ok := any(bo.Uint32(pixelBuf)).(I) if !ok { } - nativeFrame.Data[pixel][value] = v + nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v } else { return frame.Frame{}, bytesToRead, fmt.Errorf("bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) } diff --git a/read_test.go b/read_test.go index bb918908..1ff5d062 100644 --- a/read_test.go +++ b/read_test.go @@ -233,10 +233,11 @@ func TestReadNativeFrames(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint16]{ - InternalBitsPerSample: 16, - InternalRows: 5, - InternalCols: 5, - Data: [][]uint16{{1}, {2}, {3}, {4}, {5}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}}, + InternalBitsPerSample: 16, + InternalRows: 5, + InternalCols: 5, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }, }, }, @@ -259,28 +260,31 @@ func TestReadNativeFrames(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint16]{ - InternalBitsPerSample: 16, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint16{{1}, {2}, {3}, {2}}, + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 2}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint16]{ - InternalBitsPerSample: 16, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint16{{1}, {2}, {3}, {2}}, + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 2}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint16]{ - InternalBitsPerSample: 16, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint16{{1}, {2}, {3}, {0}}, + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 0}, }, }, }, @@ -303,19 +307,21 @@ func TestReadNativeFrames(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint16]{ - InternalBitsPerSample: 16, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint16{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint16{1, 2, 3, 2, 1, 2, 3, 2}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint16]{ - InternalBitsPerSample: 16, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint16{{1, 2}, {3, 2}, {1, 2}, {3, 5}}, + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint16{1, 2, 3, 2, 1, 2, 3, 5}, }, }, }, @@ -412,29 +418,32 @@ func TestReadNativeFrames(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 3, - InternalCols: 3, - Data: [][]uint8{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{11, 12, 13, 21, 22, 23, 31, 32, 33}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 3, - InternalCols: 3, - Data: [][]uint8{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{11, 12, 13, 21, 22, 23, 31, 32, 33}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 3, - InternalCols: 3, - Data: [][]uint8{{11}, {12}, {13}, {21}, {22}, {23}, {31}, {32}, {33}}, + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{11, 12, 13, 21, 22, 23, 31, 32, 33}, }, }, }, @@ -457,28 +466,31 @@ func TestReadNativeFrames(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 1, - InternalCols: 1, - Data: [][]uint8{{1, 2, 3}}, + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + InternalSamplesPerPixel: 3, + RawData: []uint8{1, 2, 3}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 1, - InternalCols: 1, - Data: [][]uint8{{1, 2, 3}}, + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + InternalSamplesPerPixel: 3, + RawData: []uint8{1, 2, 3}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 1, - InternalCols: 1, - Data: [][]uint8{{1, 2, 3}}, + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + InternalSamplesPerPixel: 3, + RawData: []uint8{1, 2, 3}, }, }, }, @@ -799,10 +811,11 @@ func TestReadNativeFrames_OneBitAllocated(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[int]{ - InternalBitsPerSample: 1, - InternalRows: 4, - InternalCols: 4, - Data: [][]int{{0}, {0}, {0}, {1}, {0}, {1}, {1}, {1}, {1}, {0}, {0}, {1}, {0}, {1}, {1}, {1}}, + InternalBitsPerSample: 1, + InternalRows: 4, + InternalCols: 4, + InternalSamplesPerPixel: 1, + RawData: []int{0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1}, }, }, }, @@ -826,10 +839,11 @@ func TestReadNativeFrames_OneBitAllocated(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[int]{ - InternalBitsPerSample: 1, - InternalRows: 4, - InternalCols: 4, - Data: [][]int{{0}, {0}, {0}, {1}, {0}, {1}, {1}, {1}, {1}, {0}, {0}, {1}, {0}, {1}, {1}, {1}}, + InternalBitsPerSample: 1, + InternalRows: 4, + InternalCols: 4, + InternalSamplesPerPixel: 1, + RawData: []int{0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1}, }, }, }, diff --git a/write.go b/write.go index 9c6182b2..20cf1b4b 100644 --- a/write.go +++ b/write.go @@ -582,20 +582,32 @@ func writePixelData(w dicomio.Writer, t tag.Tag, value Value, vr string, vl uint buf.Grow(length) bo, _ := w.GetTransferSyntax() for frame := 0; frame < numFrames; frame++ { + currFrameData := image.Frames[frame].NativeData for pixel := 0; pixel < numPixels; pixel++ { - pixelSlice := image.Frames[frame].NativeData.GetPixelAtIdx(pixel) - for _, pixelValue := range pixelSlice { + for sampleIdx := 0; sampleIdx < currFrameData.SamplesPerPixel(); sampleIdx++ { switch image.Frames[frame].NativeData.BitsPerSample() { case 8: - if err := binary.Write(buf, bo, uint8(pixelValue)); err != nil { + rawSlice, ok := currFrameData.RawDataSlice().([]uint8) + if !ok { + return fmt.Errorf("got frame with bitsAllocated=8 but can't assert RawDataSlice to []uint8") + } + if err := binary.Write(buf, bo, rawSlice[(pixel*currFrameData.SamplesPerPixel())+sampleIdx]); err != nil { return err } case 16: - if err := binary.Write(buf, bo, uint16(pixelValue)); err != nil { + rawSlice, ok := currFrameData.RawDataSlice().([]uint16) + if !ok { + return fmt.Errorf("got frame with bitsAllocated=16 but can't assert RawDataSlice to []uint16") + } + if err := binary.Write(buf, bo, rawSlice[(pixel*currFrameData.SamplesPerPixel())+sampleIdx]); err != nil { return err } case 32: - if err := binary.Write(buf, bo, uint32(pixelValue)); err != nil { + rawSlice, ok := currFrameData.RawDataSlice().([]uint32) + if !ok { + return fmt.Errorf("got frame with bitsAllocated=32 but can't assert RawDataSlice to []uint32") + } + if err := binary.Write(buf, bo, rawSlice[(pixel*currFrameData.SamplesPerPixel())+sampleIdx]); err != nil { return err } default: diff --git a/write_test.go b/write_test.go index 5f260e50..c721fae4 100644 --- a/write_test.go +++ b/write_test.go @@ -290,10 +290,11 @@ func TestWrite(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint8{{1}, {2}, {3}, {4}}, + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint8{1, 2, 3, 4}, }, }, }, @@ -320,10 +321,11 @@ func TestWrite(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint16]{ - InternalBitsPerSample: 16, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint16{{1}, {2}, {3}, {4}}, + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 4}, }, }, }, @@ -348,10 +350,11 @@ func TestWrite(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint32]{ - InternalBitsPerSample: 32, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint32{{1}, {2}, {3}, {4}}, + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint32{1, 2, 3, 4}, }, }, }, @@ -360,7 +363,7 @@ func TestWrite(t *testing.T) { expectedError: nil, }, { - name: "native PixelData: 2 SamplesPerPixel, 2 frames", + name: "native PixelData: 2 InternalSamplesPerPixel, 2 frames", dataset: Dataset{Elements: []*Element{ mustNewElement(tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), mustNewElement(tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}), @@ -376,19 +379,21 @@ func TestWrite(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint32]{ - InternalBitsPerSample: 32, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint32{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{1, 1, 2, 2, 3, 3, 4, 4}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint32]{ - InternalBitsPerSample: 32, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint32{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{5, 1, 2, 2, 3, 3, 4, 5}, }, }, }, @@ -459,19 +464,21 @@ func TestWrite(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint32]{ - InternalBitsPerSample: 32, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint32{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{1, 1, 2, 2, 3, 3, 4, 4}, }, }, { Encapsulated: false, NativeData: &frame.NativeFrame[uint32]{ - InternalBitsPerSample: 32, - InternalRows: 2, - InternalCols: 2, - Data: [][]uint32{{5, 1}, {2, 2}, {3, 3}, {4, 5}}, + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{5, 1, 2, 2, 3, 3, 4, 5}, }, }, }, @@ -496,10 +503,11 @@ func TestWrite(t *testing.T) { { Encapsulated: false, NativeData: &frame.NativeFrame[uint8]{ - InternalBitsPerSample: 8, - InternalRows: 1, - InternalCols: 3, - Data: [][]uint8{{1}, {2}, {3}}, + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{1, 2, 3}, }, }, }, From 4630689e840a60437237b49ec37cb3de1f376924 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Thu, 8 Aug 2024 22:27:04 -0400 Subject: [PATCH 06/11] Add comments, NativeFrame tests --- pkg/frame/native.go | 52 ++++++++++++++++------ pkg/frame/native_test.go | 93 ++++++++++++++++++++++++++++++++++++++++ read.go | 21 ++++----- write.go | 2 +- 4 files changed, 143 insertions(+), 25 deletions(-) diff --git a/pkg/frame/native.go b/pkg/frame/native.go index f882fc21..ba58f557 100644 --- a/pkg/frame/native.go +++ b/pkg/frame/native.go @@ -11,14 +11,42 @@ import ( var UnsupportedSamplesPerPixel = errors.New("unsupported samples per pixel") +// INativeFrame is an interface representation of NativeFrame[I]'s capabilities, +// and offers a way to use a NativeFrame _without_ requiring propogation of +// type parameters. This allows for some more ergonomic signatures, though +// NativeFrame[I] can be used directly as well for those who prefer it. type INativeFrame interface { + // Rows returns the number of rows in this frame. Rows() int + // Cols returns the number of columns in this frame. Cols() int + // SamplesPerPixel returns the number of samples per pixel in this frame. SamplesPerPixel() int + // BitsPerSample returns the bits per sample in this frame. BitsPerSample() int - GetPixel(x, y int) []int - GetPixelAtIdx(idx int) []int + // GetPixel returns the samples (as a slice) for the pixel at (x, y). + // The coordinate system of the image starts with (0, 0) in the upper left + // corner of the image, with X increasing to the right, and Y increasing + // down: + // + // 0 -------▶ X + // | + // | + // ▼ + // Y + GetPixel(x, y int) ([]int, error) + // RawDataSlice will return the underlying data slice, which will be of type + // []I. Based on BitsPerSample, you can anticipate what type of slice you'll + // get, and type assert as needed: + // BitsPerSample Slice + // 8 []uint8 + // 16 []uint16 + // 32 []uint32 RawDataSlice() any + // Equals returns true if this INativeFrame exactly equals the provided + // INativeFrame. This checks every pixel value, so may be expensive. + // In the future we may compute a one time hash during construction to make + // this less expensive in the future if called multiple time. Equals(frame INativeFrame) bool CommonFrame } @@ -51,25 +79,21 @@ func NewNativeFrame[I constraints.Integer](bitsPerSample, rows, cols, pixelsPerF } } -func (n *NativeFrame[I]) Rows() int { return n.InternalRows } -func (n *NativeFrame[I]) Cols() int { return n.InternalCols } -func (n *NativeFrame[I]) BitsPerSample() int { return n.InternalBitsPerSample } - +func (n *NativeFrame[I]) Rows() int { return n.InternalRows } +func (n *NativeFrame[I]) Cols() int { return n.InternalCols } +func (n *NativeFrame[I]) BitsPerSample() int { return n.InternalBitsPerSample } func (n *NativeFrame[I]) SamplesPerPixel() int { return n.InternalSamplesPerPixel } -func (n *NativeFrame[I]) GetPixelAtIdx(idx int) []int { - vals := make([]int, n.InternalSamplesPerPixel) - for i := 0; i < n.InternalSamplesPerPixel; i++ { - vals[i] = int(n.RawData[idx+i]) + +func (n *NativeFrame[I]) GetPixel(x, y int) ([]int, error) { + if x < 0 || y < 0 || x >= n.InternalCols || y >= n.InternalRows { + return nil, fmt.Errorf("provided zero-indexed coordinate (%v, %v) is out of bounds for this frame which has dimension %v x %v", x, y, n.InternalCols, n.InternalRows) } - return vals -} -func (n *NativeFrame[I]) GetPixel(x, y int) []int { pixelIdx := (x * n.InternalSamplesPerPixel) + (y * (n.Cols() * n.InternalSamplesPerPixel)) vals := make([]int, n.InternalSamplesPerPixel) for i := 0; i < n.InternalSamplesPerPixel; i++ { vals[i] = int(n.RawData[pixelIdx+i]) } - return vals + return vals, nil } func (n *NativeFrame[I]) GetSample(x, y, sampleIdx int) int { diff --git a/pkg/frame/native_test.go b/pkg/frame/native_test.go index 2d11eaa7..064ea71d 100644 --- a/pkg/frame/native_test.go +++ b/pkg/frame/native_test.go @@ -2,9 +2,11 @@ package frame_test import ( "errors" + "fmt" "image" "testing" + "github.com/google/go-cmp/cmp" "github.com/suyashkumar/dicom/pkg/frame" ) @@ -111,6 +113,97 @@ func TestNativeFrame_GetImage_Errors(t *testing.T) { } } +func TestNativeFrame_SimpleHelpers(t *testing.T) { + // Tests various helper methods on NativeFrame[I]. GetImage is tested in a + // separate top-level test case. + f := frame.NativeFrame[uint8]{ + RawData: []uint8{1, 2, 3, 4, 5, 6}, + InternalSamplesPerPixel: 2, + InternalRows: 1, + InternalCols: 3, + InternalBitsPerSample: 8, + } + + if got := f.Rows(); got != 1 { + t.Errorf("Rows() unexpected value, got: %v, want: %v", got, 1) + } + if got := f.Cols(); got != 3 { + t.Errorf("Cols() unexpected value, got: %v, want: %v", got, 3) + } + if got := f.SamplesPerPixel(); got != 2 { + t.Errorf("SamplesPerPixel() unexpected value, got: %v, want: %v", got, 2) + } + if got := f.BitsPerSample(); got != 8 { + t.Errorf("BitsPerSample() unexpected value, got: %v, want: %v", got, 8) + } +} + +func TestNativeFrame_GetPixel(t *testing.T) { + f := frame.NativeFrame[uint8]{ + RawData: []uint8{1, 2, 3, 4, 5, 6, 7, 8}, + InternalSamplesPerPixel: 2, + InternalRows: 2, + InternalCols: 2, + InternalBitsPerSample: 8, + } + cases := []struct { + x int + y int + want []int + }{ + { + x: 0, + y: 0, + want: []int{1, 2}, + }, + { + x: 1, + y: 0, + want: []int{3, 4}, + }, + { + x: 0, + y: 1, + want: []int{5, 6}, + }, + { + x: 1, + y: 1, + want: []int{7, 8}, + }, + } + for _, tc := range cases { + t.Run(fmt.Sprintf("x: %d, y: %d", tc.x, tc.y), func(t *testing.T) { + got, err := f.GetPixel(tc.x, tc.y) + if err != nil { + t.Errorf("GetPixel(%d, %d) got unexpected error: %v", tc.x, tc.y, err) + } + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("GetPixel(%d, %d) got unexpected slice. diff: %v", tc.x, tc.y, diff) + } + }) + } +} + +func TestNativeFrame_RawDataSlice(t *testing.T) { + f := frame.NativeFrame[uint8]{ + RawData: []uint8{1, 2, 3, 4, 5, 6, 7, 8}, + InternalSamplesPerPixel: 2, + InternalRows: 2, + InternalCols: 2, + InternalBitsPerSample: 8, + } + + sl := f.RawDataSlice() + rd, ok := sl.([]uint8) + if !ok { + t.Errorf("RawDataSlice() should have returned a []uint8, but unable to type cast to []uint8") + } + if diff := cmp.Diff(rd, f.RawData); diff != "" { + t.Errorf("RawDataSlice() got unexpected slice. diff: %v", diff) + } +} + // within returns true if pt is in the []point func within(pt point, set []point) bool { for _, item := range set { diff --git a/read.go b/read.go index 7af3104d..b310642c 100644 --- a/read.go +++ b/read.go @@ -503,8 +503,9 @@ func (r *reader) readNativeFrames(parsedData *Dataset, fc chan<- *frame.Frame, v return &image, bytesToRead, nil } +// readNativeFrame builds and reads a single NativeFrame[I] from the rawReader. +// TODO(suyashkumar): refactor args to an options struct, or something more compact and readable. func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRead, samplesPerPixel, pixelsPerFrame int, pixelBuf []byte, rawReader *dicomio.Reader) (frame.Frame, int, error) { - // Init current frame nativeFrame := frame.NewNativeFrame[I](bitsAllocated, rows, cols, pixelsPerFrame, samplesPerPixel) currentFrame := frame.Frame{ Encapsulated: false, @@ -519,29 +520,29 @@ func readNativeFrame[I constraints.Integer](bitsAllocated, rows, cols, bytesToRe return frame.Frame{}, bytesToRead, fmt.Errorf("could not read uint%d from input: %w", bitsAllocated, err) } - if bitsAllocated == 8 { + switch bitsAllocated { + case 8: v, ok := any(pixelBuf[0]).(I) if !ok { - + return frame.Frame{}, bytesToRead, fmt.Errorf("internal error - readNativeFrame unexpectedly unable to type cast pixel buffer data to the I type (%T), where bitsAllocated=%v", *new(I), bitsAllocated) } nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v - } else if bitsAllocated == 16 { + case 16: v, ok := any(bo.Uint16(pixelBuf)).(I) if !ok { - + return frame.Frame{}, bytesToRead, fmt.Errorf("internal error - readNativeFrame unexpectedly unable to type cast pixel buffer data to the I type (%T), where bitsAllocated=%v", *new(I), bitsAllocated) } nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v - } else if bitsAllocated == 32 { + case 32: v, ok := any(bo.Uint32(pixelBuf)).(I) if !ok { - + return frame.Frame{}, bytesToRead, fmt.Errorf("internal error - readNativeFrame unexpectedly unable to type cast pixel buffer data to the I type (%T), where bitsAllocated=%v", *new(I), bitsAllocated) } nativeFrame.RawData[(pixel*samplesPerPixel)+value] = v - } else { - return frame.Frame{}, bytesToRead, fmt.Errorf("bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) + default: + return frame.Frame{}, bytesToRead, fmt.Errorf("readNativeFrame unsupported bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) } } - // nativeFrame.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] } return currentFrame, bytesToRead, nil } diff --git a/write.go b/write.go index 6d35dfd8..1c35171b 100644 --- a/write.go +++ b/write.go @@ -638,7 +638,7 @@ func writePixelData(w *dicomio.Writer, t tag.Tag, value Value, vr string, vl uin } numFrames := len(image.Frames) numPixels := image.Frames[0].NativeData.Rows() * image.Frames[0].NativeData.Cols() - numValues := len(image.Frames[0].NativeData.GetPixelAtIdx(0)) + numValues := image.Frames[0].NativeData.SamplesPerPixel() // Total required buffer length in bytes: length := numFrames * numPixels * numValues * image.Frames[0].NativeData.BitsPerSample() / 8 From a69ec441c5433cbc2ca1e497ab5eaaa50272ce72 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Thu, 8 Aug 2024 22:35:34 -0400 Subject: [PATCH 07/11] More comments, cleanup --- pkg/frame/native.go | 43 +++++++++++++++++++++++++++++++++------- pkg/frame/native_test.go | 2 +- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/pkg/frame/native.go b/pkg/frame/native.go index ba58f557..a4e63634 100644 --- a/pkg/frame/native.go +++ b/pkg/frame/native.go @@ -9,16 +9,18 @@ import ( "golang.org/x/exp/constraints" ) -var UnsupportedSamplesPerPixel = errors.New("unsupported samples per pixel") +var ErrUnsupportedSamplesPerPixel = errors.New("unsupported samples per pixel") // INativeFrame is an interface representation of NativeFrame[I]'s capabilities, // and offers a way to use a NativeFrame _without_ requiring propogation of // type parameters. This allows for some more ergonomic signatures, though // NativeFrame[I] can be used directly as well for those who prefer it. type INativeFrame interface { - // Rows returns the number of rows in this frame. + // Rows returns the number of rows in this frame (which is the max y + // dimension). Rows() int - // Cols returns the number of columns in this frame. + // Cols returns the number of columns in this frame (which is the max x + // dimension). Cols() int // SamplesPerPixel returns the number of samples per pixel in this frame. SamplesPerPixel() int @@ -79,11 +81,29 @@ func NewNativeFrame[I constraints.Integer](bitsPerSample, rows, cols, pixelsPerF } } -func (n *NativeFrame[I]) Rows() int { return n.InternalRows } -func (n *NativeFrame[I]) Cols() int { return n.InternalCols } -func (n *NativeFrame[I]) BitsPerSample() int { return n.InternalBitsPerSample } +// Rows returns the number of rows in this frame (which is the max y dimension). +func (n *NativeFrame[I]) Rows() int { return n.InternalRows } + +// Cols returns the number of columns in this frame (which is the max x +// dimension). +func (n *NativeFrame[I]) Cols() int { return n.InternalCols } + +// BitsPerSample returns the bits per sample. +func (n *NativeFrame[I]) BitsPerSample() int { return n.InternalBitsPerSample } + +// SamplesPerPixel returns the samples per pixel. func (n *NativeFrame[I]) SamplesPerPixel() int { return n.InternalSamplesPerPixel } +// GetPixel returns the samples (as a slice) for the pixel at (x, y). +// The coordinate system of the image starts with (0, 0) in the upper left +// corner of the image, with X increasing to the right, and Y increasing +// down: +// +// 0 -------▶ X +// | +// | +// ▼ +// Y func (n *NativeFrame[I]) GetPixel(x, y int) ([]int, error) { if x < 0 || y < 0 || x >= n.InternalCols || y >= n.InternalRows { return nil, fmt.Errorf("provided zero-indexed coordinate (%v, %v) is out of bounds for this frame which has dimension %v x %v", x, y, n.InternalCols, n.InternalRows) @@ -96,11 +116,20 @@ func (n *NativeFrame[I]) GetPixel(x, y int) ([]int, error) { return vals, nil } +// GetSample returns a specific sample inside a pixel at (x, y). func (n *NativeFrame[I]) GetSample(x, y, sampleIdx int) int { dataSampleIdx := (x * n.InternalSamplesPerPixel) + (y * (n.Cols() * n.InternalSamplesPerPixel)) + sampleIdx return int(n.RawData[dataSampleIdx]) } +// RawDataSlice will return the underlying data slice, which will be of type +// []I. Based on BitsPerSample, you can anticipate what type of slice you'll +// get, and type assert as needed: +// +// BitsPerSample Slice +// 8 []uint8 +// 16 []uint16 +// 32 []uint32 func (n *NativeFrame[I]) RawDataSlice() any { return n.RawData } // IsEncapsulated indicates if the frame is encapsulated or not. @@ -123,7 +152,7 @@ func (n *NativeFrame[I]) GetEncapsulatedFrame() (*EncapsulatedFrame, error) { // autoscale pixel values or use window width or level info. func (n *NativeFrame[I]) GetImage() (image.Image, error) { if n.InternalSamplesPerPixel != 1 { - return nil, fmt.Errorf("GetImage(): unexpected InternalSamplesPerPixel got %v, expected 1 since only grayscale images are supported for now %w", n.InternalSamplesPerPixel, UnsupportedSamplesPerPixel) + return nil, fmt.Errorf("GetImage(): unexpected InternalSamplesPerPixel got %v, expected 1 since only grayscale images are supported for now %w", n.InternalSamplesPerPixel, ErrUnsupportedSamplesPerPixel) } i := image.NewGray16(image.Rect(0, 0, n.Cols(), n.Rows())) for idx := 0; idx < len(n.RawData); idx++ { diff --git a/pkg/frame/native_test.go b/pkg/frame/native_test.go index 064ea71d..3ce576c0 100644 --- a/pkg/frame/native_test.go +++ b/pkg/frame/native_test.go @@ -99,7 +99,7 @@ func TestNativeFrame_GetImage_Errors(t *testing.T) { nativeFrame: frame.NativeFrame[int]{ InternalSamplesPerPixel: 2, }, - wantErr: frame.UnsupportedSamplesPerPixel, + wantErr: frame.ErrUnsupportedSamplesPerPixel, }, } From 87f9dc9cc20893d43d1bb2cd5c8a750817ab18a4 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Thu, 8 Aug 2024 23:03:37 -0400 Subject: [PATCH 08/11] add some equals tests --- pkg/frame/native.go | 3 +- pkg/frame/native_test.go | 86 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/pkg/frame/native.go b/pkg/frame/native.go index a4e63634..6a11aab1 100644 --- a/pkg/frame/native.go +++ b/pkg/frame/native.go @@ -169,7 +169,8 @@ func (n *NativeFrame[I]) Equals(target INativeFrame) bool { } if n.Rows() != target.Rows() || n.Cols() != target.Cols() || - n.BitsPerSample() != n.BitsPerSample() { + n.BitsPerSample() != target.BitsPerSample() || + n.SamplesPerPixel() != target.SamplesPerPixel() { return false } diff --git a/pkg/frame/native_test.go b/pkg/frame/native_test.go index 3ce576c0..1669e2a4 100644 --- a/pkg/frame/native_test.go +++ b/pkg/frame/native_test.go @@ -204,6 +204,92 @@ func TestNativeFrame_RawDataSlice(t *testing.T) { } } +func TestNativeFrame_Equals(t *testing.T) { + cases := []struct { + name string + a frame.NativeFrame[int] + b frame.NativeFrame[int] + equal bool + }{ + { + name: "equal", + a: frame.NativeFrame[int]{ + RawData: []int{1, 2, 3}, + InternalSamplesPerPixel: 2, + InternalCols: 3, + InternalRows: 4, + InternalBitsPerSample: 64, + }, + b: frame.NativeFrame[int]{ + RawData: []int{1, 2, 3}, + InternalSamplesPerPixel: 2, + InternalCols: 3, + InternalRows: 4, + InternalBitsPerSample: 64, + }, + equal: true, + }, + { + name: "mismatched data", + a: frame.NativeFrame[int]{ + RawData: []int{1, 2, 3}, + }, + b: frame.NativeFrame[int]{ + RawData: []int{2, 2, 3}, + }, + equal: false, + }, + { + name: "mismatched BitsPerSample", + a: frame.NativeFrame[int]{ + InternalBitsPerSample: 2, + }, + b: frame.NativeFrame[int]{ + InternalBitsPerSample: 4, + }, + equal: false, + }, + { + name: "mismatched SamplesPerPixel", + a: frame.NativeFrame[int]{ + InternalSamplesPerPixel: 2, + }, + b: frame.NativeFrame[int]{ + InternalSamplesPerPixel: 4, + }, + equal: false, + }, + { + name: "mismatched Rows", + a: frame.NativeFrame[int]{ + InternalRows: 2, + }, + b: frame.NativeFrame[int]{ + InternalRows: 4, + }, + equal: false, + }, + { + name: "mismatched Cols", + a: frame.NativeFrame[int]{ + InternalCols: 2, + }, + b: frame.NativeFrame[int]{ + InternalCols: 4, + }, + equal: false, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.a.Equals(&tc.b) + if got != tc.equal { + t.Errorf("Equals(%+v, %+v) got unexpected value. got: %v, want: %v", tc.a, tc.b, got, tc.equal) + } + }) + } +} + // within returns true if pt is in the []point func within(pt point, set []point) bool { for _, item := range set { From e598a0ecd0b7f1e112cd2e44f433e0f499ffc925 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Thu, 8 Aug 2024 23:06:15 -0400 Subject: [PATCH 09/11] comment update --- pkg/frame/native.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/frame/native.go b/pkg/frame/native.go index 6a11aab1..04acff83 100644 --- a/pkg/frame/native.go +++ b/pkg/frame/native.go @@ -71,6 +71,9 @@ type NativeFrame[I constraints.Integer] struct { InternalBitsPerSample int } +// NewNativeFrame creates a new NativeFrame[I] given the input parameters. It +// initializes the NativeFrame's internal RawData slice based on pixelsPerFrame +// and samplesPerPixel. func NewNativeFrame[I constraints.Integer](bitsPerSample, rows, cols, pixelsPerFrame, samplesPerPixel int) *NativeFrame[I] { return &NativeFrame[I]{ InternalBitsPerSample: bitsPerSample, From a667e8eb4ea723a7f4324512666fec034c2c1e6a Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Thu, 8 Aug 2024 23:29:25 -0400 Subject: [PATCH 10/11] add uint32 read test --- read_test.go | 77 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/read_test.go b/read_test.go index b7e4ecbe..04e5b12c 100644 --- a/read_test.go +++ b/read_test.go @@ -221,15 +221,16 @@ func TestReadNativeFrames(t *testing.T) { cases := []struct { Name string existingData Dataset - data []uint16 + uint16Data []uint16 dataBytes []byte + uint32Data []uint32 expectedPixelData *PixelDataInfo expectedError error pixelVLOverride uint32 parseOptSet parseOptSet }{ { - Name: "5x5, 1 frame, 1 samples/pixel", + Name: "5x5, 1 frame, 1 samples/pixel, bitsAllocated=16", existingData: Dataset{Elements: []*Element{ mustNewElement(tag.Rows, []int{5}), mustNewElement(tag.Columns, []int{5}), @@ -237,7 +238,7 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{16}), mustNewElement(tag.SamplesPerPixel, []int{1}), }}, - data: []uint16{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + uint16Data: []uint16{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, expectedPixelData: &PixelDataInfo{ IsEncapsulated: false, Frames: []*frame.Frame{ @@ -256,7 +257,7 @@ func TestReadNativeFrames(t *testing.T) { expectedError: nil, }, { - Name: "2x2, 3 frames, 1 samples/pixel", + Name: "2x2, 3 frames, 1 samples/pixel, bitsAllocated=16", existingData: Dataset{Elements: []*Element{ mustNewElement(tag.Rows, []int{2}), mustNewElement(tag.Columns, []int{2}), @@ -264,7 +265,7 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{16}), mustNewElement(tag.SamplesPerPixel, []int{1}), }}, - data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 0}, + uint16Data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 0}, expectedPixelData: &PixelDataInfo{ IsEncapsulated: false, Frames: []*frame.Frame{ @@ -303,7 +304,7 @@ func TestReadNativeFrames(t *testing.T) { expectedError: nil, }, { - Name: "2x2, 2 frames, 2 samples/pixel", + Name: "2x2, 2 frames, 2 samples/pixel, bitsAllocated=16", existingData: Dataset{Elements: []*Element{ mustNewElement(tag.Rows, []int{2}), mustNewElement(tag.Columns, []int{2}), @@ -311,7 +312,7 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{16}), mustNewElement(tag.SamplesPerPixel, []int{2}), }}, - data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 5}, + uint16Data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 5}, expectedPixelData: &PixelDataInfo{ IsEncapsulated: false, Frames: []*frame.Frame{ @@ -339,6 +340,33 @@ func TestReadNativeFrames(t *testing.T) { }, expectedError: nil, }, + { + Name: "bitsAllocated=32", + existingData: Dataset{Elements: []*Element{ + mustNewElement(tag.Rows, []int{5}), + mustNewElement(tag.Columns, []int{5}), + mustNewElement(tag.NumberOfFrames, []string{"1"}), + mustNewElement(tag.BitsAllocated, []int{32}), + mustNewElement(tag.SamplesPerPixel, []int{1}), + }}, + uint32Data: []uint32{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + expectedPixelData: &PixelDataInfo{ + IsEncapsulated: false, + Frames: []*frame.Frame{ + { + Encapsulated: false, + NativeData: &frame.NativeFrame[uint32]{ + InternalBitsPerSample: 32, + InternalRows: 5, + InternalCols: 5, + InternalSamplesPerPixel: 1, + RawData: []uint32{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + }, + }, + }, + expectedError: nil, + }, { Name: "insufficient bytes, uint32", existingData: Dataset{Elements: []*Element{ @@ -348,7 +376,7 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{32}), mustNewElement(tag.SamplesPerPixel, []int{2}), }}, - data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3}, + uint16Data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3}, expectedPixelData: nil, expectedError: ErrorMismatchPixelDataLength, }, @@ -361,7 +389,7 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{32}), mustNewElement(tag.SamplesPerPixel, []int{2}), }}, - data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 2}, + uint16Data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 2}, expectedPixelData: nil, expectedError: ErrorMismatchPixelDataLength, }, @@ -374,7 +402,7 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{32}), mustNewElement(tag.SamplesPerPixel, []int{2}), }}, - data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 2}, + uint16Data: []uint16{1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2, 2}, expectedPixelData: &PixelDataInfo{ ParseErr: ErrorMismatchPixelDataLength, Frames: []*frame.Frame{ @@ -396,7 +424,7 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{16}), mustNewElement(tag.SamplesPerPixel, []int{1}), }}, - data: []uint16{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + uint16Data: []uint16{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, expectedPixelData: nil, expectedError: ErrorElementNotFound, }, @@ -409,12 +437,12 @@ func TestReadNativeFrames(t *testing.T) { mustNewElement(tag.BitsAllocated, []int{24}), mustNewElement(tag.SamplesPerPixel, []int{1}), }}, - data: []uint16{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + uint16Data: []uint16{1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, expectedPixelData: nil, expectedError: ErrorUnsupportedBitsAllocated, }, { - Name: "3x3, 3 frames, 1 samples/pixel, data bytes with padded 0", + Name: "3x3, 3 frames, 1 samples/pixel, bytes data (uint8) with padded 0", existingData: Dataset{Elements: []*Element{ mustNewElement(tag.Rows, []int{3}), mustNewElement(tag.Columns, []int{3}), @@ -462,7 +490,7 @@ func TestReadNativeFrames(t *testing.T) { expectedError: nil, }, { - Name: "1x1, 3 frames, 3 samples/pixel, data bytes with padded 0", + Name: "1x1, 3 frames, 3 samples/pixel, bytes data (uint8) with padded 0", existingData: Dataset{Elements: []*Element{ mustNewElement(tag.Rows, []int{1}), mustNewElement(tag.Columns, []int{1}), @@ -530,7 +558,7 @@ func TestReadNativeFrames(t *testing.T) { dcmdata := bytes.Buffer{} var expectedBytes int - if len(tc.data) == 0 { + if len(tc.dataBytes) != 0 { // writing byte-by-byte expectedBytes = len(tc.dataBytes) for _, item := range tc.dataBytes { @@ -538,10 +566,17 @@ func TestReadNativeFrames(t *testing.T) { t.Errorf("TestReadNativeFrames: Unable to setup test buffer") } } - } else { + } else if len(tc.uint16Data) != 0 { // writing 2 bytes (uint16) at a time - expectedBytes = len(tc.data) * 2 - for _, item := range tc.data { + expectedBytes = len(tc.uint16Data) * 2 + for _, item := range tc.uint16Data { + if err := binary.Write(&dcmdata, binary.LittleEndian, item); err != nil { + t.Errorf("TestReadNativeFrames: Unable to setup test buffer") + } + } + } else if len(tc.uint32Data) != 0 { + expectedBytes = len(tc.uint32Data) * 4 + for _, item := range tc.uint32Data { if err := binary.Write(&dcmdata, binary.LittleEndian, item); err != nil { t.Errorf("TestReadNativeFrames: Unable to setup test buffer") } @@ -562,14 +597,14 @@ func TestReadNativeFrames(t *testing.T) { pixelData, bytesRead, err := r.readNativeFrames(&tc.existingData, nil, vl) if !errors.Is(err, tc.expectedError) { - t.Errorf("TestReadNativeFrames(%v): did not get expected error. got: %v, want: %v", tc.data, err, tc.expectedError) + t.Errorf("TestReadNativeFrames(%+v): did not get expected error. got: %v, want: %v", tc, err, tc.expectedError) } if err == nil && bytesRead != expectedBytes { - t.Errorf("TestReadNativeFrames(%v): did not read expected number of bytes. got: %d, want: %d", tc.data, bytesRead, expectedBytes) + t.Errorf("TestReadNativeFrames(%+v): did not read expected number of bytes. got: %d, want: %d", tc, bytesRead, expectedBytes) } if diff := cmp.Diff(tc.expectedPixelData, pixelData, cmpopts.EquateErrors()); diff != "" { - t.Errorf("TestReadNativeFrames(%v): unexpected diff: %v", tc.data, diff) + t.Errorf("TestReadNativeFrames(%+v): unexpected diff: %v", tc, diff) } }) } From f0126e87fbc75b58ead1ea7a9f37f249aa3eddfe Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Thu, 8 Aug 2024 23:31:45 -0400 Subject: [PATCH 11/11] fix typo --- write_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/write_test.go b/write_test.go index ff231a4a..56ca9549 100644 --- a/write_test.go +++ b/write_test.go @@ -362,7 +362,7 @@ func TestWrite(t *testing.T) { wantError: nil, }, { - name: "native PixelData: 2 InternalSamplesPerPixel, 2 frames", + name: "native PixelData: 2 SamplesPerPixel, 2 frames", dataset: Dataset{Elements: []*Element{ mustNewElement(tag.MediaStorageSOPClassUID, []string{"1.2.840.10008.5.1.4.1.1.1.2"}), mustNewElement(tag.MediaStorageSOPInstanceUID, []string{"1.2.3.4.5.6.7"}),