Skip to content

Commit

Permalink
Convert to *image.RGBA for faster JPEG encoding
Browse files Browse the repository at this point in the history
related to issue #7
  • Loading branch information
stapelberg committed May 4, 2017
1 parent 2f7d475 commit e8ab7d2
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 40 deletions.
49 changes: 15 additions & 34 deletions internal/fss500/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,24 @@ package fss500

import (
"image"
"image/color"
)

// Image represents one scanned DIN A4 sized page in 600 dpi
// (i.e. 4960x7016 pixels).
type Image struct {
Pixels []byte
}

// Bounds returns the domain for which At can return non-zero color.
func (m *Image) Bounds() image.Rectangle {
return image.Rect(0, 0, 4960, 7016)
}

// ColorModel returns the Image's color model.
func (m *Image) ColorModel() color.Model {
return color.RGBAModel
}

// RGBAAt returns the color of the pixel at (x, y) as color.RGBA.
func (m *Image) RGBAAt(x, y int) color.RGBA {
if x < 0 || x >= 4960 || y < 0 || y >= 7016 {
return color.RGBA{}
}

// ToRGBA copies one scanned DIN A4 sized page in 600 dpi
// (i.e. 4960x7016 pixels) into an *image.RGBA.
func ToRGBA(pixels []byte) *image.RGBA {
// TODO(later): do that conversion using NEON instructions: 3-element vector load, 4-element vector store
const bytesPerLine = 3 * 4960
const channels = 3
i := bytesPerLine*y + channels*x
return color.RGBA{
R: uint8(m.Pixels[i+0]),
G: uint8(m.Pixels[i+1]),
B: uint8(m.Pixels[i+2]),
A: uint8(0xff), // opaque
img := image.NewRGBA(image.Rect(0, 0, 4960, 7016))
for y := 0; y < 7016; y++ {
for x := 0; x < 4960; x++ {
i := bytesPerLine*y + channels*x
offset := y*4*4960 + x*4
img.Pix[offset+0] = pixels[i+0]
img.Pix[offset+1] = pixels[i+1]
img.Pix[offset+2] = pixels[i+2]
img.Pix[offset+3] = 0xff
}
}
}

// At returns the color of the pixel at (x, y).
func (m *Image) At(x, y int) color.Color {
return m.RGBAAt(x, y)
return img
}
22 changes: 16 additions & 6 deletions local.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,14 @@ func scan(tr trace.Trace, dev io.ReadWriter) error {
cnt++
cnt := cnt // copy
eg.Go(func() error {
m := &fss500.Image{Pixels: pixels[side]}
// Convert the pixel data to *image.RGBA, for which image/jpeg has a specialized version.
// This brings down JPEG encoding times from 30s to 15s.
start := time.Now()
m := fss500.ToRGBA(pixels[side])
tr.LazyPrintf("converted to *image.RGBA in %v", time.Since(start))

fn := filepath.Join(scanDir, fmt.Sprintf("page%d.jpg", cnt))
tr.LazyPrintf("saving as JPG in %q", fn)
start = time.Now()
o, err := os.Create(fn)
if err != nil {
return err
Expand All @@ -199,27 +204,32 @@ func scan(tr trace.Trace, dev io.ReadWriter) error {
return err
}
createCompleteMarker(resp.User, relName, "scan")
tr.LazyPrintf("saved")
tr.LazyPrintf("saved to JPG in %q in %v", fn, time.Since(start))
return nil
})

eg.Go(func() error {
// binarize (takes 3s on a Raspberry Pi 3)
start := time.Now()
bin, whitePct := binarizeFSS500(pixels[side])
blank := whitePct > 0.99
tr.LazyPrintf("white percentage of page %d is %f, blank = %v", cnt, whitePct, blank)
tr.LazyPrintf("white percentage of page %d is %f, blank = %v (binarized in %v)", cnt, whitePct, blank, time.Since(start))
if blank {
return nil
}

// rotate (takes 5s on a Raspberry Pi 3)
start = time.Now()
// rotate (takes 7.9s on a Raspberry Pi 3)
bin = rotate180(bin)
tr.LazyPrintf("rotated in %v", time.Since(start))

// compress (takes 22s on a Raspberry Pi 3)
// compress (takes 3.4s on a Raspberry Pi 3)
var buf bytes.Buffer
start = time.Now()
if err := g3.NewEncoder(&buf).Encode(bin); err != nil {
return err
}
tr.LazyPrintf("compressed in %v", time.Since(start))
// Prepend (!) the page — the ScanSnap iX500’s
// document feeder scans the last page first, so we
// need to reverse the order.
Expand Down

0 comments on commit e8ab7d2

Please sign in to comment.