100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
|
|
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
|
||
|
|
}
|