refactor: 全面重构代码并加白话注释,移动全局并行,去掉去向/重复记录
- 重构后端与前端,全程大白话注释(不懂程序也能看懂) - 移动阶段改为全局并行(MoveAll,工人数=CPU核心数) - 去掉记录每个文件搬去哪了(删除去向回填,解决写库卡顿) - 去掉重复文件标记与统计:重复判定改为 MD5+路径都相同才算,重复则原地忽略不移动;MD5 相同但路径不同仍照常搬 - 媒体库去掉归类目录列,看板去掉重复/归类分布统计
This commit is contained in:
@@ -14,18 +14,23 @@ import (
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// 明暗分析相关的三个参数:
|
||||
const (
|
||||
// brightnessDecodeLimit 明暗分析允许解码的最大像素数,超出时跳过分析(按 mid 归类),避免超大图 OOM
|
||||
// brightnessDecodeLimit 允许分析的最大像素数。超过就跳过分析(按中性归类),
|
||||
// 防止超大图把内存吃爆。
|
||||
brightnessDecodeLimit = 40_000_000
|
||||
// thumbnailMaxEdge 明暗分析时大图缩放后的最长边像素,缩小到该尺寸内再统计
|
||||
// thumbnailMaxEdge 大图缩放后的最长边像素。先缩小再统计,省内存省时间。
|
||||
thumbnailMaxEdge = 256
|
||||
// thumbnailScaleThreshold 像素数超过该值才缩放为缩略图再统计
|
||||
// thumbnailScaleThreshold 像素数超过这个值才缩放成缩略图再统计。
|
||||
thumbnailScaleThreshold = 250_000
|
||||
)
|
||||
|
||||
// AnalyzeBrightness 打开并解码图片,分析明暗程度。
|
||||
// darkScore: 暗色像素(亮度 < 80)占比;lightScore: 亮色像素(亮度 > 175)占比。
|
||||
// 超大图(超过 brightnessDecodeLimit 像素)直接跳过分析返回 mid;大图先缩放为缩略图再统计,降低内存与耗时。
|
||||
// AnalyzeBrightness 打开一张图片,分析它偏暗还是偏亮。
|
||||
// - darkScore:暗色像素(亮度 < 80)占多少比例
|
||||
// - lightScore:亮色像素(亮度 > 175)占多少比例
|
||||
//
|
||||
// 超大图直接跳过(返回两个 0,调用方会按"中性"归类);
|
||||
// 普通大图先缩小成缩略图再统计,既快又不失真(整体明暗分布基本保留)。
|
||||
func AnalyzeBrightness(filePath string) (darkScore float64, lightScore float64, err error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
@@ -33,13 +38,15 @@ func AnalyzeBrightness(filePath string) (darkScore float64, lightScore float64,
|
||||
}
|
||||
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 归类
|
||||
return 0, 0, nil // 超大图,跳过
|
||||
}
|
||||
// 回到文件开头,重新完整解码
|
||||
if _, err := f.Seek(0, 0); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -48,6 +55,7 @@ func AnalyzeBrightness(filePath string) (darkScore float64, lightScore float64,
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// 大图先缩小成缩略图
|
||||
if img.Bounds().Dx()*img.Bounds().Dy() > thumbnailScaleThreshold {
|
||||
img = scaleToThumbnail(img, thumbnailMaxEdge)
|
||||
}
|
||||
@@ -55,8 +63,8 @@ func AnalyzeBrightness(filePath string) (darkScore float64, lightScore float64,
|
||||
return dark, light, nil
|
||||
}
|
||||
|
||||
// scaleToThumbnail 将大图等比缩放到最长边不超过 maxEdge 的缩略图(纯内存,不落盘)。
|
||||
// 缩放是像素平均,整体亮度分布基本保留,用于粗粒度明暗分类足够准确。
|
||||
// scaleToThumbnail 把大图等比缩放到最长边不超过 maxEdge 的缩略图(只占内存,不写盘)。
|
||||
// 缩小时是像素平均,整体明暗分布基本保留,用来粗分亮暗足够了。
|
||||
func scaleToThumbnail(img image.Image, maxEdge int) image.Image {
|
||||
srcBounds := img.Bounds()
|
||||
srcW, srcH := srcBounds.Dx(), srcBounds.Dy()
|
||||
@@ -76,9 +84,11 @@ func scaleToThumbnail(img image.Image, maxEdge int) image.Image {
|
||||
return dst
|
||||
}
|
||||
|
||||
// analyzeBrightness 统计暗色/亮色像素占比(ITU-R BT.601 亮度公式)。
|
||||
// 完全透明的像素(alpha=0)不计入统计。
|
||||
// 对常见内存布局(NRGBA/RGBA/Gray)直接访问像素数组,避免 image.At 接口逐像素开销。
|
||||
// analyzeBrightness 数一数暗色/亮色像素各占多少比例。
|
||||
// 用 ITU-R BT.601 的亮度公式算每个像素的"亮度值"。
|
||||
// 完全透明的像素(alpha=0)不算数。
|
||||
// 为了快,常见的内存格式(NRGBA/RGBA/Gray)直接翻底层像素数组,
|
||||
// 不常见的才用慢一点的通用方式。
|
||||
func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64) {
|
||||
bounds := img.Bounds()
|
||||
totalPixels := bounds.Dx() * bounds.Dy()
|
||||
@@ -108,7 +118,8 @@ func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
offset := m.PixOffset(bounds.Min.X, y)
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
// 预乘 alpha:若完全不透明则按直接值,否则近似取非预乘分量
|
||||
// RGBA 是"预乘 alpha",完全不透明就用原值,
|
||||
// 半透明的按比例还原出真实颜色分量再算亮度
|
||||
a := pix[offset+3]
|
||||
if a == 0 {
|
||||
offset += 4
|
||||
@@ -126,6 +137,7 @@ func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64)
|
||||
}
|
||||
}
|
||||
case *image.Gray:
|
||||
// 灰度图:像素本身就是亮度值
|
||||
pix := m.Pix
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
offset := m.PixOffset(bounds.Min.X, y)
|
||||
@@ -141,6 +153,7 @@ func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// 其他格式:用通用的读法
|
||||
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()
|
||||
@@ -159,7 +172,7 @@ func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64)
|
||||
return float64(darkCount) / float64(sampleCount), float64(lightCount) / float64(sampleCount)
|
||||
}
|
||||
|
||||
// countBrightness 按 BT.601 亮度公式累计暗/亮像素计数
|
||||
// countBrightness 按亮度公式(BT.601)判断一个像素偏暗还是偏亮,并计数。
|
||||
func countBrightness(r, g, b float64, darkCount, lightCount *int64) {
|
||||
brightness := 0.299*r + 0.587*g + 0.114*b
|
||||
if brightness < 80 {
|
||||
|
||||
Reference in New Issue
Block a user