feature: 算法更新
This commit is contained in:
@@ -1,9 +1,153 @@
|
|||||||
# kenaito-dark-wallpaper-finder
|
# Kenaito Dark Wallpaper Finder 🖤
|
||||||
|
|
||||||
#### 介绍
|
一个自动扫描、分析并整理暗色系壁纸的命令行工具。
|
||||||
当壁纸数量达到一个量级的时候,人工筛选黑色调的壁纸过于的繁杂,一个好的算法+自动化是最佳的解决方案
|
|
||||||
|
|
||||||
#### 执行结果
|
程序会递归扫描指定目录下的所有图片,根据亮度算法计算每张图片的"黑暗程度"评分,然后按评分等级自动分类移动(或复制)到不同子目录中。
|
||||||

|
|
||||||
|
|
||||||

|
## 功能特性
|
||||||
|
|
||||||
|
- **智能暗色分析**:基于 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 |
@@ -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,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=
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user