Skip to content

Commit

Permalink
Use a once-allocated buffer for reading native pixel values instead o…
Browse files Browse the repository at this point in the history
…f using binary.Read (which allocates a slice under the hood) (#163)

This change replaces calls to binary.Read with manual unpacking of various flavors of uints into a once-allocated buffer in readNativeFrames. This leads to a significant performance speedup by avoiding binary.Read's implicit slice allocation once per read.
  • Loading branch information
kaxap authored Dec 22, 2020
1 parent 33d864f commit a6f29f2
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 17 deletions.
14 changes: 14 additions & 0 deletions mocks/pkg/dicomio/mock_reader.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkg/dicomio/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Reader interface {
// SetCodingSystem sets the charset.CodingSystem to be used when ReadString
// is called.
SetCodingSystem(cs charset.CodingSystem)
ByteOrder() binary.ByteOrder
}

type reader struct {
Expand Down Expand Up @@ -234,3 +235,7 @@ func (r *reader) SetCodingSystem(cs charset.CodingSystem) {
func (r *reader) Peek(n int) ([]byte, error) {
return r.in.Peek(n)
}

func (r *reader) ByteOrder() binary.ByteOrder {
return r.bo
}
29 changes: 13 additions & 16 deletions read.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ func readNativeFrames(d dicomio.Reader, parsedData *Dataset, fc chan<- *frame.Fr

// Parse the pixels:
image.Frames = make([]frame.Frame, nFrames)
bo := d.ByteOrder()
bytesAllocated := bitsAllocated / 8
pixelBuf := make([]byte, bytesAllocated)
for frameIdx := 0; frameIdx < nFrames; frameIdx++ {
// Init current frame
currentFrame := frame.Frame{
Expand All @@ -226,24 +229,18 @@ func readNativeFrames(d dicomio.Reader, parsedData *Dataset, fc chan<- *frame.Fr
buf := make([]int, int(pixelsPerFrame)*samplesPerPixel)
for pixel := 0; pixel < int(pixelsPerFrame); pixel++ {
for value := 0; value < samplesPerPixel; value++ {
_, err := io.ReadFull(d, pixelBuf)
if err != nil {
return nil, bytesRead,
fmt.Errorf("could not read uint%d from input: %w", bitsAllocated, err)
}

if bitsAllocated == 8 {
val, err := d.ReadUInt8()
if err != nil {
return nil, bytesRead, err
}
buf[(pixel*samplesPerPixel)+value] = int(val)
buf[(pixel*samplesPerPixel)+value] = int(pixelBuf[0])
} else if bitsAllocated == 16 {
val, err := d.ReadUInt16()
if err != nil {
return nil, bytesRead, err
}
buf[(pixel*samplesPerPixel)+value] = int(val)
buf[(pixel*samplesPerPixel)+value] = int(bo.Uint16(pixelBuf))
} else if bitsAllocated == 32 {
val, err := d.ReadUInt32()
if err != nil {
return nil, bytesRead, err
}
buf[(pixel*samplesPerPixel)+value] = int(val)
buf[(pixel*samplesPerPixel)+value] = int(bo.Uint32(pixelBuf))
}
}
currentFrame.NativeData.Data[pixel] = buf[pixel*samplesPerPixel : (pixel+1)*samplesPerPixel]
Expand All @@ -254,7 +251,7 @@ func readNativeFrames(d dicomio.Reader, parsedData *Dataset, fc chan<- *frame.Fr
}
}

bytesRead = (bitsAllocated / 8) * samplesPerPixel * pixelsPerFrame * nFrames
bytesRead = bytesAllocated * samplesPerPixel * pixelsPerFrame * nFrames

return &image, bytesRead, nil
}
Expand Down
17 changes: 16 additions & 1 deletion read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math/rand"
"strconv"
"testing"
Expand Down Expand Up @@ -249,6 +251,19 @@ func TestReadNativeFrames(t *testing.T) {
},
expectedError: nil,
},
{
Name: "insufficient bytes, uint32",
existingData: Dataset{Elements: []*Element{
mustNewElement(tag.Rows, []int{2}),
mustNewElement(tag.Columns, []int{2}),
mustNewElement(tag.NumberOfFrames, []string{"2"}),
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},
expectedPixelData: nil,
expectedError: io.ErrUnexpectedEOF,
},
{
Name: "missing Columns",
existingData: Dataset{Elements: []*Element{
Expand Down Expand Up @@ -278,7 +293,7 @@ func TestReadNativeFrames(t *testing.T) {
}

pixelData, _, err := readNativeFrames(r, &tc.existingData, nil)
if err != tc.expectedError {
if !errors.Is(err, tc.expectedError) {
t.Errorf("TestReadNativeFrames(%v): did not get expected error. got: %v, want: %v", tc.data, err, tc.expectedError)
}

Expand Down

0 comments on commit a6f29f2

Please sign in to comment.