FloorMaterialAnalyzer/internal/imageproc/prepare.go

125 lines
3.5 KiB
Go
Raw Normal View History

2026-07-27 03:03:51 +00:00
package imageproc
import (
"fmt"
"image"
"math"
"net/url"
"strings"
)
type AnalysisImage struct {
Image image.Image
OriginalWidth int
OriginalHeight int
Region image.Rectangle
Strategy string
RoomLike bool
RoomLikeConfidence string
RoomLikeReason string
}
func PrepareAnalysisImage(src image.Image, source string) AnalysisImage {
b := src.Bounds()
roomLike, confidence, reason := DetectRoomLike(src, source)
region := b
strategy := "full_texture"
if roomLike {
top := b.Min.Y + int(float64(b.Dy())*0.45)
left := b.Min.X + int(float64(b.Dx())*0.08)
right := b.Max.X - int(float64(b.Dx())*0.08)
region = image.Rect(left, top, right, b.Max.Y).Intersect(b)
strategy = "room_like_lower_floor_crop"
}
return AnalysisImage{
Image: Crop(src, region), OriginalWidth: b.Dx(), OriginalHeight: b.Dy(),
Region: region, Strategy: strategy, RoomLike: roomLike,
RoomLikeConfidence: confidence, RoomLikeReason: reason,
}
}
func DetectRoomLike(src image.Image, source string) (bool, string, string) {
b := src.Bounds()
w, h := b.Dx(), b.Dy()
if w == 0 || h == 0 {
return false, "", ""
}
urlHint, hint := roomSceneHint(source)
ratio := float64(w) / float64(h)
roomGeometry := false
if urlHint {
top := image.Rect(b.Min.X, b.Min.Y, b.Max.X, b.Min.Y+h/2)
bottom := image.Rect(b.Min.X, b.Min.Y+h/2, b.Max.X, b.Max.Y)
topEdge, topVar := regionEdgeAndVariance(src, top)
bottomEdge, bottomVar := regionEdgeAndVariance(src, bottom)
roomGeometry = (ratio > 1.28 || ratio < 0.78) && bottomEdge > topEdge*1.25 && bottomVar > topVar*1.10
}
switch {
case urlHint && roomGeometry:
return true, "high", fmt.Sprintf("source image path has %q token plus room-like edge split (ratio %.2f)", hint, ratio)
case urlHint:
return true, "medium", fmt.Sprintf("source image path contains %q room-scene token", hint)
default:
return false, "", ""
}
}
func roomSceneHint(source string) (bool, string) {
candidate := source
if parsed, err := url.Parse(source); err == nil && parsed.Path != "" {
candidate = parsed.Path
}
if decoded, err := url.PathUnescape(candidate); err == nil {
candidate = decoded
}
for _, token := range sceneTokens(strings.ToLower(candidate)) {
switch token {
case "room", "rooms", "lifestyle", "installed", "installation", "gallery", "ambient", "beauty":
return true, token
}
}
return false, ""
}
func sceneTokens(s string) []string {
return strings.FieldsFunc(s, func(r rune) bool {
return !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'))
})
}
func regionEdgeAndVariance(src image.Image, rect image.Rectangle) (float64, float64) {
rect = rect.Intersect(src.Bounds())
if rect.Dx() < 3 || rect.Dy() < 3 {
return 0, 0
}
step := int(math.Sqrt(float64(rect.Dx()*rect.Dy())/12000.0)) + 1
var n int
var sum, sumSq float64
var edges int
for y := rect.Min.Y + step; y < rect.Max.Y-step; y += step {
for x := rect.Min.X + step; x < rect.Max.X-step; x += step {
l := luminanceAt(src, x, y)
lx := luminanceAt(src, x+step, y) - luminanceAt(src, x-step, y)
ly := luminanceAt(src, x, y+step) - luminanceAt(src, x, y-step)
if math.Hypot(lx, ly) > 26 {
edges++
}
sum += l
sumSq += l * l
n++
}
}
if n == 0 {
return 0, 0
}
mean := sum / float64(n)
variance := sumSq/float64(n) - mean*mean
return float64(edges) / float64(n), variance
}
func luminanceAt(src image.Image, x, y int) float64 {
r, g, b := rgba8(src.At(x, y))
return 0.2126*float64(r) + 0.7152*float64(g) + 0.0722*float64(b)
}