feature: 算法更新

This commit is contained in:
2024-06-08 21:13:51 +08:00
parent 9c39c2c76b
commit 9f1225b3a8
6 changed files with 471 additions and 517 deletions
+150 -6
View File
@@ -1,9 +1,153 @@
# kenaito-dark-wallpaper-finder # Kenaito Dark Wallpaper Finder 🖤
#### 介绍 一个自动扫描、分析并整理暗色系壁纸的命令行工具。
当壁纸数量达到一个量级的时候,人工筛选黑色调的壁纸过于的繁杂,一个好的算法+自动化是最佳的解决方案
#### 执行结果 程序会递归扫描指定目录下的所有图片,根据亮度算法计算每张图片的"黑暗程度"评分,然后按评分等级自动分类移动(或复制)到不同子目录中。
![1](./doc/QQ20251221-093651.png)
![1](./doc/QQ20251221-100004.png) ## 功能特性
- **智能暗色分析**:基于 ITU-R BT.601 亮度公式计算黑暗评分,纯黑加分,鲜艳色惩罚
- **多格式支持**:JPG、PNG、GIF、BMP、WebP、TIFF
- **五级分类**:按 ≥95%、≥85%、≥75%、≥65%、≥55% 五个黑暗等级自动归档
- **并发处理**:默认 24 线程并行分析,大图自动采样加速
- **灵活配置**:所有参数均可通过命令行调整
## 编译
```bash
# Linux / macOS
go build -o kenaito-dark-wallpaper-finder main.go
# Windows
go build -o kenaito-dark-wallpaper-finder.exe main.go
```
或使用构建脚本:
```bash
bash build.sh
```
## 快速开始
```bash
# 在壁纸目录下运行(默认移动模式,等待5秒确认)
cd ~/Pictures/wallpapers
./kenaito-dark-wallpaper-finder
# 用复制模式试运行(不会移动原文件)
./kenaito-dark-wallpaper-finder -copy -sleep 0
```
执行后,源目录下会生成如下结构:
```
wallpapers/
├── wallpapers_dark_wallpapers/
│ ├── dark_95/ ← 黑暗度 ≥ 95%
│ │ ├── photo1_[2560x1440]_dark97.jpg
│ │ └── ...
│ ├── dark_85/ ← 黑暗度 ≥ 85%
│ ├── dark_75/ ← 黑暗度 ≥ 75%
│ ├── dark_65/ ← 黑暗度 ≥ 65%
│ └── dark_55/ ← 黑暗度 ≥ 55%
├── bright_wallpaper.jpg
└── ...
```
## 命令行参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `-source` | string | `.` | 源目录路径,递归扫描该目录下所有图片 |
| `-target` | string | 自动生成 | 目标目录基名,留空则自动使用 `<源目录名>_dark_wallpapers` |
| `-min-width` | int | `1920` | 图片最小宽度(像素),低于此值跳过 |
| `-min-height` | int | `1080` | 图片最小高度(像素),低于此值跳过 |
| `-min-size` | int | `100` | 最小文件大小(KB),小于此值跳过 |
| `-max-size` | int | `100` | 最大文件大小(MB),大于此值跳过(排除异常大文件) |
| `-copy` | bool | `false` | 复制模式,设为 true 则复制原文件而非移动 |
| `-parallel` | int | `24` | 并发分析线程数,根据 CPU 核心数调整 |
| `-verbose` | bool | `true` | 详细输出,显示每个文件的处理过程 |
| `-sleep` | int | `5` | 执行前等待秒数,设为 0 则立即执行。等待期间可按 Ctrl+C 取消 |
## 使用示例
### 基础用法
```bash
# 扫描当前目录,移动暗色壁纸到子目录(默认行为)
./kenaito-dark-wallpaper-finder
# 扫描指定目录
./kenaito-dark-wallpaper-finder -source ~/Downloads/wallpapers
# 复制模式(保留原文件不动)
./kenaito-dark-wallpaper-finder -source ~/Pictures -copy
# 跳过等待,直接执行
./kenaito-dark-wallpaper-finder -sleep 0
```
### 自定义筛选条件
```bash
# 只要 4K 壁纸(3840x2160 以上)
./kenaito-dark-wallpaper-finder -min-width 3840 -min-height 2160
# 只处理 10MB 以上的大图
./kenaito-dark-wallpaper-finder -min-size 10240
# 只处理 500KB ~ 5MB 之间的图片
./kenaito-dark-wallpaper-finder -min-size 500 -max-size 5
```
### 性能调优
```bash
# 低配机器,减少并发数
./kenaito-dark-wallpaper-finder -parallel 4
# 高配机器,关闭详细输出加速
./kenaito-dark-wallpaper-finder -parallel 48 -verbose=false
```
### 组合使用
```bash
# 扫描外接硬盘上的壁纸,复制模式,4K 以上,不等待,静默执行
./kenaito-dark-wallpaper-finder \
-source /Volumes/ExternalHD/Wallpapers \
-target ~/Pictures/DarkWallpapers \
-copy \
-min-width 3840 \
-min-height 2160 \
-sleep 0 \
-verbose=false
```
## 工作原理
1. **扫描阶段**:递归遍历源目录,识别支持格式的图片文件,按分辨率和文件大小初步筛选
2. **分析阶段**:多线程解码图片,对大图自动采样(>100万像素时按 sqrt(N/10万) 步长采样),通过 ITU-R BT.601 亮度公式计算黑暗评分
3. **分类阶段**:按五个阈值等级(95%、85%、75%、65%、55%)筛选,移动到对应子目录。每个文件只归入其最高匹配的等级目录
### 黑暗评分算法
```
brightness = 0.299×R + 0.587×G + 0.114×B
darkScore = (1 - brightness/255) ^ 0.6
```
- 纯黑像素(RGB < 40)额外加成 ×1.3
- 鲜艳像素(任一通道 > 200)惩罚 ×0.7
## 注意事项
- 默认使用**移动**模式,原文件会从源目录移走。首次使用建议加 `-copy` 试运行
- 目标目录会被自动跳过,不会重复处理之前已整理的文件
- 文件名冲突时自动追加序号 `_(1)`、`_(2)` 等
- 支持的图片格式依赖 Go 标准库 + `golang.org/x/image`:JPG、PNG、GIF、BMP、WebP、TIFF
## License
MIT
Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 KiB

+1 -11
View File
@@ -2,14 +2,4 @@ module kenaito-media-group-tool
go 1.21 go 1.21
require ( require golang.org/x/image v0.16.0
github.com/gabriel-vasile/mimetype v1.4.3
github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d
github.com/satori/go.uuid v1.2.0
golang.org/x/image v0.16.0
)
require (
golang.org/x/net v0.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
+1 -12
View File
@@ -1,13 +1,2 @@
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d h1:h/hohIqMUCML2Rp9BXXAu0I3ZR68d7eqMHLPNq7N2tg=
github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d/go.mod h1:LiYo3EFlYfk46Re4zgQysMo6yO3/kAXslB2fyMdl+uw=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+311 -480
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
@@ -13,13 +14,16 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
) )
// 支持的图片格式 // supportedExts 支持的图片格式
var supportedExts = map[string]bool{ var supportedExts = map[string]bool{
".jpg": true, ".jpeg": true, ".png": true, ".jpg": true, ".jpeg": true, ".png": true,
".bmp": true, ".webp": true, ".gif": true, ".bmp": true, ".webp": true, ".gif": true,
@@ -38,37 +42,142 @@ type WallpaperInfo struct {
// Config 配置结构 // Config 配置结构
type Config struct { type Config struct {
SourceDir string // 源目录,如果是当前目录,设置为 "." SourceDir string // 源目录
TargetDir string // 移动或复制到的目标目录,不建议设置为 ".",因为那不是脱了裤子放屁吗 TargetDir string // 目标目录基名(自动按黑暗等级创建子目录)
DarkThreshold float64 // 黑暗程度评分(0-1,越高越暗)
MinWidth int // 图片最小宽度 MinWidth int // 图片最小宽度
MinHeight int // 图片最大宽度 MinHeight int // 图片最小高度
MinSizeKB int64 // 文件大小最小值 MinSizeKB int64 // 文件大小最小值(KB)
MaxSizeMB int64 // 文件大小最大值 MaxSizeMB int64 // 文件大小最大值(MB)
CopyMode bool // true=复制, false=移动 CopyMode bool // true=复制, false=移动
Parallel int // 并发数 Parallel int // 并发数
Verbose bool // 是否打印日志 Verbose bool // 是否打印详细日志
SleepSeconds int // 执行前等待秒数(0=不等待)
} }
// 默认配置 // DefaultConfig 默认配置
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
SourceDir: ".", SourceDir: ".",
TargetDir: "./dark_wallpapers",
DarkThreshold: 0.75,
MinWidth: 1920, MinWidth: 1920,
MinHeight: 1080, MinHeight: 1080,
MinSizeKB: 100, // 最小100KB MinSizeKB: 100,
MaxSizeMB: 100, // 最大100MB MaxSizeMB: 100,
CopyMode: false, // 默认移动 CopyMode: false,
Parallel: 24, // i5-12490f配置 Parallel: 24,
Verbose: true, Verbose: true,
SleepSeconds: 5,
} }
} }
// 处理单个图片文件 func main() {
func processImageFile(path string, config Config) (*WallpaperInfo, error) { config := DefaultConfig()
// 检查文件大小
// 命令行参数
flag.StringVar(&config.SourceDir, "source", config.SourceDir, "源目录路径")
flag.StringVar(&config.TargetDir, "target", "", "目标目录基名(留空则自动用源目录名)")
flag.IntVar(&config.MinWidth, "min-width", config.MinWidth, "图片最小宽度(像素)")
flag.IntVar(&config.MinHeight, "min-height", config.MinHeight, "图片最小高度(像素)")
flag.Int64Var(&config.MinSizeKB, "min-size", config.MinSizeKB, "最小文件大小(KB)")
flag.Int64Var(&config.MaxSizeMB, "max-size", config.MaxSizeMB, "最大文件大小(MB)")
flag.BoolVar(&config.CopyMode, "copy", config.CopyMode, "复制模式(默认移动)")
flag.IntVar(&config.Parallel, "parallel", config.Parallel, "并发数")
flag.BoolVar(&config.Verbose, "verbose", config.Verbose, "详细输出")
flag.IntVar(&config.SleepSeconds, "sleep", config.SleepSeconds, "执行前等待秒数(0=不等待)")
flag.Parse()
// 检查源目录
if _, err := os.Stat(config.SourceDir); os.IsNotExist(err) {
log.Fatalf("源目录不存在: %s", config.SourceDir)
}
absSource, _ := filepath.Abs(config.SourceDir)
baseDirName := filepath.Base(absSource)
// 如果目标目录为空,自动生成
if config.TargetDir == "" {
config.TargetDir = filepath.Join(absSource, baseDirName+"_dark_wallpapers")
} else {
if !filepath.IsAbs(config.TargetDir) {
config.TargetDir = filepath.Join(absSource, config.TargetDir)
}
}
fmt.Println("🖤 黑色壁纸查找与移动工具")
fmt.Println(strings.Repeat("=", 50))
fmt.Printf(" 源目录: %s\n", config.SourceDir)
fmt.Printf(" 目标目录: %s(按黑暗等级分子目录)\n", config.TargetDir)
fmt.Printf(" 最小分辨率: %dx%d\n", config.MinWidth, config.MinHeight)
fmt.Printf(" 文件大小: %dKB ~ %dMB\n", config.MinSizeKB, config.MaxSizeMB)
fmt.Printf(" 模式: %s\n", map[bool]string{true: "复制", false: "移动"}[config.CopyMode])
fmt.Println(strings.Repeat("=", 50))
if config.SleepSeconds > 0 {
fmt.Printf("\n⏳ 程序将在 %d 秒后开始执行(Ctrl+C 取消)...\n", config.SleepSeconds)
time.Sleep(time.Duration(config.SleepSeconds) * time.Second)
}
// 黑暗阈值等级(从高到低)
thresholds := []int{95, 85, 75, 65, 55}
// 第一步:一次性扫描所有图片并分析
fmt.Println("\n🔍 开始扫描图片...")
allWallpapers, err := scanAllImages(config)
if err != nil {
log.Fatalf("扫描失败: %v", err)
}
fmt.Printf("\n✅ 扫描完成!共 %d 张图片满足分辨率和大小要求\n", len(allWallpapers))
if len(allWallpapers) == 0 {
fmt.Println("❌ 未找到符合条件的图片")
return
}
// 第二步:按黑暗等级分类并移动到不同子目录(从高到低,每张图片只归入一个等级)
for _, threshold := range thresholds {
minScore := float64(threshold) / 100.0
targetDir := filepath.Join(config.TargetDir, fmt.Sprintf("dark_%d", threshold))
fmt.Printf("\n%s\n", strings.Repeat("=", 60))
fmt.Printf("📁 处理黑暗等级 ≥ %d%% 的图片\n", threshold)
fmt.Printf(" 目标目录: %s\n", targetDir)
fmt.Println(strings.Repeat("=", 60))
filtered := filterByDarkness(allWallpapers, minScore)
if len(filtered) == 0 {
fmt.Printf(" ⚠️ 没有符合黑暗等级 ≥ %d%% 的图片\n", threshold)
continue
}
fmt.Printf(" 找到 %d 张符合条件的图片\n", len(filtered))
movedCount, moveErr := moveWallpapers(filtered, targetDir, config.CopyMode)
if moveErr != nil {
log.Printf("⚠️ 移动部分文件时出错: %v", moveErr)
}
fmt.Printf(" ✓ 成功移动/复制 %d 张图片\n", movedCount)
// 从待处理列表中移除本轮已处理的图片,避免后续低阈值重复处理
processed := make(map[string]struct{}, len(filtered))
for _, wp := range filtered {
processed[wp.Path] = struct{}{}
}
remaining := make([]WallpaperInfo, 0, len(allWallpapers))
for _, wp := range allWallpapers {
if _, ok := processed[wp.Path]; !ok {
remaining = append(remaining, wp)
}
}
allWallpapers = remaining
}
fmt.Printf("\n🎉 所有操作完成!文件已整理到: %s/\n", config.TargetDir)
}
// analyzeImageFile 分析单个图片的黑暗程度(不做阈值过滤)
func analyzeImageFile(path string, config Config) (*WallpaperInfo, error) {
fileInfo, err := os.Stat(path) fileInfo, err := os.Stat(path)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -83,32 +192,23 @@ func processImageFile(path string, config Config) (*WallpaperInfo, error) {
return nil, fmt.Errorf("文件太大: %dMB > %dMB", fileInfo.Size()/1024/1024, config.MaxSizeMB) return nil, fmt.Errorf("文件太大: %dMB > %dMB", fileInfo.Size()/1024/1024, config.MaxSizeMB)
} }
// 打开图片文件
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close() defer file.Close()
// 解码图片
img, _, err := image.Decode(file) img, _, err := image.Decode(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 分析图片
darkScore, avgColor, width, height := analyzeImage(img) darkScore, avgColor, width, height := analyzeImage(img)
// 检查分辨率
if width < config.MinWidth || height < config.MinHeight { if width < config.MinWidth || height < config.MinHeight {
return nil, fmt.Errorf("分辨率不足: %dx%d < %dx%d", width, height, config.MinWidth, config.MinHeight) return nil, fmt.Errorf("分辨率不足: %dx%d < %dx%d", width, height, config.MinWidth, config.MinHeight)
} }
// 检查黑暗程度
if darkScore < config.DarkThreshold {
return nil, fmt.Errorf("不够黑暗: %.2f < %.2f", darkScore, config.DarkThreshold)
}
return &WallpaperInfo{ return &WallpaperInfo{
Path: path, Path: path,
DarkScore: darkScore, DarkScore: darkScore,
@@ -119,70 +219,209 @@ func processImageFile(path string, config Config) (*WallpaperInfo, error) {
}, nil }, nil
} }
// 移动或复制文件到目标目录 // analyzeImage 分析图片的黑暗程度和尺寸
func moveOrCopyFile(wp *WallpaperInfo, config Config) error { // 使用暗色像素占比(亮度 < 80 的像素比例)作为评分,比平均亮度更能准确反映暗色背景壁纸
// 创建目标目录 func analyzeImage(img image.Image) (darkScore float64, avgColor color.RGBA, width, height int) {
err := os.MkdirAll(config.TargetDir, 0755) bounds := img.Bounds()
width = bounds.Dx()
height = bounds.Dy()
totalPixels := width * height
if totalPixels == 0 {
return 0, color.RGBA{0, 0, 0, 255}, width, height
}
var sumR, sumG, sumB uint64
darkCount := 0
sampleCount := 0
// 大图采样以提高性能
sampleStep := 1
if totalPixels > 1_000_000 {
sampleStep = int(math.Sqrt(float64(totalPixels) / 100_000))
}
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()
r8 := uint8(r >> 8)
g8 := uint8(g >> 8)
b8 := uint8(b >> 8)
sumR += uint64(r8)
sumG += uint64(g8)
sumB += uint64(b8)
// 亮度计算(ITU-R BT.601),判断是否为暗色像素
brightness := 0.299*float64(r8) + 0.587*float64(g8) + 0.114*float64(b8)
if brightness < 80 {
darkCount++
}
sampleCount++
}
}
if sampleCount == 0 {
return 0, color.RGBA{0, 0, 0, 255}, width, height
}
// 暗色像素占比作为黑暗评分(0-1),比平均亮度更准确:
// 一张 80% 黑色背景 + 20% 明亮主体的壁纸,评分 0.80,正确反映"暗色背景"特性
darkScore = float64(darkCount) / float64(sampleCount)
avgR := uint8(sumR / uint64(sampleCount))
avgG := uint8(sumG / uint64(sampleCount))
avgB := uint8(sumB / uint64(sampleCount))
return darkScore, color.RGBA{avgR, avgG, avgB, 255}, width, height
}
// scanAllImages 一次性扫描所有图片并分析黑暗程度
func scanAllImages(config Config) ([]WallpaperInfo, error) {
var allWallpapers []WallpaperInfo
var mu sync.Mutex
jobs := make(chan string, 100)
results := make(chan *WallpaperInfo, 100)
// 预计算目标目录的绝对路径,遍历时跳过
absTarget, _ := filepath.Abs(config.TargetDir)
// 启动 worker 池
var workerWg sync.WaitGroup
for i := 0; i < config.Parallel; i++ {
workerWg.Add(1)
go func(workerID int) {
defer workerWg.Done()
for path := range jobs {
if config.Verbose {
fmt.Printf("Worker %d 处理: %s\n", workerID, filepath.Base(path))
}
wp, err := analyzeImageFile(path, config)
if err != nil { if err != nil {
if config.Verbose {
fmt.Printf("跳过 %s: %v\n", filepath.Base(path), err)
}
continue
}
results <- wp
}
}(i)
}
// 结果收集 goroutine(用 WaitGroup 保证完成)
var resultWg sync.WaitGroup
resultWg.Add(1)
go func() {
defer resultWg.Done()
for wp := range results {
mu.Lock()
allWallpapers = append(allWallpapers, *wp)
mu.Unlock()
}
}()
// 遍历源目录,发送任务
var processedCount int
err := filepath.WalkDir(config.SourceDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
// 跳过目标目录(避免重复处理之前的结果)
if absTarget != "" {
absPath, _ := filepath.Abs(path)
if absPath == absTarget || strings.HasPrefix(absPath, absTarget+string(os.PathSeparator)) {
return filepath.SkipDir
}
}
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if supportedExts[ext] {
processedCount++
jobs <- path
}
return nil
})
close(jobs)
workerWg.Wait()
close(results)
resultWg.Wait() // 等待结果收集完成后再排序
// 按黑暗程度排序(从高到低)
sort.Slice(allWallpapers, func(i, j int) bool {
return allWallpapers[i].DarkScore > allWallpapers[j].DarkScore
})
fmt.Printf(" 扫描文件总数: %d\n", processedCount)
fmt.Printf(" 符合分辨率/大小要求: %d\n", len(allWallpapers))
return allWallpapers, err
}
// filterByDarkness 根据黑暗阈值筛选图片
func filterByDarkness(wallpapers []WallpaperInfo, minDarkScore float64) []WallpaperInfo {
var filtered []WallpaperInfo
for _, wp := range wallpapers {
if wp.DarkScore >= minDarkScore {
filtered = append(filtered, wp)
}
}
return filtered
}
// cleanFileName 清理文件名中的特殊字符
func cleanFileName(name string) string {
replacements := map[string]string{
" ": "_", "/": "_", "\\": "_", ":": "_",
"*": "", "?": "", "\"": "", "<": "", ">": "", "|": "",
}
result := name
for old, n := range replacements {
result = strings.ReplaceAll(result, old, n)
}
// 按 rune 截断,避免截断多字节 UTF-8 字符
runes := []rune(result)
if len(runes) > 100 {
result = string(runes[:100])
}
return result
}
// moveOrCopyFile 移动或复制文件到目标目录
func moveOrCopyFile(wp *WallpaperInfo, targetDir string, copyMode bool) error {
if err := os.MkdirAll(targetDir, 0755); err != nil {
return err return err
} }
// 生成目标文件名(保留原文件名,添加黑暗评分)
ext := filepath.Ext(wp.Path) ext := filepath.Ext(wp.Path)
baseName := strings.TrimSuffix(filepath.Base(wp.Path), ext) baseName := strings.TrimSuffix(filepath.Base(wp.Path), ext)
// 清理文件名中的特殊字符
cleanName := cleanFileName(baseName) cleanName := cleanFileName(baseName)
// 添加分辨率信息
newFilename := fmt.Sprintf("%s_[%dx%d]_dark%.0f%s", newFilename := fmt.Sprintf("%s_[%dx%d]_dark%.0f%s",
cleanName, wp.Width, wp.Height, wp.DarkScore*100, ext) cleanName, wp.Width, wp.Height, wp.DarkScore*100, ext)
dstPath := filepath.Join(config.TargetDir, newFilename) dstPath := filepath.Join(targetDir, newFilename)
// 检查目标文件是否已存在 // 文件名冲突:追加序号
if _, err := os.Stat(dstPath); err == nil { if _, err := os.Stat(dstPath); err == nil {
// 文件已存在,添加序号
for i := 1; i < 100; i++ { for i := 1; i < 100; i++ {
newFilename = fmt.Sprintf("%s_[%dx%d]_dark%.0f_(%d)%s", newFilename = fmt.Sprintf("%s_[%dx%d]_dark%.0f_(%d)%s",
cleanName, wp.Width, wp.Height, wp.DarkScore*100, i, ext) cleanName, wp.Width, wp.Height, wp.DarkScore*100, i, ext)
dstPath = filepath.Join(config.TargetDir, newFilename) dstPath = filepath.Join(targetDir, newFilename)
if _, err := os.Stat(dstPath); os.IsNotExist(err) { if _, err := os.Stat(dstPath); os.IsNotExist(err) {
break break
} }
} }
} }
if config.CopyMode { if copyMode {
// 复制文件
return copyFile(wp.Path, dstPath) return copyFile(wp.Path, dstPath)
} else { }
// 移动文件
return moveFile(wp.Path, dstPath) return moveFile(wp.Path, dstPath)
} }
}
// 清理文件名 // copyFile 复制文件
func cleanFileName(name string) string {
// 替换或移除特殊字符
replacements := map[string]string{
" ": "_", "/": "_", "\\": "_", ":": "_",
"*": "", "?": "", "\"": "", "<": "", ">": "", "|": "",
}
result := name
for old, n := range replacements {
result = strings.ReplaceAll(result, old, n)
}
// 限制长度
if len(result) > 100 {
result = result[:100]
}
return result
}
// 复制文件
func copyFile(src, dst string) error { func copyFile(src, dst string) error {
srcFile, err := os.Open(src) srcFile, err := os.Open(src)
if err != nil { if err != nil {
@@ -200,432 +439,24 @@ func copyFile(src, dst string) error {
return err return err
} }
// 移动文件 // moveFile 移动文件(优先 rename,跨分区时复制后删除)
func moveFile(src, dst string) error { func moveFile(src, dst string) error {
// 先尝试重命名(如果在同一分区) if err := os.Rename(src, dst); err == nil {
err := os.Rename(src, dst)
if err == nil {
return nil return nil
} }
// 如果重命名失败,则复制后删除
if err := copyFile(src, dst); err != nil { if err := copyFile(src, dst); err != nil {
return err return err
} }
// 删除原文件
return os.Remove(src) return os.Remove(src)
} }
// 显示结果摘要 // moveWallpapers 将图片列表移动到指定目标目录
func printSummary(wallpapers []WallpaperInfo, config Config) { func moveWallpapers(wallpapers []WallpaperInfo, targetDir string, copyMode bool) (int, error) {
if len(wallpapers) == 0 {
fmt.Println("❌ 未找到符合条件的黑色壁纸")
return
}
fmt.Println("\n🎯 找到的黑色调壁纸:")
fmt.Println(strings.Repeat("=", 60))
for i, wp := range wallpapers {
if i >= 20 { // 最多显示20个
fmt.Printf("... 还有 %d 个未显示\n", len(wallpapers)-20)
break
}
action := "移动到"
if config.CopyMode {
action = "复制到"
}
fmt.Printf("%2d. %s\n", i+1, filepath.Base(wp.Path))
fmt.Printf(" 原路径: %s\n", wp.Path)
fmt.Printf(" %s: %s\n", action, config.TargetDir)
fmt.Printf(" 黑暗程度: %.1f%%\n", wp.DarkScore*100)
fmt.Printf(" 平均颜色: RGB(%3d, %3d, %3d)\n", wp.AvgColor.R, wp.AvgColor.G, wp.AvgColor.B)
fmt.Printf(" 分辨率: %d × %d\n", wp.Width, wp.Height)
fmt.Printf(" 文件大小: %.1f MB\n", float64(wp.Size)/1024/1024)
// 黑暗程度可视化
bars := int(wp.DarkScore * 20)
fmt.Printf(" [")
for j := 0; j < 20; j++ {
if j < bars {
fmt.Printf("█")
} else {
fmt.Printf(" ")
}
}
fmt.Printf("]\n\n")
}
}
// 保存结果到日志文件
func saveResultsToLog(wallpapers []WallpaperInfo, config Config) {
logFile := filepath.Join(config.TargetDir, "dark_wallpapers_log.txt")
file, err := os.Create(logFile)
if err != nil {
log.Printf("无法创建日志文件: %v", err)
return
}
defer file.Close()
fmt.Fprintf(file, "黑色壁纸处理日志 - %s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Fprintf(file, "源目录: %s\n", config.SourceDir)
fmt.Fprintf(file, "目标目录: %s\n", config.TargetDir)
fmt.Fprintf(file, "黑暗阈值: %.2f\n", config.DarkThreshold)
fmt.Fprintf(file, "找到壁纸数量: %d\n\n", len(wallpapers))
for i, wp := range wallpapers {
fmt.Fprintf(file, "%d. %s\n", i+1, filepath.Base(wp.Path))
fmt.Fprintf(file, " 黑暗程度: %.1f%%\n", wp.DarkScore*100)
fmt.Fprintf(file, " 分辨率: %dx%d\n", wp.Width, wp.Height)
fmt.Fprintf(file, " 大小: %.2f MB\n", float64(wp.Size)/1024/1024)
fmt.Fprintf(file, " 原路径: %s\n", wp.Path)
fmt.Fprintf(file, " 目标: %s/%s\n\n", config.TargetDir, filepath.Base(wp.Path))
}
fmt.Printf("📝 详细日志已保存到: %s\n", logFile)
}
// 分析图片的黑暗程度和尺寸
func analyzeImage(img image.Image) (darkScore float64, avgColor color.RGBA, width, height int) {
bounds := img.Bounds()
width = bounds.Dx()
height = bounds.Dy()
totalPixels := width * height
if totalPixels == 0 {
return 0, color.RGBA{0, 0, 0, 255}, width, height
}
var sumR, sumG, sumB uint64
// 采样分析(对于大图片进行采样以提高性能)
sampleStep := 1
if totalPixels > 1000000 { // 超过100万像素则采样
sampleStep = int(math.Sqrt(float64(totalPixels) / 100000))
}
sampleCount := 0
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()
sumR += uint64(r >> 8)
sumG += uint64(g >> 8)
sumB += uint64(b >> 8)
sampleCount++
}
}
if sampleCount == 0 {
return 0, color.RGBA{0, 0, 0, 255}, width, height
}
avgR := uint8(sumR / uint64(sampleCount))
avgG := uint8(sumG / uint64(sampleCount))
avgB := uint8(sumB / uint64(sampleCount))
// 计算黑暗程度评分
brightness := 0.299*float64(avgR) + 0.587*float64(avgG) + 0.114*float64(avgB)
darkScore = 1.0 - (brightness / 255.0)
// 增强黑色权重
darkScore = math.Pow(darkScore, 0.6)
// 如果平均颜色值很低,进一步提高评分
if avgR < 40 && avgG < 40 && avgB < 40 {
darkScore = math.Min(1.0, darkScore*1.3)
}
// 惩罚太鲜艳的颜色
if avgR > 200 || avgG > 200 || avgB > 200 {
darkScore *= 0.7
}
return darkScore, color.RGBA{avgR, avgG, avgB, 255}, width, height
}
// findAndMoveDarkWallpapers 查找并移动黑色壁纸(单次扫描版本)
func findAndMoveDarkWallpapers(config Config) ([]WallpaperInfo, error) {
var darkWallpapers []WallpaperInfo
var mu sync.Mutex
var wg sync.WaitGroup
// 创建worker池
jobs := make(chan string, 100)
results := make(chan *WallpaperInfo, 100)
errors := make(chan error, 100)
// 启动worker
for i := 0; i < config.Parallel; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for path := range jobs {
if config.Verbose {
fmt.Printf("Worker %d 处理: %s\n", workerID, filepath.Base(path))
}
wp, err := processImageFile(path, config)
if err != nil {
if config.Verbose {
fmt.Printf("跳过 %s: %v\n", filepath.Base(path), err)
}
continue
}
results <- wp
}
}(i)
}
// 收集结果并移动文件
var processedCount int
var movedCount int
go func() {
for wp := range results {
mu.Lock()
darkWallpapers = append(darkWallpapers, *wp)
mu.Unlock()
// 移动文件
if err := moveOrCopyFile(wp, config); err != nil {
errors <- fmt.Errorf("移动文件失败 %s: %v", wp.Path, err)
continue
}
movedCount++
if config.Verbose {
fmt.Printf("✓ 已处理: %s (黑暗度: %.1f%%)\n",
filepath.Base(wp.Path), wp.DarkScore*100)
}
}
}()
// 收集错误
var processErrors []error
go func() {
for err := range errors {
processErrors = append(processErrors, err)
}
}()
// 遍历源目录,发送任务
err := filepath.WalkDir(config.SourceDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
// 跳过目标目录
if filepath.Clean(path) == filepath.Clean(config.TargetDir) {
return filepath.SkipDir
}
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if supportedExts[ext] {
processedCount++
jobs <- path
}
return nil
})
close(jobs)
wg.Wait()
close(results)
close(errors)
// 按黑暗程度排序
sort.Slice(darkWallpapers, func(i, j int) bool {
return darkWallpapers[i].DarkScore > darkWallpapers[j].DarkScore
})
// 打印统计信息
fmt.Printf("\n📊 处理完成统计:\n")
fmt.Printf(" 扫描文件总数: %d\n", processedCount)
fmt.Printf(" 找到黑色壁纸: %d\n", len(darkWallpapers))
fmt.Printf(" 成功移动/复制: %d\n", movedCount)
if len(processErrors) > 0 {
fmt.Printf(" 错误数量: %d\n", len(processErrors))
}
if len(processErrors) > 0 && config.Verbose {
fmt.Println("\n⚠️ 错误列表:")
for _, err := range processErrors {
fmt.Printf(" %v\n", err)
}
}
return darkWallpapers, err
}
func main() {
// 配置
config := DefaultConfig()
config.SourceDir = "."
// 检查源目录是否存在
if _, err := os.Stat(config.SourceDir); os.IsNotExist(err) {
log.Fatalf("源目录不存在: %s", config.SourceDir)
return
}
rootDir, _ := os.Getwd()
baseDirName := filepath.Base(rootDir)
fmt.Println("🖤 黑色壁纸查找与移动工具")
fmt.Println(strings.Repeat("=", 40))
fmt.Println("整理完成的文件在上一级目录中,请勿整理磁盘根目录文件,若无疑问,程序将在 5s 后执行")
fmt.Println(strings.Repeat("=", 40))
time.Sleep(time.Second * 5)
// 定义多个黑暗阈值等级
targetDarkThresholds := []int{95, 85, 75, 65, 55}
// 第一步:一次性扫描所有图片,分析黑暗程度
fmt.Println("\n🔍 开始扫描图片...")
allWallpapers, err := scanAllImages(config)
if err != nil {
log.Fatalf("扫描失败: %v", err)
}
fmt.Printf("\n✅ 扫描完成!共找到 %d 张符合条件的图片\n", len(allWallpapers))
// 第二步:根据黑暗等级分类并移动到不同目录
for _, threshold := range targetDarkThresholds {
darkThreshold := float64(threshold) / 100.0 // 转换为小数形式(如 0.95)
targetDir := "../" + baseDirName + "_dark_wallpapers_" + strconv.Itoa(threshold)
fmt.Printf("\n" + strings.Repeat("=", 60) + "\n")
fmt.Printf("📁 处理黑暗等级 ≥ %d%% 的图片\n", threshold)
fmt.Printf(" 目标目录: %s\n", targetDir)
fmt.Println(strings.Repeat("=", 60))
// 筛选出符合当前黑暗阈值的图片
filteredWallpapers := filterWallpapersByDarkness(allWallpapers, darkThreshold)
if len(filteredWallpapers) == 0 {
fmt.Printf(" ⚠️ 没有符合黑暗等级 ≥ %d%% 的图片\n", threshold)
continue
}
fmt.Printf(" 找到 %d 张符合条件的图片\n", len(filteredWallpapers))
// 移动这些图片到对应目录
movedCount, err := moveWallpapersToTarget(filteredWallpapers, targetDir, config.CopyMode)
if err != nil {
log.Printf("⚠️ 移动部分文件时出错: %v", err)
}
fmt.Printf(" ✓ 成功移动/复制 %d 张图片\n", movedCount)
}
fmt.Println("\n🎉 所有操作完成!")
}
// scanAllImages 一次性扫描所有图片并分析黑暗程度
func scanAllImages(config Config) ([]WallpaperInfo, error) {
var allWallpapers []WallpaperInfo
var mu sync.Mutex
var wg sync.WaitGroup
// 创建worker池
jobs := make(chan string, 100)
results := make(chan *WallpaperInfo, 100)
// 启动worker
for i := 0; i < config.Parallel; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for path := range jobs {
if config.Verbose {
fmt.Printf("Worker %d 处理: %s\n", workerID, filepath.Base(path))
}
wp, err := processImageFile(path, config)
if err != nil {
// 静默跳过不符合条件的图片
continue
}
results <- wp
}
}(i)
}
// 收集结果
go func() {
for wp := range results {
mu.Lock()
allWallpapers = append(allWallpapers, *wp)
mu.Unlock()
}
}()
// 遍历源目录,发送任务
var processedCount int
err := filepath.WalkDir(config.SourceDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if supportedExts[ext] {
processedCount++
jobs <- path
}
return nil
})
close(jobs)
wg.Wait()
close(results)
// 按黑暗程度排序(从高到低)
sort.Slice(allWallpapers, func(i, j int) bool {
return allWallpapers[i].DarkScore > allWallpapers[j].DarkScore
})
fmt.Printf(" 扫描文件总数: %d\n", processedCount)
fmt.Printf(" 符合条件的图片: %d\n", len(allWallpapers))
return allWallpapers, err
}
// filterWallpapersByDarkness 根据黑暗阈值筛选图片
func filterWallpapersByDarkness(wallpapers []WallpaperInfo, minDarkScore float64) []WallpaperInfo {
var filtered []WallpaperInfo
for _, wp := range wallpapers {
if wp.DarkScore >= minDarkScore {
filtered = append(filtered, wp)
}
}
return filtered
}
// moveWallpapersToTarget 将图片移动到指定目标目录
func moveWallpapersToTarget(wallpapers []WallpaperInfo, targetDir string, copyMode bool) (int, error) {
movedCount := 0 movedCount := 0
var firstErr error var firstErr error
for _, wp := range wallpapers { for _, wp := range wallpapers {
// 创建临时配置用于移动文件 if err := moveOrCopyFile(&wp, targetDir, copyMode); err != nil {
tempConfig := Config{
TargetDir: targetDir,
CopyMode: copyMode,
}
if err := moveOrCopyFile(&wp, tempConfig); err != nil {
if firstErr == nil { if firstErr == nil {
firstErr = err firstErr = err
} }