Former/face_scan_test.go

266 lines
6.8 KiB
Go

package main
import (
"image"
"image/color"
"math"
"testing"
)
func TestOtsuThreshold(t *testing.T) {
// Bimodal image: left half dark (~50), right half light (~200)
img := image.NewGray(image.Rect(0, 0, 200, 200))
for y := 0; y < 200; y++ {
for x := 0; x < 100; x++ {
v := uint8(40 + (x+y)%20) // dark cluster around 40-59
img.SetGray(x, y, color.Gray{Y: v})
}
for x := 100; x < 200; x++ {
v := uint8(190 + (x+y)%20) // light cluster around 190-209
img.SetGray(x, y, color.Gray{Y: v})
}
}
thresh := otsuThreshold(img)
if thresh < 55 || thresh > 195 {
t.Errorf("otsu threshold %d should separate dark cluster (~40-59) from light cluster (~190-209)", thresh)
}
}
func TestEstimateDPI(t *testing.T) {
// Letter paper at 300 DPI: 215.9mm = 8.5in -> 2550px, 279.4mm = 11in -> 3300px
dpi := estimateDPI(2550, 3300, 215.9, 279.4)
if math.Abs(dpi-300) > 1 {
t.Errorf("expected ~300 DPI, got %.1f", dpi)
}
}
func TestMmPxConversion(t *testing.T) {
px := mmToPx(25.4, 300)
if math.Abs(px-300) > 0.1 {
t.Errorf("25.4mm at 300DPI should be 300px, got %.1f", px)
}
mm := pxToMM(300, 300)
if math.Abs(mm-25.4) > 0.1 {
t.Errorf("300px at 300DPI should be 25.4mm, got %.1f", mm)
}
}
func TestAffineIdentity(t *testing.T) {
src := [4][2]float64{{0, 0}, {100, 0}, {0, 100}, {100, 100}}
dst := [4][2]float64{{0, 0}, {100, 0}, {0, 100}, {100, 100}}
xform := computeAffine(src, dst)
for _, pt := range src {
ox, oy := xform.transform(pt[0], pt[1])
if math.Abs(ox-pt[0]) > 0.01 || math.Abs(oy-pt[1]) > 0.01 {
t.Errorf("identity transform failed: (%.1f,%.1f) -> (%.4f,%.4f)", pt[0], pt[1], ox, oy)
}
}
}
func TestAffineScaleTranslate(t *testing.T) {
// Pixels at corners of a 1000x1000 image -> mm positions
src := [4][2]float64{{100, 100}, {900, 100}, {100, 900}, {900, 900}}
dst := [4][2]float64{{10, 10}, {90, 10}, {10, 90}, {90, 90}}
xform := computeAffine(src, dst)
ox, oy := xform.transform(500, 500)
if math.Abs(ox-50) > 0.1 || math.Abs(oy-50) > 0.1 {
t.Errorf("center should map to (50,50), got (%.2f,%.2f)", ox, oy)
}
}
func TestAffineInverse(t *testing.T) {
src := [4][2]float64{{50, 50}, {500, 50}, {50, 700}, {500, 700}}
dst := [4][2]float64{{15, 15}, {200, 15}, {15, 280}, {200, 280}}
xform := computeAffine(src, dst)
inv := invertAffine(xform)
for i, pt := range src {
mx, my := xform.transform(pt[0], pt[1])
rx, ry := inv.transform(mx, my)
if math.Abs(rx-pt[0]) > 0.1 || math.Abs(ry-pt[1]) > 0.1 {
t.Errorf("roundtrip %d: (%.1f,%.1f) -> (%.2f,%.2f) -> (%.2f,%.2f)", i, pt[0], pt[1], mx, my, rx, ry)
}
}
}
func TestDouglasPeucker(t *testing.T) {
// Square with many intermediate points on each edge
var pts [][2]float64
for i := 0; i <= 10; i++ {
pts = append(pts, [2]float64{float64(i) * 10, 0})
}
for i := 1; i <= 10; i++ {
pts = append(pts, [2]float64{100, float64(i) * 10})
}
for i := 9; i >= 0; i-- {
pts = append(pts, [2]float64{float64(i) * 10, 100})
}
for i := 9; i >= 1; i-- {
pts = append(pts, [2]float64{0, float64(i) * 10})
}
simplified := douglasPeucker(pts, 1.0)
if len(simplified) > 6 {
t.Errorf("square should simplify to ~4-5 points, got %d", len(simplified))
}
if len(simplified) < 4 {
t.Errorf("square should have at least 4 points, got %d", len(simplified))
}
}
func TestMorphOpen(t *testing.T) {
// Create a binary mask with a thin line (1px) and a thick blob (10px diameter)
w, h := 100, 100
mask := make([]bool, w*h)
// Thin horizontal line at y=20
for x := 10; x < 90; x++ {
mask[20*w+x] = true
}
// Thick filled circle at (50,60) r=8
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
dx := float64(x) - 50
dy := float64(y) - 60
if dx*dx+dy*dy <= 64 {
mask[y*w+x] = true
}
}
}
opened := morphOpen(mask, w, h, 3)
// Thin line should be removed
thinRemoved := true
for x := 20; x < 80; x++ {
if opened[20*w+x] {
thinRemoved = false
break
}
}
if !thinRemoved {
t.Error("morphological opening should remove 1px thin line with radius 3")
}
// Thick circle should survive (at least partially)
thickSurvived := false
for y := 55; y < 65; y++ {
for x := 45; x < 55; x++ {
if opened[y*w+x] {
thickSurvived = true
break
}
}
}
if !thickSurvived {
t.Error("morphological opening should preserve thick circle")
}
}
func TestFindBlobs(t *testing.T) {
w, h := 50, 50
mask := make([]bool, w*h)
// Blob 1: small circle at (10,10)
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if (x-10)*(x-10)+(y-10)*(y-10) <= 9 {
mask[y*w+x] = true
}
}
}
// Blob 2: larger circle at (35,35)
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if (x-35)*(x-35)+(y-35)*(y-35) <= 36 {
mask[y*w+x] = true
}
}
}
blobs := findBlobs(mask, w, h)
if len(blobs) != 2 {
t.Fatalf("expected 2 blobs, got %d", len(blobs))
}
// Verify centroids are near expected positions
for _, b := range blobs {
near10 := math.Abs(b.cx-10) < 2 && math.Abs(b.cy-10) < 2
near35 := math.Abs(b.cx-35) < 2 && math.Abs(b.cy-35) < 2
if !near10 && !near35 {
t.Errorf("blob centroid (%.1f,%.1f) not near expected positions", b.cx, b.cy)
}
}
}
func TestContourTracing(t *testing.T) {
// Filled rectangle
w, h := 50, 50
mask := make([]bool, w*h)
for y := 10; y < 40; y++ {
for x := 10; x < 40; x++ {
mask[y*w+x] = true
}
}
contour := traceLargestContour(mask, w, h)
if len(contour) < 4 {
t.Errorf("rectangle contour should have many boundary points, got %d", len(contour))
}
// All contour points should be on the boundary
for _, pt := range contour {
x, y := pt[0], pt[1]
if x < 10 || x >= 40 || y < 10 || y >= 40 {
t.Errorf("contour point (%d,%d) outside rectangle bounds", x, y)
break
}
}
}
func TestAdaptiveThreshold(t *testing.T) {
// Dark lines on light background (mimics pen strokes on paper)
sz := 200
img := image.NewGray(image.Rect(0, 0, sz, sz))
for y := 0; y < sz; y++ {
for x := 0; x < sz; x++ {
img.SetGray(x, y, color.Gray{Y: 220})
}
}
// Thin dark line (5px wide) — like a traced outline
for y := 95; y < 105; y++ {
for x := 40; x < 160; x++ {
img.SetGray(x, y, color.Gray{Y: 30})
}
}
mask := adaptiveThreshold(img, 51, 15)
// Center of dark line should be detected
if !mask[100*sz+100] {
t.Error("center of dark line should be detected")
}
// Corner (light area, far from line) should not be detected
if mask[10*sz+10] {
t.Error("corner (light area) should not be detected")
}
}
func TestPointToSegmentDist(t *testing.T) {
// Point above horizontal segment
d := pointToSegmentDist([2]float64{5, 3}, [2]float64{0, 0}, [2]float64{10, 0})
if math.Abs(d-3) > 0.01 {
t.Errorf("expected distance 3, got %.4f", d)
}
// Point beyond segment end
d = pointToSegmentDist([2]float64{15, 0}, [2]float64{0, 0}, [2]float64{10, 0})
if math.Abs(d-5) > 0.01 {
t.Errorf("expected distance 5, got %.4f", d)
}
}