FloorMaterialAnalyzer/internal/imageproc/statistics.go

100 lines
2.9 KiB
Go
Raw Normal View History

2026-07-27 03:03:51 +00:00
package imageproc
import (
"image"
"math"
"sort"
"materialanalyzer/internal/model"
)
func ExtractCanonicalStatistics(img image.Image, visual model.VisualFeatures, texture model.TextureFeatures) model.CanonicalStatistics {
samples := collectSamples(img, 65000)
stats := model.CanonicalStatistics{
MeanRGB: visual.AverageRGB,
DominantOrientationDeg: texture.PrimaryOrientationDeg,
BrightnessHistogramBins: 16,
GradientHistogramBins: 18,
BrightnessDistribution: make([]float64, 16),
GradientHistogram: make([]float64, 18),
}
if len(samples) == 0 {
return stats
}
rs := make([]int, len(samples))
gs := make([]int, len(samples))
bs := make([]int, len(samples))
var varianceSum float64
for i, s := range samples {
rs[i] = int(math.Round(s.r))
gs[i] = int(math.Round(s.g))
bs[i] = int(math.Round(s.b))
dr := s.r - float64(visual.AverageRGB.R)
dg := s.g - float64(visual.AverageRGB.G)
db := s.b - float64(visual.AverageRGB.B)
varianceSum += (dr*dr + dg*dg + db*db) / 3
bin := min(len(stats.BrightnessDistribution)-1, int((s.lum/255.0)*float64(len(stats.BrightnessDistribution))))
stats.BrightnessDistribution[bin]++
}
sort.Ints(rs)
sort.Ints(gs)
sort.Ints(bs)
mid := len(samples) / 2
stats.MedianRGB = model.RGB{R: rs[mid], G: gs[mid], B: bs[mid]}
stats.ColorVariance = round4(varianceSum / float64(len(samples)) / (255.0 * 255.0))
for i := range stats.BrightnessDistribution {
stats.BrightnessDistribution[i] = round6(stats.BrightnessDistribution[i] / float64(len(samples)))
}
stats.TextureVariance, stats.GradientHistogram = gradientStatistics(img, len(stats.GradientHistogram))
return stats
}
func gradientStatistics(img image.Image, bins int) (float64, []float64) {
small := ResizeToMax(img, 384)
gray, w, h := grayscale(small)
hist := make([]float64, bins)
if w < 3 || h < 3 || bins <= 0 {
return 0, hist
}
var mags []float64
var totalWeight float64
for y := 1; y < h-1; y++ {
for x := 1; x < w-1; x++ {
gx := -int(gray[(y-1)*w+x-1]) + int(gray[(y-1)*w+x+1]) -
2*int(gray[y*w+x-1]) + 2*int(gray[y*w+x+1]) -
int(gray[(y+1)*w+x-1]) + int(gray[(y+1)*w+x+1])
gy := -int(gray[(y-1)*w+x-1]) - 2*int(gray[(y-1)*w+x]) - int(gray[(y-1)*w+x+1]) +
int(gray[(y+1)*w+x-1]) + 2*int(gray[(y+1)*w+x]) + int(gray[(y+1)*w+x+1])
mag := math.Hypot(float64(gx), float64(gy))
mags = append(mags, mag)
angle := math.Atan2(float64(gy), float64(gx)) + math.Pi/2
deg := math.Mod(angle*180/math.Pi+180, 180)
bin := min(bins-1, int(deg/180*float64(bins)))
hist[bin] += mag
totalWeight += mag
}
}
if len(mags) == 0 {
return 0, hist
}
var mean, variance float64
for _, mag := range mags {
mean += mag
}
mean /= float64(len(mags))
for _, mag := range mags {
d := mag - mean
variance += d * d
}
variance = variance / float64(len(mags)) / (1020.0 * 1020.0)
if totalWeight > 0 {
for i := range hist {
hist[i] = round6(hist[i] / totalWeight)
}
}
return round4(variance), hist
}