FloorMaterialAnalyzer/internal/service/semantic.go
2026-07-27 11:03:51 +08:00

275 lines
7.3 KiB
Go

package service
import (
"fmt"
"math"
"strings"
"materialanalyzer/internal/model"
)
func BuildSemantic(p model.Product, visual model.VisualFeatures, texture model.TextureFeatures) model.SemanticFeatures {
family := MaterialFamily(p.Category, p.Material)
color := colorFamily(visual.DominantRGB)
finish := strings.TrimSpace(p.Finish)
if finish == "" {
finish = inferFinish(visual, texture)
}
variation := variationLevel(visual, texture)
style := visualStyle(family, p, visual)
grain := grainLabel(family, p, texture)
description := fmt.Sprintf("%s %s %s flooring with %s finish, %s visual variation, and %s texture direction.",
color, strings.ToLower(style), cleanMaterialLabel(p), strings.ToLower(finish), strings.ToLower(variation), strings.ToLower(grain))
tags := []string{family, color, style, finish, variation}
if p.ColorTone != "" {
tags = append(tags, p.ColorTone)
}
if p.Material != "" {
tags = append(tags, p.Material)
}
return model.SemanticFeatures{
Provider: "local_rules_v2",
Model: "rule_based",
Description: description,
MaterialType: family,
SurfaceFinish: finish,
GrainType: grain,
ColorFamily: color,
VisualStyle: style,
Variation: variation,
GlossLevel: glossLevel(finish, visual),
Grain: grain,
Tags: compactStrings(tags),
Confidence: 0.72,
FieldConfidence: map[string]float64{
"description": 0.68,
"material_type": 0.85,
"surface_finish": confidenceFromSource(p.Finish),
"grain_type": 0.62,
"color_family": 0.88,
"visual_style": 0.72,
"variation": 0.74,
"gloss_level": 0.65,
},
}
}
func MaterialFamily(category, material string) string {
cat := strings.ToLower(category)
mat := strings.ToLower(material)
text := cat + " " + mat
switch {
case containsAnyText(text, "spc", "lvp", "wpc", "luxury vinyl", "vinyl", "rigid core", "pvc"):
return "vinyl"
case containsAnyText(text, "porcelain", "ceramic", "tile"):
if containsAnyText(mat, "stone", "marble", "travertine", "slate", "limestone", "granite", "terrazzo") {
return "stone_tile"
}
return "tile"
case containsAnyText(text, "stone", "marble", "travertine", "slate", "limestone"):
return "stone_tile"
case containsAnyText(text, "hardwood", "engineered", "laminate", "oak", "maple", "hickory", "walnut", "pine", "birch", "cherry", "acacia", "bamboo"):
return "wood"
default:
return "generic"
}
}
func cleanMaterialLabel(p model.Product) string {
for _, candidate := range []string{p.Material, p.Category, "material"} {
candidate = strings.TrimSpace(candidate)
if candidate != "" {
return candidate
}
}
return "material"
}
func colorFamily(rgb model.RGB) string {
r, g, b := float64(rgb.R), float64(rgb.G), float64(rgb.B)
maxV := math.Max(r, math.Max(g, b))
minV := math.Min(r, math.Min(g, b))
brightness := maxV / 255.0
chroma := (maxV - minV) / 255.0
if brightness < 0.18 {
return "Black"
}
if brightness > 0.86 && chroma < 0.10 {
return "White"
}
if chroma < 0.08 {
if brightness < 0.42 {
return "Dark Gray"
}
if brightness > 0.72 {
return "Light Gray"
}
return "Gray"
}
if r > g*1.08 && g > b*1.05 {
if brightness > 0.65 {
return "Honey"
}
return "Brown"
}
if r > 150 && g > 125 && b > 95 && math.Abs(r-g) < 45 {
return "Beige"
}
if b > r && b > g {
return "Cool Gray"
}
return "Natural"
}
func inferFinish(visual model.VisualFeatures, texture model.TextureFeatures) string {
if visual.Contrast < 0.12 && texture.TextureFrequency < 0.18 {
return "Matte"
}
if visual.Brightness > 0.72 && texture.EdgeDensity < 0.08 {
return "Satin"
}
return "Textured"
}
func glossLevel(finish string, visual model.VisualFeatures) string {
lower := strings.ToLower(finish)
switch {
case strings.Contains(lower, "gloss"):
return "High"
case strings.Contains(lower, "satin"):
return "Medium"
case strings.Contains(lower, "matte"):
return "Low"
case visual.Brightness > 0.72 && visual.Contrast < 0.12:
return "Medium"
default:
return "Low"
}
}
func confidenceFromSource(v string) float64 {
if strings.TrimSpace(v) == "" {
return 0.55
}
return 0.88
}
func variationLevel(visual model.VisualFeatures, texture model.TextureFeatures) string {
colorDistance := 0.0
if visual.DominantRGB != visual.SecondaryRGB {
dr := float64(visual.DominantRGB.R - visual.SecondaryRGB.R)
dg := float64(visual.DominantRGB.G - visual.SecondaryRGB.G)
db := float64(visual.DominantRGB.B - visual.SecondaryRGB.B)
colorDistance = math.Sqrt(dr*dr+dg*dg+db*db) / 441.67
}
score := visual.Contrast*0.45 + texture.EdgeDensity*0.35 + colorDistance*0.20
switch {
case score > 0.20 || texture.Entropy > 7.35:
return "High"
case score > 0.11 || texture.Entropy > 6.55:
return "Medium"
default:
return "Low"
}
}
func visualStyle(family string, p model.Product, visual model.VisualFeatures) string {
text := strings.ToLower(p.StyleName + " " + p.Material + " " + p.Category + " " + p.ColorTone)
switch family {
case "wood", "vinyl":
if containsAnyText(text, "weathered", "distressed", "reclaimed", "rustic") {
return "Rustic"
}
if containsAnyText(text, "oak", "hickory", "maple", "pine", "walnut", "cherry") {
return "Natural Wood"
}
case "tile", "stone_tile":
if containsAnyText(text, "oak", "hickory", "maple", "pine", "walnut", "wood") {
return "Wood Look Tile"
}
if containsAnyText(text, "marble") {
return "Marble"
}
if containsAnyText(text, "slate", "travertine", "limestone", "stone") {
return "Natural Stone"
}
return "Contemporary Tile"
}
if visual.Saturation < 0.12 {
return "Neutral"
}
return "Natural"
}
func grainLabel(family string, p model.Product, texture model.TextureFeatures) string {
if family == "tile" || family == "stone_tile" {
text := strings.ToLower(p.Material + " " + p.StyleName)
if containsAnyText(text, "oak", "hickory", "maple", "pine", "walnut", "wood") {
return orientationLabel(texture.PrimaryOrientationDeg) + " " + inferSpecies(p)
}
if texture.OrientationVariance < 0.38 {
return orientationLabel(texture.PrimaryOrientationDeg) + " Vein"
}
return "Mixed Vein"
}
species := inferSpecies(p)
direction := orientationLabel(texture.PrimaryOrientationDeg)
if texture.OrientationVariance > 0.70 {
return "Mixed " + species
}
return direction + " " + species
}
func inferSpecies(p model.Product) string {
text := strings.ToLower(p.WoodSpecies + " " + p.Material + " " + p.StyleName)
for _, s := range []string{"oak", "maple", "hickory", "walnut", "pine", "birch", "cherry", "acacia", "bamboo", "ash", "elm", "teak"} {
if strings.Contains(text, s) {
return strings.Title(s)
}
}
return "Grain"
}
func orientationLabel(deg float64) string {
switch {
case deg < 22.5 || deg >= 157.5:
return "Horizontal"
case deg < 67.5:
return "Diagonal"
case deg < 112.5:
return "Vertical"
default:
return "Diagonal"
}
}
func compactStrings(values []string) []string {
seen := map[string]bool{}
out := make([]string, 0, len(values))
for _, v := range values {
v = strings.TrimSpace(v)
if v == "" {
continue
}
key := strings.ToLower(v)
if seen[key] {
continue
}
seen[key] = true
out = append(out, v)
}
return out
}
func containsAnyText(text string, needles ...string) bool {
for _, needle := range needles {
if strings.Contains(text, needle) {
return true
}
}
return false
}