Skip to content

Commit ebb720f

Browse files
committed
works pretty good rn ~80% confidence
0 parents  commit ebb720f

11 files changed

Lines changed: 330 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

bg-1.png

91.6 KB
Loading

cropped_slice.png

4.93 KB
Loading

edge_bg.png

22.5 KB
Loading

edge_slice.png

1.84 KB
Loading

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module geeTestSolver
2+
3+
go 1.25
4+
5+
require github.com/anthonynsimon/bild v0.14.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
2+
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=

main.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"image"
6+
"image/color"
7+
"image/draw"
8+
"image/png"
9+
"log"
10+
"math"
11+
"os"
12+
13+
"github.com/anthonynsimon/bild/blur"
14+
)
15+
16+
func loadImage(path string) (image.Image, error) {
17+
file, err := os.Open(path)
18+
if err != nil {
19+
return nil, err
20+
}
21+
defer file.Close()
22+
23+
img, _, err := image.Decode(file)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return img, nil
28+
}
29+
30+
func saveImage(path string, img image.Image) {
31+
f, err := os.Create(path)
32+
if err != nil {
33+
log.Printf("Failed to create %s: %v", path, err)
34+
return
35+
}
36+
defer f.Close()
37+
if err := png.Encode(f, img); err != nil {
38+
log.Printf("Failed to encode %s: %v", path, err)
39+
}
40+
fmt.Printf("Saved %s\n", path)
41+
}
42+
43+
func abs(x int) int {
44+
if x < 0 {
45+
return -x
46+
}
47+
return x
48+
}
49+
50+
// CropToContent removes borders based on the top-left pixel color
51+
func CropToContent(img *image.RGBA) *image.RGBA {
52+
bounds := img.Bounds()
53+
minX, minY := bounds.Max.X, bounds.Max.Y
54+
maxX, maxY := bounds.Min.X, bounds.Min.Y
55+
found := false
56+
57+
bgC := img.At(bounds.Min.X, bounds.Min.Y)
58+
br, bg_g, bb, ba := bgC.RGBA()
59+
60+
isBackground := func(x, y int) bool {
61+
c := img.At(x, y)
62+
r, g, b, a := c.RGBA()
63+
diff := abs(int(r)-int(br)) + abs(int(g)-int(bg_g)) + abs(int(b)-int(bb)) + abs(int(a)-int(ba))
64+
return diff < 3000
65+
}
66+
67+
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
68+
for x := bounds.Min.X; x < bounds.Max.X; x++ {
69+
if !isBackground(x, y) {
70+
if x < minX {
71+
minX = x
72+
}
73+
if x > maxX {
74+
maxX = x
75+
}
76+
if y < minY {
77+
minY = y
78+
}
79+
if y > maxY {
80+
maxY = y
81+
}
82+
found = true
83+
}
84+
}
85+
}
86+
87+
if !found {
88+
return img
89+
}
90+
91+
rect := image.Rect(minX, minY, maxX+1, maxY+1)
92+
newImg := image.NewRGBA(image.Rect(0, 0, rect.Dx(), rect.Dy()))
93+
draw.Draw(newImg, newImg.Bounds(), img, rect.Min, draw.Src)
94+
return newImg
95+
}
96+
97+
// SobelEdgeDetection calculates gradient magnitude
98+
func SobelEdgeDetection(img image.Image) *image.Gray {
99+
bounds := img.Bounds()
100+
gray := image.NewGray(bounds)
101+
draw.Draw(gray, bounds, img, bounds.Min, draw.Src)
102+
103+
output := image.NewGray(bounds)
104+
105+
for y := bounds.Min.Y + 1; y < bounds.Max.Y-1; y++ {
106+
for x := bounds.Min.X + 1; x < bounds.Max.X-1; x++ {
107+
gx := int(gray.GrayAt(x+1, y).Y) - int(gray.GrayAt(x-1, y).Y)
108+
gy := int(gray.GrayAt(x, y+1).Y) - int(gray.GrayAt(x, y-1).Y)
109+
110+
mag := math.Sqrt(float64(gx*gx + gy*gy))
111+
if mag > 255 {
112+
mag = 255
113+
}
114+
output.SetGray(x, y, color.Gray{Y: uint8(mag)})
115+
}
116+
}
117+
return output
118+
}
119+
120+
func calculateMaskedNCC(background, puzzlePiece *image.Gray, mask *image.Alpha, offsetX, offsetY int) float64 {
121+
var sum1, sum2, sum1Sq, sum2Sq, sum12 float64
122+
n := 0
123+
124+
bounds := puzzlePiece.Bounds()
125+
for py := 0; py < bounds.Dy(); py++ {
126+
for px := 0; px < bounds.Dx(); px++ {
127+
// Check mask (if pixel is transparent/background, skip it)
128+
if mask.AlphaAt(bounds.Min.X+px, bounds.Min.Y+py).A == 0 {
129+
continue
130+
}
131+
132+
v1 := float64(puzzlePiece.GrayAt(bounds.Min.X+px, bounds.Min.Y+py).Y)
133+
v2 := float64(background.GrayAt(offsetX+px, offsetY+py).Y)
134+
135+
sum1 += v1
136+
sum2 += v2
137+
sum1Sq += v1 * v1
138+
sum2Sq += v2 * v2
139+
sum12 += v1 * v2
140+
n++
141+
}
142+
}
143+
144+
if n == 0 {
145+
return 0
146+
}
147+
148+
denom := math.Sqrt((sum1Sq - (sum1 * sum1 / float64(n))) * (sum2Sq - (sum2 * sum2 / float64(n))))
149+
if denom == 0 {
150+
return 0
151+
}
152+
return (sum12 - (sum1 * sum2 / float64(n))) / denom
153+
}
154+
155+
func FindBestMatchMasked(background, puzzlePiece *image.Gray, mask *image.Alpha) (image.Point, float64) {
156+
bestScore := -1.0
157+
var bestPosition image.Point
158+
159+
// Optimization: Skip edges where the piece doesn't fit
160+
for y := 0; y <= background.Bounds().Dy()-puzzlePiece.Bounds().Dy(); y++ {
161+
for x := 0; x <= background.Bounds().Dx()-puzzlePiece.Bounds().Dx(); x++ {
162+
score := calculateMaskedNCC(background, puzzlePiece, mask, x, y)
163+
if score > bestScore {
164+
bestScore = score
165+
bestPosition = image.Point{X: x, Y: y}
166+
// fmt.Printf("Best score: %v x %v y %v\n", bestScore, x, y)
167+
}
168+
}
169+
}
170+
return bestPosition, bestScore
171+
}
172+
173+
func ExtractMask(img *image.RGBA) *image.Alpha {
174+
bounds := img.Bounds()
175+
mask := image.NewAlpha(bounds)
176+
177+
br, bg, bb, ba := img.At(bounds.Min.X, bounds.Min.Y).RGBA()
178+
179+
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
180+
for x := bounds.Min.X; x < bounds.Max.X; x++ {
181+
r, g, b, a := img.At(x, y).RGBA()
182+
diff := abs(int(r)-int(br)) + abs(int(g)-int(bg)) + abs(int(b)-int(bb)) + abs(int(a)-int(ba))
183+
if diff < 3000 {
184+
mask.SetAlpha(x, y, color.Alpha{A: 0}) // Mask out
185+
} else {
186+
mask.SetAlpha(x, y, color.Alpha{A: 255}) // Keep
187+
}
188+
}
189+
}
190+
return mask
191+
}
192+
193+
func main() {
194+
bgPath := "bg-1.png"
195+
slicePath := "slice-1.png"
196+
197+
bgImg, err := loadImage(bgPath)
198+
if err != nil {
199+
log.Fatalf("Failed to load background image: %v", err)
200+
}
201+
202+
sliceImg, err := loadImage(slicePath)
203+
if err != nil {
204+
log.Fatalf("Failed to load slice image: %v", err)
205+
}
206+
207+
bgRGBA, err := ConvertImageToRGBA(bgImg)
208+
if err != nil {
209+
log.Fatalf("Failed to convert background to RGBA: %v", err)
210+
}
211+
212+
sliceRGBA, err := ConvertImageToRGBA(sliceImg)
213+
if err != nil {
214+
log.Fatalf("Failed to convert slice to RGBA: %v", err)
215+
}
216+
217+
// Crop the slice
218+
sliceRGBA = CropToContent(sliceRGBA)
219+
saveImage("cropped_slice.png", sliceRGBA)
220+
221+
// Extract Mask (for NCC)
222+
mask := ExtractMask(sliceRGBA)
223+
224+
// Apply Gaussian Blur (to reduce noise)
225+
// Radius 2.0, Sigma 1.0
226+
bgBlurred := blur.Gaussian(bgRGBA, 2.0)
227+
sliceBlurred := blur.Gaussian(sliceRGBA, 2.0)
228+
229+
// Apply Sobel Edge Detection
230+
bgGray := SobelEdgeDetection(bgBlurred)
231+
sliceGray := SobelEdgeDetection(sliceBlurred)
232+
233+
saveImage("edge_bg.png", bgGray)
234+
saveImage("edge_slice.png", sliceGray)
235+
236+
// Find best match with Mask
237+
pos, score := FindBestMatchMasked(bgGray, sliceGray, mask)
238+
239+
fmt.Printf("Best match found at: %v with score: %f\n", pos, score)
240+
241+
// Draw result
242+
rectColor := color.RGBA{255, 0, 0, 255}
243+
pieceBounds := sliceRGBA.Bounds()
244+
width := pieceBounds.Dx()
245+
height := pieceBounds.Dy()
246+
247+
for x := 0; x < width; x++ {
248+
bgRGBA.Set(pos.X+x, pos.Y, rectColor)
249+
bgRGBA.Set(pos.X+x, pos.Y+height-1, rectColor)
250+
}
251+
for y := 0; y < height; y++ {
252+
bgRGBA.Set(pos.X, pos.Y+y, rectColor)
253+
bgRGBA.Set(pos.X+width-1, pos.Y+y, rectColor)
254+
}
255+
256+
saveImage("result.png", bgRGBA)
257+
}

result.png

85.7 KB
Loading

slice-1.png

7.17 KB
Loading

0 commit comments

Comments
 (0)