Files
mesh-catalog/modules/brother/cmd/brother/crop_test.go
T

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)
}
}