diff --git a/element.go b/element.go index d487017f..b335d02d 100644 --- a/element.go +++ b/element.go @@ -426,8 +426,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 @@ -453,7 +455,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 f6ab5351..3c5ae833 100644 --- a/element_test.go +++ b/element_test.go @@ -246,11 +246,12 @@ 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, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 4}, }, }, }, @@ -260,11 +261,12 @@ 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, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 4}, }, }, }, @@ -278,11 +280,12 @@ 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, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 6}, }, }, }, @@ -292,11 +295,12 @@ 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, + InternalSamplesPerPixel: 1, + RawData: []int{1, 2, 3, 4}, }, }, }, diff --git a/go.mod b/go.mod index 7d963732..2b45339e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module github.com/suyashkumar/dicom go 1.22 require ( - github.com/google/go-cmp v0.5.2 + 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/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect diff --git a/go.sum b/go.sum index da47d818..6ac7ec39 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ -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/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/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -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= 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..04acff83 100644 --- a/pkg/frame/native.go +++ b/pkg/frame/native.go @@ -1,62 +1,195 @@ package frame import ( + "errors" + "fmt" "image" "image/color" + + "golang.org/x/exp/constraints" ) +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 (which is the max y + // dimension). + Rows() int + // 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 + // BitsPerSample returns the bits per sample in this frame. + BitsPerSample() 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 +} + // NativeFrame represents a native image frame -type NativeFrame struct { - // Data is a slice of pixels, where each pixel can have multiple values - Data [][]int - Rows int - Cols int - BitsPerSample int +type NativeFrame[I constraints.Integer] struct { + // 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 +} + +// 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, + InternalRows: rows, + InternalCols: cols, + RawData: make([]I, pixelsPerFrame*samplesPerPixel), + InternalSamplesPerPixel: samplesPerPixel, + } } +// 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) + } + 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, 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. -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)) - 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 +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, ErrUnsupportedSamplesPerPixel) + } + i := image.NewGray16(image.Rect(0, 0, n.Cols(), n.Rows())) + 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 } // 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() != target.BitsPerSample() || + n.SamplesPerPixel() != target.SamplesPerPixel() { return false } - for pixIdx, pix := range n.Data { - for valIdx, val := range pix { - if val != target.Data[pixIdx][valIdx] { - return false - } + + // If BitsPerSample are equal, we assume the target is of type + // *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. + } + + // 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 3d3ed3ba..1669e2a4 100644 --- a/pkg/frame/native_test.go +++ b/pkg/frame/native_test.go @@ -1,9 +1,12 @@ package frame_test import ( + "errors" + "fmt" "image" "testing" + "github.com/google/go-cmp/cmp" "github.com/suyashkumar/dicom/pkg/frame" ) @@ -16,33 +19,36 @@ 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, + InternalSamplesPerPixel: 1, + RawData: []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, + InternalSamplesPerPixel: 1, + RawData: []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, + 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}}, }, @@ -61,8 +67,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 { @@ -82,6 +88,208 @@ 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.ErrUnsupportedSamplesPerPixel, + }, + } + + 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) + } + }) + } +} + +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) + } +} + +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 { diff --git a/read.go b/read.go index 3aba364c..b310642c 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 ( @@ -458,44 +459,35 @@ 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, samplesPerPixel) for pixel := 0; pixel < pixelsPerFrame; pixel++ { for value := 0; value < samplesPerPixel; value++ { - currentFrame.NativeData.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel] + nativeFrame.RawData[(pixel*samplesPerPixel)+value] = buf[pixel*samplesPerPixel+value] } } + 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("error when reading Native PixelData, unsupported bitsAllocated, got 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) + 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 @@ -511,6 +503,50 @@ 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) { + nativeFrame := frame.NewNativeFrame[I](bitsAllocated, rows, cols, pixelsPerFrame, samplesPerPixel) + currentFrame := frame.Frame{ + Encapsulated: false, + NativeData: nativeFrame, + } + + 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) + } + 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 + 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 + 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 + default: + return frame.Frame{}, bytesToRead, fmt.Errorf("readNativeFrame unsupported bitsAllocated=%d : %w", bitsAllocated, ErrorUnsupportedBitsAllocated) + } + } + } + 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 2527f5de..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,17 +238,18 @@ 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{ { 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[uint16]{ + 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}, }, }, }, @@ -255,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}), @@ -263,35 +265,38 @@ 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{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {2}}, + NativeData: &frame.NativeFrame[uint16]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 2}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {2}}, + NativeData: &frame.NativeFrame[uint16]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 2}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1}, {2}, {3}, {0}}, + NativeData: &frame.NativeFrame[uint16]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 0}, }, }, }, @@ -299,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}), @@ -307,26 +312,55 @@ 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{ { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 16, - Rows: 2, - Cols: 2, - Data: [][]int{{1, 2}, {3, 2}, {1, 2}, {3, 2}}, + NativeData: &frame.NativeFrame[uint16]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint16{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[uint16]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint16{1, 2, 3, 2, 1, 2, 3, 5}, + }, + }, + }, + }, + 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}, }, }, }, @@ -342,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, }, @@ -355,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, }, @@ -368,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{ @@ -390,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, }, @@ -403,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}), @@ -422,30 +456,33 @@ 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[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{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[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{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[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 3, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{11, 12, 13, 21, 22, 23, 31, 32, 33}, }, }, }, @@ -453,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}), @@ -467,29 +504,32 @@ 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[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + InternalSamplesPerPixel: 3, + RawData: []uint8{1, 2, 3}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 1, - Cols: 1, - Data: [][]int{{1, 2, 3}}, + NativeData: &frame.NativeFrame[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + InternalSamplesPerPixel: 3, + RawData: []uint8{1, 2, 3}, }, }, { Encapsulated: false, - NativeData: frame.NativeFrame{ - BitsPerSample: 8, - Rows: 1, - Cols: 1, - Data: [][]int{{1, 2, 3}}, + NativeData: &frame.NativeFrame[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 1, + InternalSamplesPerPixel: 3, + RawData: []uint8{1, 2, 3}, }, }, }, @@ -518,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 { @@ -526,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") } @@ -550,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) } }) } @@ -812,11 +859,12 @@ 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, + InternalSamplesPerPixel: 1, + RawData: []int{0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1}, }, }, }, @@ -839,11 +887,12 @@ 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, + 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 4175b188..1c35171b 100644 --- a/write.go +++ b/write.go @@ -637,29 +637,41 @@ func writePixelData(w *dicomio.Writer, t tag.Tag, value Value, vr string, vl uin 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 := image.Frames[0].NativeData.SamplesPerPixel() // 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++ { + currFrameData := image.Frames[frame].NativeData 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 { + 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 b689cc51..56ca9549 100644 --- a/write_test.go +++ b/write_test.go @@ -288,11 +288,12 @@ 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[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint8{1, 2, 3, 4}, }, }, }, @@ -318,11 +319,12 @@ 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[uint16]{ + InternalBitsPerSample: 16, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint16{1, 2, 3, 4}, }, }, }, @@ -346,11 +348,12 @@ 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[uint32]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 1, + RawData: []uint32{1, 2, 3, 4}, }, }, }, @@ -374,20 +377,22 @@ 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[uint32]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{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[uint32]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{5, 1, 2, 2, 3, 3, 4, 5}, }, }, }, @@ -463,20 +468,22 @@ 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[uint32]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{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[uint32]{ + InternalBitsPerSample: 32, + InternalRows: 2, + InternalCols: 2, + InternalSamplesPerPixel: 2, + RawData: []uint32{5, 1, 2, 2, 3, 3, 4, 5}, }, }, }, @@ -500,11 +507,12 @@ 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[uint8]{ + InternalBitsPerSample: 8, + InternalRows: 1, + InternalCols: 3, + InternalSamplesPerPixel: 1, + RawData: []uint8{1, 2, 3}, }, }, },