230 lines
8.2 KiB
Go
230 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"image/jpeg"
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
// lidWhite is a w x h gray image of the lid's white, with a few specks of dust in one patch.
|
|
func lidWhite(w, h int, seed int64) *image.Gray {
|
|
g := image.NewGray(image.Rect(0, 0, w, h))
|
|
draw.Draw(g, g.Bounds(), &image.Uniform{color.Gray{255}}, image.Point{}, draw.Src)
|
|
r := rand.New(rand.NewSource(seed))
|
|
for i := 0; i < 40; i++ {
|
|
g.SetGray(100+r.Intn(w/4), 100+r.Intn(h/4), color.Gray{120}) // all in one patch, so counting them would make a box
|
|
}
|
|
return g
|
|
}
|
|
|
|
func encode(t *testing.T, img image.Image) []byte {
|
|
t.Helper()
|
|
var b bytes.Buffer
|
|
if err := jpeg.Encode(&b, img, &jpeg.Options{Quality: 95}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b.Bytes()
|
|
}
|
|
|
|
// 1440/AC3: the captured receipt, top-left of a 1696x2203 scan at 200 dpi.
|
|
func TestCropFindsTheCapturedReceipt(t *testing.T) {
|
|
c, err := Crop(fixture(t, "scan.jpg"), true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !c.Cropped {
|
|
t.Fatal("cropped = false for a scan with a receipt on it")
|
|
}
|
|
img, err := jpeg.Decode(bytes.NewReader(fixture(t, "scan.jpg")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
box, found := Box(img)
|
|
if !found {
|
|
t.Fatal("no box found")
|
|
}
|
|
if box.Min.X > 10 || box.Min.Y > 10 {
|
|
t.Errorf("box starts at %v, want within 10 px of (0,0)", box.Min)
|
|
}
|
|
if box.Max.X < 630 || box.Max.X > 660 {
|
|
t.Errorf("right edge %d, want 630..660", box.Max.X)
|
|
}
|
|
if box.Max.Y < 1730 || box.Max.Y > 1810 {
|
|
t.Errorf("bottom %d, want 1730..1810", box.Max.Y)
|
|
}
|
|
if c.Width != box.Dx() || c.Height != box.Dy() {
|
|
t.Errorf("cropped image is %dx%d, the box %dx%d", c.Width, c.Height, box.Dx(), box.Dy())
|
|
}
|
|
got, err := jpeg.DecodeConfig(bytes.NewReader(c.JPEG))
|
|
if err != nil || got.Width != box.Dx() || got.Height != box.Dy() {
|
|
t.Errorf("the cropped JPEG is %+v (%v), want %dx%d", got, err, box.Dx(), box.Dy())
|
|
}
|
|
// About 80-84 mm wide at 200 dpi.
|
|
if mm := float64(c.Width) / 200 * 25.4; mm < 80 || mm > 84 {
|
|
t.Errorf("%.1f mm wide, want 80-84", mm)
|
|
}
|
|
}
|
|
|
|
// 1440/AC4: a plain lid with specks has no document.
|
|
func TestCropKeepsAPlainLidWhole(t *testing.T) {
|
|
scan := encode(t, lidWhite(800, 1100, 1))
|
|
c, err := Crop(scan, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Cropped || c.Width != 800 || c.Height != 1100 || !bytes.Equal(c.JPEG, scan) {
|
|
t.Fatalf("a plain lid was cropped: %+v", c)
|
|
}
|
|
}
|
|
|
|
// 1440/AC4: a document that fills the platen is no crop either.
|
|
func TestCropKeepsAFullPlatenDocumentWhole(t *testing.T) {
|
|
g := lidWhite(800, 1100, 2)
|
|
draw.Draw(g, image.Rect(0, 0, 790, 1090), &image.Uniform{color.Gray{200}}, image.Point{}, draw.Src)
|
|
scan := encode(t, g)
|
|
c, err := Crop(scan, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Cropped || c.Width != 800 || c.Height != 1100 {
|
|
t.Fatalf("a document filling the platen was cropped: %+v", c)
|
|
}
|
|
}
|
|
|
|
// 1440/AC4: crop=false keeps the captured scan whole, though it has a document.
|
|
func TestCropOffKeepsTheWholeImage(t *testing.T) {
|
|
scan := fixture(t, "scan.jpg")
|
|
c, err := Crop(scan, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Cropped || c.Width != 1696 || c.Height != 2203 || !bytes.Equal(c.JPEG, scan) {
|
|
t.Fatalf("crop=false cropped: %+v", c)
|
|
}
|
|
}
|
|
|
|
// 1440/AC3: a document away from the origin is found where it is, not assumed in the corner.
|
|
func TestCropFindsADocumentInTheMiddleOfTheGlass(t *testing.T) {
|
|
g := lidWhite(800, 1100, 3)
|
|
draw.Draw(g, image.Rect(200, 300, 500, 700), &image.Uniform{color.Gray{240}}, image.Point{}, draw.Src)
|
|
img, err := jpeg.Decode(bytes.NewReader(encode(t, g)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
box, found := Box(img)
|
|
if !found {
|
|
t.Fatal("no box")
|
|
}
|
|
if box.Min.X < 190 || box.Min.X > 205 || box.Max.X < 495 || box.Max.X > 510 || box.Min.Y < 290 || box.Min.Y > 305 || box.Max.Y < 695 || box.Max.Y > 710 {
|
|
t.Fatalf("box = %v, want about (200,300)-(500,700)", box)
|
|
}
|
|
}
|
|
|
|
// onWhite is a lid-white image of w x h with the rectangles drawn on it in gray (generated, not captured: the
|
|
// thresholds of Boxes are in millimetres, and this is 100 dpi, 1 mm about 4 px).
|
|
func onWhite(t *testing.T, seed int64, rects ...image.Rectangle) []byte {
|
|
t.Helper()
|
|
g := lidWhite(800, 1100, seed)
|
|
for _, r := range rects {
|
|
draw.Draw(g, r, &image.Uniform{color.Gray{200}}, image.Point{}, draw.Src)
|
|
}
|
|
return encode(t, g)
|
|
}
|
|
|
|
// 1528: documents lying apart on the white are each cut out, in reading order; one document, as before, is one.
|
|
func TestCropAllCutsEachDocumentApartFromTheOthers(t *testing.T) {
|
|
a := image.Rect(100, 100, 300, 300) // 51 mm square
|
|
b := image.Rect(450, 100, 650, 400) // 38 mm right of a
|
|
c := image.Rect(100, 600, 400, 900)
|
|
for name, tc := range map[string]struct {
|
|
rects []image.Rectangle
|
|
want []image.Rectangle
|
|
}{
|
|
"one": {[]image.Rectangle{a}, nil},
|
|
"two": {[]image.Rectangle{b, a}, []image.Rectangle{a, b}},
|
|
"three": {[]image.Rectangle{c, b, a}, []image.Rectangle{a, b, c}},
|
|
} {
|
|
got, err := CropAll(onWhite(t, 5, tc.rects...), true, 100)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tc.want == nil {
|
|
one, _ := Crop(onWhite(t, 5, a), true)
|
|
if len(got) != 1 || !bytes.Equal(got[0].JPEG, one.JPEG) {
|
|
t.Errorf("%s: one document is not Crop's answer", name)
|
|
}
|
|
continue
|
|
}
|
|
if len(got) != len(tc.want) {
|
|
t.Fatalf("%s: %d documents, want %d", name, len(got), len(tc.want))
|
|
}
|
|
for i, w := range tc.want {
|
|
// about the rectangle, within the margin and a block of rounding
|
|
if !got[i].Cropped || got[i].Width < w.Dx() || got[i].Width > w.Dx()+2*margin+2*4 || got[i].Height < w.Dy() || got[i].Height > w.Dy()+2*margin+2*4 {
|
|
t.Errorf("%s: document %d is %dx%d, want about %dx%d", name, i, got[i].Width, got[i].Height, w.Dx(), w.Dy())
|
|
}
|
|
cfg, err := jpeg.DecodeConfig(bytes.NewReader(got[i].JPEG))
|
|
if err != nil || cfg.Width != got[i].Width {
|
|
t.Errorf("%s: document %d JPEG %+v %v", name, i, cfg, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1528: one document with a white area inside it is not split: a frame with an island inside it, a sheet with a
|
|
// white band across it narrower than the gap, and specks of dust beside it.
|
|
func TestCropAllDoesNotSplitOneDocumentWithWhiteInsideIt(t *testing.T) {
|
|
g := lidWhite(800, 1100, 6)
|
|
frame := image.Rect(100, 100, 600, 800)
|
|
draw.Draw(g, frame, &image.Uniform{color.Gray{200}}, image.Point{}, draw.Src)
|
|
draw.Draw(g, frame.Inset(50), &image.Uniform{color.Gray{255}}, image.Point{}, draw.Src) // white inside the frame
|
|
draw.Draw(g, image.Rect(300, 400, 400, 500), &image.Uniform{color.Gray{200}}, image.Point{}, draw.Src) // an island in it
|
|
got, err := CropAll(encode(t, g), true, 100)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("a frame with an island inside was cut into %d documents", len(got))
|
|
}
|
|
|
|
g = lidWhite(800, 1100, 7)
|
|
draw.Draw(g, image.Rect(100, 100, 500, 500), &image.Uniform{color.Gray{200}}, image.Point{}, draw.Src)
|
|
draw.Draw(g, image.Rect(100, 530, 500, 900), &image.Uniform{color.Gray{200}}, image.Point{}, draw.Src) // 30 px = 7.6 mm of white between
|
|
got, err = CropAll(encode(t, g), true, 100)
|
|
if err != nil || len(got) != 1 {
|
|
t.Fatalf("a sheet with a narrow white band across it: %d documents, %v", len(got), err)
|
|
}
|
|
}
|
|
|
|
// 1528: a speck smaller than the minimum is no document, and crop off files the whole platen as one.
|
|
func TestCropAllIgnoresSpecksAndCropOffIsOneWholeImage(t *testing.T) {
|
|
scan := onWhite(t, 8, image.Rect(100, 100, 300, 300), image.Rect(600, 800, 620, 815)) // 5 x 4 mm stain
|
|
got, err := CropAll(scan, true, 100)
|
|
if err != nil || len(got) != 1 {
|
|
t.Fatalf("a stain made %d documents (%v)", len(got), err)
|
|
}
|
|
two := onWhite(t, 9, image.Rect(100, 100, 300, 300), image.Rect(450, 100, 650, 400))
|
|
got, err = CropAll(two, false, 100)
|
|
if err != nil || len(got) != 1 || got[0].Cropped {
|
|
t.Fatalf("crop off: %d documents, cropped %v", len(got), got[0].Cropped)
|
|
}
|
|
}
|
|
|
|
// 1528: the captured scan (one receipt) is one document, the same bytes Crop gives.
|
|
func TestCropAllOfTheCapturedReceiptIsCropsAnswer(t *testing.T) {
|
|
scan := fixture(t, "scan.jpg")
|
|
got, err := CropAll(scan, true, 200)
|
|
one, _ := Crop(scan, true)
|
|
if err != nil || len(got) != 1 || !bytes.Equal(got[0].JPEG, one.JPEG) {
|
|
t.Fatalf("%d documents (%v), want Crop's one", len(got), err)
|
|
}
|
|
img, _ := jpeg.Decode(bytes.NewReader(scan))
|
|
if boxes, _ := Boxes(img, 200); len(boxes) != 1 {
|
|
t.Errorf("the receipt is %d regions: %v", len(boxes), boxes)
|
|
}
|
|
}
|