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

Utils function for read/write value from unprocessed data #306

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (d *Dataset) FindElementByTag(tag tag.Tag) (*Element, error) {
return nil, ErrorElementNotFound
}

func (d *Dataset) transferSyntax() (binary.ByteOrder, bool, error) {
func (d *Dataset) TransferSyntax() (binary.ByteOrder, bool, error) {
elem, err := d.FindElementByTag(tag.TransferSyntaxUID)
if err != nil {
return nil, false, err
Expand Down
143 changes: 143 additions & 0 deletions pkg/frame/inplace/pixeldata_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package inplace

import (
"encoding/binary"
"errors"
"fmt"
"strconv"

"github.com/suyashkumar/dicom"
"github.com/suyashkumar/dicom/pkg/tag"
)

// GetIntFromValue is similar to dicom.MustGetInts but without panic
func GetIntFromValue(v dicom.Value) (int, error) {
if v.ValueType() != dicom.Ints {
return 0, errors.New("value is not ints")
}
arr, ok := v.GetValue().([]int)
if !ok {
return 0, errors.New("actual value is not ints")
}
if len(arr) == 0 {
return 0, errors.New("empty ints value")
}
return arr[0], nil
}

// GetStringFromValue is similar to dicom.MustGetStrings but without panic
func GetStringFromValue(v dicom.Value) (string, error) {
if v.ValueType() != dicom.Strings {
return "", errors.New("value is not ints")
}
arr, ok := v.GetValue().([]string)
if !ok {
return "", errors.New("actual value is not ints")
}
if len(arr) == 0 {
return "", errors.New("empty ints value")
}
return arr[0], nil
}

// PixelDataMetadata is the metadata for tag.PixelData
type PixelDataMetadata struct {
Rows int
Cols int
Frames int
SamplesPerPixel int
BitsAllocated int
PlanarConfiguration int
Bo binary.ByteOrder
}

// GetPixelDataMetadata returns the pixel data metadata.
func GetPixelDataMetadata(ds *dicom.Dataset) (*PixelDataMetadata, error) {
re := &PixelDataMetadata{}
rows, err := ds.FindElementByTag(tag.Rows)
if err != nil {
return nil, fmt.Errorf("get Rows element: %w", err)
}
if re.Rows, err = GetIntFromValue(rows.Value); err != nil {
return nil, fmt.Errorf("convert Rows element to int: %w", err)
}

cols, err := ds.FindElementByTag(tag.Columns)
if err != nil {
return nil, fmt.Errorf("get Columns element: %w", err)
}
if re.Cols, err = GetIntFromValue(cols.Value); err != nil {
return nil, fmt.Errorf("convert Columns element to int: %w", err)
}

numberOfFrames, err := ds.FindElementByTag(tag.NumberOfFrames)
if err != nil {
re.Frames = 1
} else {
var framesStr string
framesStr, err = GetStringFromValue(numberOfFrames.Value)
if err != nil {
return nil, fmt.Errorf("convert NumberOfFrames element to str: %w", err)
}
if re.Frames, err = strconv.Atoi(framesStr); err != nil {
return nil, fmt.Errorf("convert NumberOfFrames to int: %w", err)
}
}

samplesPerPixel, err := ds.FindElementByTag(tag.SamplesPerPixel)
if err != nil {
return nil, fmt.Errorf("get SamplesPerPixel element: %w", err)
}
if re.SamplesPerPixel, err = GetIntFromValue(samplesPerPixel.Value); err != nil {
return nil, fmt.Errorf("convert SamplesPerPixel element to int: %w", err)
}
bitsAllocated, err := ds.FindElementByTag(tag.BitsAllocated)
if err != nil {
return nil, fmt.Errorf("get BitsAllocated element: %w", err)
}
if re.BitsAllocated, err = GetIntFromValue(bitsAllocated.Value); err != nil {
return nil, fmt.Errorf("convert BitsAllocated element to int: %w", err)
}
re.Bo, _, err = ds.TransferSyntax()
if err != nil {
return nil, fmt.Errorf("get byteOrder: %w", err)
}

planarConfElement, err := ds.FindElementByTag(tag.PlanarConfiguration)
if err != nil {
re.PlanarConfiguration = 0
} else {
if re.PlanarConfiguration, err = GetIntFromValue(planarConfElement.Value); err != nil {
return nil, fmt.Errorf("convert Rows element to int: %w", err)
}
}

return re, nil
}

// IsSafeForUnprocessedValueDataHandling check if we can support in-place read-write
// from Pixeldata.UnprocessedValueData
// This avoids the case that we can not handle it, yet.
func IsSafeForUnprocessedValueDataHandling(info *PixelDataMetadata, unprocessedValueData []byte) error {
// https://dicom.innolitics.com/ciods/enhanced-mr-image/enhanced-mr-image/00280006
if info.PlanarConfiguration == 1 {
ducquangkstn marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("unsupported PlanarConfiguration: %d", info.PlanarConfiguration)
}
// TODO: support for BitsAllocated == 1
switch info.BitsAllocated {
case 8, 16, 32:
default: // bitsAllocated = 1 and other cases
return fmt.Errorf("unsupported bit allocated: %d", info.BitsAllocated)
}
pixelsPerFrame := info.Rows * info.Cols
bytesAllocated := info.BitsAllocated / 8
expectedBytes := bytesAllocated * info.SamplesPerPixel * info.Frames * pixelsPerFrame
// odd number of bytes.
if expectedBytes%2 == 1 {
expectedBytes += 1
}
if len(unprocessedValueData) != expectedBytes {
return errors.New("mismatch data size")
}
return nil
}
29 changes: 29 additions & 0 deletions pkg/frame/inplace/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Package inplace contains code for handling UnprocessedValueData
package inplace

// ReadUnprocessedValueData read the value of Dicom image directly
// from the byte array of PixelData.UnprocessedValueData with given frame ID.
// This ease the memory usage of reading DICOM image.
func ReadUnprocessedValueData(info *PixelDataMetadata, unprocessedValueData []byte, frameIndex int) ([][]int, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this returns [][]int, this will still have the same issue of expanding the memory from the smaller int to the 64-bit int right? Is the main place this saves memory in the fact that it only does one frame at a time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the main place this saves memory in the fact that it only does one frame at a time

Yes, most of the DICOM with the issues contains multiple frames.

But, I think I would change to a new structure of storing data.

B/c users need to adapt to new way of read/write pixel data anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed at 16701fa

pixelsPerFrame := info.Rows * info.Cols
bytesAllocated := info.BitsAllocated / 8
offset := frameIndex * pixelsPerFrame * info.SamplesPerPixel * bytesAllocated
samplesPerPixel := info.SamplesPerPixel

re := make([][]int, samplesPerPixel)
for i := 0; i < samplesPerPixel; i++ {
re[i] = make([]int, pixelsPerFrame)
for j := 0; j < pixelsPerFrame; j++ {
pointOffset := offset + j*info.SamplesPerPixel*bytesAllocated + i*bytesAllocated
switch bytesAllocated {
case 1:
re[i][j] = int(unprocessedValueData[pointOffset])
case 2:
re[i][j] = int(info.Bo.Uint16(unprocessedValueData[pointOffset : pointOffset+bytesAllocated]))
case 4:
re[i][j] = int(info.Bo.Uint32(unprocessedValueData[pointOffset : pointOffset+bytesAllocated]))
}
}
}
return re, nil
}
Loading
Loading