package probe import ( "image" "os" _ "image/gif" _ "image/jpeg" _ "image/png" _ "golang.org/x/image/bmp" "golang.org/x/image/draw" _ "golang.org/x/image/tiff" _ "golang.org/x/image/webp" ) const ( // brightnessDecodeLimit 明暗分析允许解码的最大像素数,超出时跳过分析(按 mid 归类),避免超大图 OOM brightnessDecodeLimit = 40_000_000 // thumbnailMaxEdge 明暗分析时大图缩放后的最长边像素,缩小到该尺寸内再统计 thumbnailMaxEdge = 512 // thumbnailScaleThreshold 像素数超过该值才缩放为缩略图再统计 thumbnailScaleThreshold = 1_000_000 ) // AnalyzeBrightness 打开并解码图片,分析明暗程度。 // darkScore: 暗色像素(亮度 < 80)占比;lightScore: 亮色像素(亮度 > 175)占比。 // 超大图(超过 brightnessDecodeLimit 像素)直接跳过分析返回 mid;大图先缩放为缩略图再统计,降低内存与耗时。 func AnalyzeBrightness(filePath string) (darkScore float64, lightScore float64, err error) { f, err := os.Open(filePath) if err != nil { return 0, 0, err } defer f.Close() cfg, _, err := image.DecodeConfig(f) if err != nil { return 0, 0, err } if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width*cfg.Height > brightnessDecodeLimit { return 0, 0, nil // 超大图跳过,由调用方按 mid 归类 } if _, err := f.Seek(0, 0); err != nil { return 0, 0, err } img, _, err := image.Decode(f) if err != nil { return 0, 0, err } if img.Bounds().Dx()*img.Bounds().Dy() > thumbnailScaleThreshold { img = scaleToThumbnail(img, thumbnailMaxEdge) } dark, light := analyzeBrightness(img) return dark, light, nil } // scaleToThumbnail 将大图等比缩放到最长边不超过 maxEdge 的缩略图(纯内存,不落盘)。 // 缩放是像素平均,整体亮度分布基本保留,用于粗粒度明暗分类足够准确。 func scaleToThumbnail(img image.Image, maxEdge int) image.Image { srcBounds := img.Bounds() srcW, srcH := srcBounds.Dx(), srcBounds.Dy() if srcW <= 0 || srcH <= 0 { return img } ratio := float64(maxEdge) / float64(max(srcW, srcH)) dstW, dstH := int(float64(srcW)*ratio), int(float64(srcH)*ratio) if dstW < 1 { dstW = 1 } if dstH < 1 { dstH = 1 } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) draw.BiLinear.Scale(dst, dst.Bounds(), img, srcBounds, draw.Over, nil) return dst } // analyzeBrightness 统计暗色/亮色像素占比(ITU-R BT.601 亮度公式)。 // 完全透明的像素(alpha=0)不计入统计。 func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64) { bounds := img.Bounds() totalPixels := bounds.Dx() * bounds.Dy() if totalPixels == 0 { return 0, 0 } darkCount, lightCount, sampleCount := 0, 0, 0 for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { r, g, b, a := img.At(x, y).RGBA() if a == 0 { continue } r8 := uint8(r >> 8) g8 := uint8(g >> 8) b8 := uint8(b >> 8) brightness := 0.299*float64(r8) + 0.587*float64(g8) + 0.114*float64(b8) if brightness < 80 { darkCount++ } else if brightness > 175 { lightCount++ } sampleCount++ } } if sampleCount == 0 { return 0, 0 } return float64(darkCount) / float64(sampleCount), float64(lightCount) / float64(sampleCount) }