Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[work in progress / hacking] Support multi-go-type pixeldata #315

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions element.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
40 changes: 20 additions & 20 deletions element_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}},
},
},
},
Expand All @@ -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}},
},
},
},
Expand All @@ -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}},
},
},
},
Expand All @@ -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}},
},
},
},
Expand Down
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ module github.com/suyashkumar/dicom
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/exp v0.0.0-20240525044651-4c93da0ed11d
golang.org/x/text v0.3.8
)

require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
16 changes: 4 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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=
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/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=
4 changes: 2 additions & 2 deletions pkg/frame/encapsulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 7 additions & 7 deletions pkg/frame/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -33,20 +33,20 @@ 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.
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()
}
Expand Down Expand Up @@ -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
Expand Down
85 changes: 66 additions & 19 deletions pkg/frame/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
30 changes: 15 additions & 15 deletions pkg/frame/native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}},
},
Expand All @@ -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 {
Expand Down
Loading
Loading