diff --git a/README.md b/README.md index ae950f4..4925e97 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - **标准分辨率归类**:720P、1080P、2K、2.5K、影院 2K、DCI 4K、8K 及带鱼屏(UltraWide 2K/3K/4K)等标准规格单独建目录 - **非标分辨率归类**:按像素区间(每 1024px 一档)自动归入 1K ~ 19K / 普通 / 原图(原画质)目录 - **方向区分**:横屏、竖屏、等比(正方形)分开整理,GIF 单独处理 -- **明暗分类**:基于 ITU-R BT.601 亮度公式分析每张图片明暗程度,暗色(亮度 < 80)像素占比为 darkScore、亮色(亮度 > 175)像素占比为 lightScore,按主导分在目录名末尾追加 `-dark_100` ~ `-dark_55` / `-bright_85` ~ `-bright_15` 明暗等级 +- **明暗分类**:基于 ITU-R BT.601 亮度公式分析每张图片明暗程度,暗色(亮度 < 80)像素占比为 darkScore、亮色(亮度 > 175)像素占比为 lightScore;以两者差幅判定亮暗主导,在目录名末尾追加 `-dark_N` / `-mid` / `-bright_N`(N 为占比四舍五入到 10% 的档位) - **音频归类**:音频文件单独归入「音频」目录 - **异常兜底**:读取失败、不支持的格式自动归入「读取异常」「已忽略」目录 - **自动整理**:无需人工干预,扫描 → 分析 → 移动一次完成,跨磁盘自动降级为复制后删除 @@ -56,19 +56,9 @@ kenaito-media-picker.exe ```text {uid}-图片_横屏_720P-dark_100 ← 1280×720,暗色主导 -{uid}-图片_横屏_1D5KP-dark_95 ← 1600×900 -{uid}-图片_横屏_1080P-bright_85 ← 1920×1080,亮色主导 -{uid}-图片_横屏_2DKP-dark_75 ← 2048×1080(影院 2K) -{uid}-图片_横屏_2D5KP-bright_65 ← 2560×1440(2.5K) -{uid}-图片_横屏_ultraWide2KP-dark_55 ← 2560×1080(带鱼屏) -{uid}-图片_横屏_ultraWide3KP-bright_45 ← 3440×1440(带鱼屏) -{uid}-图片_横屏_ultraWide4KP-dark_85 ← 5120×2160(带鱼屏) -{uid}-图片_横屏_4KP-bright_35 ← 3840×2160 -{uid}-图片_横屏_4KHP-dark_65 ← 4096×2160(DCI 4K) -{uid}-图片_横屏_8KP-bright_25 ← 7680×4320 -{uid}-图片_横屏_1K ~ 19K-bright_15 ← 非标规格按像素区间(每 1024px 一档) -{uid}-图片_横屏_普通-dark_100 ← 像素不足 1024 -{uid}-图片_横屏_原图-bright_85 ← 像素 ≥ 20480 +{uid}-图片_横屏_1D5KP-mid ← 1600×900,明暗均衡 +{uid}-图片_横屏_1080P-bright_100 ← 1920×1080,亮色主导 +{uid}-图片_横屏_2DKP-dark_70 ← 2048×1080(影院 2K) {uid}-图片_竖屏_... ← 竖屏按同样规则(普通 / 1K~19K / 原图) {uid}-图片_等比_... ← 等比(正方形)按同样规则 {uid}-图片_GIF ← 所有 GIF 图片 @@ -101,7 +91,10 @@ darkScore = 亮度 < 80 的像素占比 lightScore = 亮度 > 175 的像素占比 ``` -亮色主导(`lightScore > darkScore`)归 `-bright_N`,否则归 `-dark_N`;每张图必归亮暗之一。暗色档位 55/65/75/85/95/100,亮色档位 15/25/35/45/55/65/75/85,取评分命中的最高一档(不足最低档归最低档)。大图(>100 万像素)自动采样加速。 +- **判定规则**:`lightScore` 高于 `darkScore` 15 个百分点以上归 `-bright_N`,反之归 `-dark_N`;差幅不足 15% 视为中性归 `-mid`(不强制二选一) +- **档位命名**:N 为占比四舍五入到 10% 的档位(10~100),如 darkScore 0.70 → `dark_70`,表示占比命中 [N-5%, N+5%) 区间 +- **透明处理**:完全透明的像素(alpha=0)不计入统计,全透明图片归 `-mid` +- **性能**:大图(>100 万像素)自动采样加速 ## 目录结构 diff --git a/internal/classify/brightness.go b/internal/classify/brightness.go index 3bcdebe..acf68c9 100644 --- a/internal/classify/brightness.go +++ b/internal/classify/brightness.go @@ -2,34 +2,32 @@ package classify import "fmt" -// 暗色阈值等级(从高到低) -var darkThresholds = []int{100, 95, 85, 75, 65, 55} +// neutralMargin 判定明暗主导的最小分差:两分差幅小于该值视为中性图 +const neutralMargin = 0.15 -// 亮色阈值等级(从高到低) -var brightThresholds = []int{85, 75, 65, 55, 45, 35, 25, 15} - -// BrightnessName 按主导分判定明暗等级,返回如 "dark_100" / "bright_85"。 -// 亮色主导(lightScore > darkScore)归 bright,否则归 dark,每张图必归亮暗之一。 +// BrightnessName 判定图片明暗等级,返回如 "dark_70" / "bright_40" / "mid"。 +// 以主导分差幅判定亮暗:lightScore 高于 darkScore neutralMargin 以上归 bright, +// 反之归 dark,两者接近视为中性 mid(不再强制二选一)。 func BrightnessName(darkScore, lightScore float64) string { - darkLowest := darkThresholds[len(darkThresholds)-1] - brightLowest := brightThresholds[len(brightThresholds)-1] - - if lightScore > darkScore { - picked := brightLowest - for _, t := range brightThresholds { - if lightScore >= float64(t)/100.0 { - picked = t - break - } - } - return fmt.Sprintf("bright_%d", picked) + switch { + case lightScore > darkScore+neutralMargin: + return fmt.Sprintf("bright_%d", brightnessTier(lightScore)) + case darkScore > lightScore+neutralMargin: + return fmt.Sprintf("dark_%d", brightnessTier(darkScore)) + default: + return "mid" } - picked := darkLowest - for _, t := range darkThresholds { - if darkScore >= float64(t)/100.0 { - picked = t - break - } +} + +// brightnessTier 将占比分档到最接近的 10%,表示命中区间 [N-5%, N+5%),最小 10、最大 100。 +// 例如 0.30~0.39 归 "30",0.96 以上归 "100",避免占比不足却挂高档名。 +func brightnessTier(score float64) int { + tier := int(score*100+5) / 10 * 10 + if tier < 10 { + return 10 } - return fmt.Sprintf("dark_%d", picked) + if tier > 100 { + return 100 + } + return tier } diff --git a/internal/classify/classify.go b/internal/classify/classify.go index fb6d066..64450e7 100644 --- a/internal/classify/classify.go +++ b/internal/classify/classify.go @@ -35,7 +35,7 @@ func (o Orientation) String() string { } } -// ImageDir 返回图片归类子目录名,如 "图片_横屏_1080P-dark_100" / "图片_GIF"。 +// ImageDir 返回图片归类子目录名,如 "图片_横屏_1080P-dark_70" / "图片_横屏_1080P-mid" / "图片_GIF"。 // darkScore/lightScore 用于明暗分类(GIF 不做明暗分类,忽略这两项)。 func ImageDir(ext string, width, height int, darkScore, lightScore float64) string { if strings.EqualFold(ext, ".gif") { diff --git a/internal/organize/organize.go b/internal/organize/organize.go index e747715..dfd9454 100644 --- a/internal/organize/organize.go +++ b/internal/organize/organize.go @@ -133,7 +133,7 @@ func (o *Organizer) ProcessImages(images []scanner.File) { if !strings.EqualFold(f.Ext, ".gif") { dark, light, berr := probe.AnalyzeBrightness(f.Path) if berr != nil { - fmt.Printf("=== 明暗分析失败(按默认dark_55归类): %s \n", f.Path) + fmt.Printf("=== 明暗分析失败(按中性mid归类): %s \n", f.Path) } else { darkScore, lightScore = dark, light } diff --git a/internal/probe/brightness.go b/internal/probe/brightness.go index dcfc863..9b4ec48 100644 --- a/internal/probe/brightness.go +++ b/internal/probe/brightness.go @@ -30,7 +30,8 @@ func AnalyzeBrightness(filePath string) (darkScore float64, lightScore float64, return dark, light, nil } -// analyzeBrightness 统计暗色/亮色像素占比(ITU-R BT.601 亮度公式),大图自动采样加速 +// analyzeBrightness 统计暗色/亮色像素占比(ITU-R BT.601 亮度公式),大图自动采样加速。 +// 完全透明的像素(alpha=0)不计入统计。 func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64) { bounds := img.Bounds() totalPixels := bounds.Dx() * bounds.Dy() @@ -46,7 +47,10 @@ func analyzeBrightness(img image.Image) (darkScore float64, lightScore float64) for y := bounds.Min.Y; y < bounds.Max.Y; y += sampleStep { for x := bounds.Min.X; x < bounds.Max.X; x += sampleStep { - r, g, b, _ := img.At(x, y).RGBA() + 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)