ffmpeg_classify_ratio
FFmpeg 图片按宽高比自动分类(自动识别横竖屏)
补充说明
本文提供 Windows PowerShell 和 Linux Shell 脚本,用于批量识别图片宽高比和横竖屏方向,自动分类归档。脚本通过 ffprobe 获取每张图片的分辨率,自动判断横竖屏,分别匹配对应的常见比例(±5% 容差),绝不修改原始图片。
通用参数说明
- 源目录:当前目录
.(支持通过参数自定义)- 容差范围:±5%(匹配阈值 0.05)
- 横屏输出:
landscape/<比例>/- 竖屏输出:
portrait/<比例>/- 支持格式:webp / jpg / jpeg / png / bmp / jfif
分类逻辑
- 扫描当前目录所有图片文件(不递归子目录)
- 调用
ffprobe提取宽高分辨率 - 自动判断方向:宽度 > 高度 → 横屏,否则 → 竖屏
- 计算对应方向的比例(横屏:宽÷高,竖屏:高÷宽)
- 与预定义常见比例逐一比对,取最接近项
- 误差小于 5% 归入对应比例目录,否则归入
other目录 - 横屏图片放入
landscape/下,竖屏图片放入portrait/下 - 输出分类统计结果
Windows 版本(PowerShell)
# ============================================================
# 图片按宽高比自动分类(自动识别横竖屏)
# 横屏比例:4:3, 3:2, 16:10, 16:9, 17:9, 19:10, 2:1, 20:9, 21:9
# 竖屏比例:1:1, 4:5, 3:4, 2:3, 10:16, 9:16, 6:13, 9:21
# 容差:±5%
# ============================================================
$srcDir = if ($args[0]) { $args[0] } else { $PWD }
$tolerance = 0.05
# 横屏比例(宽÷高)
$landscapeRatios = @(
@{Name="4x3"; Ratio=4/3; Dir="4x3"}
@{Name="3x2"; Ratio=3/2; Dir="3x2"}
@{Name="16x10"; Ratio=16/10; Dir="16x10"}
@{Name="16x9"; Ratio=16/9; Dir="16x9"}
@{Name="17x9"; Ratio=17/9; Dir="17x9"}
@{Name="19x10"; Ratio=19/10; Dir="19x10"}
@{Name="2x1"; Ratio=2.0; Dir="2x1"}
@{Name="18x9"; Ratio=18/9; Dir="18x9"}
@{Name="20x9"; Ratio=20/9; Dir="20x9"}
@{Name="21x9"; Ratio=21/9; Dir="21x9"}
@{Name="24x9"; Ratio=24/9; Dir="24x9"}
@{Name="32x9"; Ratio=32/9; Dir="32x9"}
)
# 竖屏比例(高÷宽)
$portraitRatios = @(
@{Name="1x1"; Ratio=1.0; Dir="1x1"}
@{Name="4x5"; Ratio=1.25; Dir="4x5"}
@{Name="3x4"; Ratio=1.333; Dir="3x4"}
@{Name="2x3"; Ratio=1.5; Dir="2x3"}
@{Name="10x16"; Ratio=1.6; Dir="10x16"}
@{Name="9x16"; Ratio=1.778; Dir="9x16"}
@{Name="6x13"; Ratio=13/6; Dir="6x13"}
@{Name="9x21"; Ratio=21/9; Dir="9x21"}
)
# 获取当前目录所有图片(不递归)
$imageFiles = Get-ChildItem -Path $srcDir -File | Where-Object {
$_.Extension -in '.webp', '.jpg', '.jpeg', '.png', '.bmp', '.jfif'
}
Write-Host "`n📊 开始智能分类(自动识别横竖屏)..." -ForegroundColor Cyan
Write-Host "共 $($imageFiles.Count) 个图片文件`n" -ForegroundColor Cyan
$stats = @{}
$moved = 0
$errors = 0
foreach ($file in $imageFiles) {
try {
$result = & ffprobe -v error -select_streams v:0 `
-show_entries stream=width,height -of csv=p=0 $file.FullName
if ($result) {
$dim = $result -split ','
$w = [int]$dim[0]
$h = [int]$dim[1]
# 判断横竖屏
if ($w -gt $h) {
$mode = "landscape"
$ratios = $landscapeRatios
$actualRatio = $w / $h
} else {
$mode = "portrait"
$ratios = $portraitRatios
$actualRatio = $h / $w
}
# 找到最接近的常见比例
$bestMatch = $null
$bestDiff = [double]::MaxValue
foreach ($r in $ratios) {
$diff = [math]::Abs($actualRatio - $r.Ratio)
if ($diff -lt $bestDiff) {
$bestDiff = $diff
$bestMatch = $r
}
}
# 决定目标目录
if ($bestDiff -lt $tolerance) {
$subDir = $bestMatch.Dir
} else {
$subDir = "other"
}
$targetDir = Join-Path $srcDir "$mode\$subDir"
if (!(Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir | Out-Null
}
Move-Item -Path $file.FullName -Destination (Join-Path $targetDir $file.Name) -Force
$moved++
$key = "$mode/$subDir"
if (!$stats[$key]) { $stats[$key] = 0 }
$stats[$key]++
if ($moved % 20 -eq 0) {
Write-Host "已处理: $moved 个图片" -ForegroundColor Cyan
}
}
} catch {
$errors++
Write-Host "❌ 处理失败: $($file.Name)" -ForegroundColor Red
}
}
Write-Host "`n========== 分类完成 ==========" -ForegroundColor Green
Write-Host "`n📊 统计结果:`n" -ForegroundColor Cyan
foreach ($key in ($stats.Keys | Sort-Object)) {
$count = $stats[$key]
$pct = [math]::Round(($count / $imageFiles.Count) * 100, 1)
Write-Host " $key`: $count 个图片 ($pct`%)" -ForegroundColor Yellow
}
Write-Host "`n✅ 共处理 $moved 个图片!" -ForegroundColor Green
Write-Host "❌ 处理失败: $errors 个`n" -ForegroundColor Red
# 验证总数
$total = (Get-ChildItem $srcDir -File -Recurse).Count
Write-Host "📁 总计: $total 个图片`n" -ForegroundColor Cyan
Linux 版本
一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/ffmpeg_classify_ratio.sh)
bash <(curl -sL ../sh/ffmpeg_classify_ratio.sh)
脚本源码
#!/bin/bash
list_color_init() {
export gl_hui=$'\033[38;5;59m'
export gl_hong=$'\033[38;5;9m'
export gl_lv=$'\033[38;5;10m'
export gl_huang=$'\033[38;5;11m'
export gl_lan=$'\033[38;5;32m'
export gl_bai=$'\033[38;5;15m'
export gl_zi=$'\033[38;5;13m'
export gl_bufan=$'\033[38;5;14m'
export reset=$'\033[0m'
}
list_color_init()
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }
handle_invalid_input() {
echo -ne "\r\033[K${gl_huang}无效的输入,请重新输入! ${gl_zi} 1 ${gl_huang} 秒后返回"
sleep_fractional 1
echo -ne "\r\033[K${gl_lv}无效的输入,请重新输入! ${gl_zi}0${gl_lv} 秒后返回"
sleep_fractional 0.5
echo -ne "\r\033[K"
return 2
}
handle_y_n() {
echo -e "${gl_hong}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai})${gl_hong}。${gl_bai}"
sleep 1
echo -e "${gl_huang}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai})${gl_huang}。${gl_bai}"
sleep 1
echo -e "${gl_lv}无效的选择,请输入 ${gl_bai}(${gl_lv}y${gl_bai}或${gl_hong}N${gl_bai})${gl_lv}。${gl_bai}"
sleep 0.5
return 2
}
break_end() {
echo -e "${gl_lv}操作完成${gl_bai}"
echo -e "${gl_bai}按任意键继续 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"
read -r -n 1 -s -r -p ""
echo ""
clear
}
sleep_fractional() {
local seconds=$1
if sleep "$seconds" 2>/dev/null; then return 0; fi
if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
local int_seconds=$(echo "$seconds" | awk '{print int($1+0.999)}')
sleep "$int_seconds"
}
exit_animation() {
echo -ne "${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
exit_script() {
echo ""
echo -ne "${gl_hong}感谢使用,再见!${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
clear
exit 0
}
cancel_return() {
local menu_name="${1:-上一级选单}"
echo -e "${gl_lv}即将返回到 ${gl_huang}${menu_name}${gl_lv}${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"
sleep 0.6
echo ""
clear
}
install_ffmpeg() {
if command -v ffprobe &>/dev/null; then
log_ok "ffmpeg已存在,无需安装"
return 0
fi
log_info "开始自动安装ffmpeg..."
if command -v apt &>/dev/null; then
apt update -y && apt install ffmpeg -y
elif command -v dnf &>/dev/null; then
dnf install ffmpeg -y
elif command -v yum &>/dev/null; then
yum install ffmpeg -y
elif command -v pacman &>/dev/null; then
pacman -S ffmpeg --noconfirm
else
log_error "不支持当前系统包管理器,请手动安装ffmpeg"
return 1
fi
if command -v ffprobe &>/dev/null; then
log_ok "ffmpeg安装成功"
return 0
else
log_error "ffmpeg安装失败"
return 1
fi
}
classify_images() {
local src="${1:-.}"
local tolerance=0.05
local -A LANDSCAPE=(
[4x3]=1.333333 [3x2]=1.500000
[16x10]=1.600000 [16x9]=1.777778
[17x9]=1.888889 [19x10]=1.900000
[2x1]=2.000000 [18x9]=2.000000
[20x9]=2.222222 [21x9]=2.333333
[24x9]=2.666667 [32x9]=3.555556
)
local -A PORTRAIT=(
[1x1]=1.000000 [4x5]=1.250000
[3x4]=1.333333 [2x3]=1.500000
[10x16]=1.600000 [9x16]=1.777778
[6x13]=2.166667 [9x21]=2.333333
)
shopt -s nullglob nocaseglob
local files=("$src"/*.{webp,jpg,jpeg,png,bmp,jfif})
shopt -u nullglob nocaseglob
local total=${#files[@]}
clear
echo -e "${gl_zi}>>> 图片智能分类(自动识别横竖屏)${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
log_info "共 ${total} 个图片文件"
echo ""
local -A stats
local moved=0 errors=0
for f in "${files[@]}"; do
local name=$(basename "$f")
local dim=$(ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height -of csv=p=0 "$f" 2>/dev/null)
[[ -z "$dim" ]] && { ((errors++)); log_error "处理失败: $name"; continue; }
local w=${dim%%,*}
local h=${dim#*,}
local mode
if (( w > h )); then
mode="landscape"
local -n R=LANDSCAPE
local val=$(awk "BEGIN{printf\"%.10f\",$w/$h}")
else
mode="portrait"
local -n R=PORTRAIT
local val=$(awk "BEGIN{printf\"%.10f\",$h/$w}")
fi
local best_dir="other" best_diff=99
for dir in "${!R[@]}"; do
local diff=$(awk "BEGIN{d=$val-${R[$dir]};if(d<0)d=-d;print d}")
local cmp=$(awk "BEGIN{print($diff<$best_diff)?1:0}")
[[ $cmp -eq 1 ]] && { best_diff=$diff; best_dir=$dir; }
done
[[ $(awk "BEGIN{print($best_diff<$tolerance)?1:0}") -eq 0 ]] && best_dir="other"
local target="$src/$mode/$best_dir"
mkdir -p "$target"
mv "$f" "$target/"
((moved++))
local key="$mode/$best_dir"
stats[$key]=$((stats[$key]+1))
((moved % 20 == 0)) && log_info "已处理: ${moved} 个图片"
done
echo ""
echo -e "${gl_huang}>>> 分类完成,分类统计${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
log_info "统计结果:"
echo ""
for key in $(printf '%s\n' "${!stats[@]}" | sort); do
local pct=$(awk "BEGIN{printf\"%.1f\",${stats[$key]}/$total*100}")
log_info " $key: ${stats[$key]} 个图片 (${pct}%)"
done
echo ""
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
log_ok "共成功处理 ${moved} 个图片!"
[[ $errors -gt 0 ]] && log_error "处理失败: ${errors} 个"
local all_files=$(find "$src" -maxdepth 3 -type f 2>/dev/null | wc -l)
log_info "脚本目录下现存总计文件数: ${all_files} 个"
}
main(){
install_ffmpeg
local run_path="${1:-.}"
if [[ ! -d "$run_path" ]];then
log_error "目录不存在: $run_path"
exit 1
fi
classify_images "$run_path"
}
main "$@"
支持的比例
横屏比例(宽 > 高)
| 目录名 | 宽÷高 | 说明 |
|---|---|---|
| 4x3 | 1.333 | 旧式显示器、平板 |
| 3x2 | 1.5 | 标准照片(135 胶片) |
| 16x10 | 1.6 | 笔记本屏幕 |
| 16x9 | 1.778 | 标准宽屏(HDTV) |
| 17x9 | 1.889 | 超宽屏 |
| 19x10 | 1.9 | 宽幅模式(无人机) |
| 2x1 / 18x9 | 2.0 | 双倍宽屏 |
| 20x9 | 2.222 | 超超宽屏 |
| 21x9 | 2.333 | 曲面超宽屏 |
| 24x9 | 2.667 | 多屏拼接 |
| 32x9 | 3.556 | 极端宽屏 |
竖屏比例(高 ≥ 宽)
| 目录名 | 高÷宽 | 常见分辨率 | 设备示例 |
|---|---|---|---|
| 1x1 | 1.0 | 1080×1080 | 社交媒体头像 |
| 4x5 | 1.25 | 864×1080 | 打印照片(4×5 寸) |
| 3x4 | 1.333 | 810×1080 | 旧式照片竖版 |
| 2x3 | 1.5 | 720×1080 | 135 胶片竖版 |
| 10x16 | 1.6 | 675×1080 | 某些相机竖拍 |
| 9x16 | 1.778 | 1080×1920 | 传统手机竖屏 |
| 6x13 | 2.167 | 1080×2340 | 全面屏手机 |
| 9x21 | 2.333 | 1440×3360 | 超宽全面屏 |
执行后的目录结构
当前目录/
├── IMG_001.jpg (所有原始图片被移走)
├── landscape\
│ ├── 16x9\ (横屏 16:9 图片)
│ ├── 4x3\ (横屏 4:3 图片)
│ ├── 3x2\ (横屏 3:2 图片)
│ ├── 16x10\ (横屏 16:10 图片)
│ ├── 21x9\ (横屏 21:9 图片)
│ ├── 2x1\ (横屏 2:1 图片)
│ ├── other\ (非常见横屏比例)
│ └── ...
├── portrait\
│ ├── 9x16\ (竖屏 9:16 图片)
│ ├── 6x13\ (竖屏 6:13 图片)
│ ├── 9x21\ (竖屏 9:21 图片)
│ ├── 2x3\ (竖屏 2:3 图片)
│ ├── 3x4\ (竖屏 3:4 图片)
│ ├── 4x5\ (竖屏 4:5 图片)
│ ├── 1x1\ (正方形 1:1 图片)
│ ├── other\ (非常见识屏比例)
│ └── ...
自定义修改
修改容差(默认 ±5%)
PowerShell:
# 改为 ±2%
if ($bestDiff -lt 0.02) { ... }
# 改为 ±10%
if ($bestDiff -lt 0.10) { ... }
Linux:
# 改函数内的 tolerance 值
local tolerance=0.02 # ±2%
# 或
local tolerance=0.10 # ±10%
添加新比例
PowerShell:在对应数组添加条目
$landscapeRatios = @(
...
@{Name="1x1"; Ratio=1.0; Dir="1x1"} # 正方形
)
$portraitRatios = @(
...
@{Name="9x21"; Ratio=21/9; Dir="9x21"} # 超长竖屏
)
Linux:在对应 declare -A 添加键值对
local -A LANDSCAPE=(
...
[1x1]=1.0
)
local -A PORTRAIT=(
...
[9x21]=2.333333
)
修改源目录
给脚本传参数即可:
# PowerShell:直接粘贴后追加目录
& { ... } "D:\我的图片"
# Linux:修改末尾函数调用的参数
}; classify_images /path/to/images
验证结果
PowerShell:
# 查看各目录文件数
Get-ChildItem -Directory | ForEach-Object {
$count = (Get-ChildItem $_.FullName -File -Recurse).Count
"$($_.Name): $count 个文件"
}
# 检查总数
(Get-ChildItem -File -Recurse).Count
Linux:
# 查看各目录文件数
for d in landscape/*/ portrait/*/; do
printf "%-10s %d 个文件\n" "$d" "$(find "$d" -type f | wc -l)"
done
# 检查总数
find . -maxdepth 3 -type f | wc -l
常见问题
1. ffprobe 不是内部或外部命令
原因:未安装 FFmpeg 或未添加到 PATH
解决:
# Windows - 检查 ffprobe 是否可用
Get-Command ffprobe -ErrorAction SilentlyContinue
# Linux - 检查 ffprobe 是否可用
which ffprobe || echo "请安装 ffmpeg"
2. 正方形图片(1:1)如何归类?
正方形图片高度 == 宽度,脚本将其归为竖屏(portrait/1x1/)。如需改为横屏,可在逻辑处调整判断条件:if ($w -ge $h)(PowerShell)或 (( w >= h ))(Linux)。
3. 实际比例与理论比例不一致
很多手机标注 "20:9",但实际像素可能是 6:13 (2.167) 或 19.5:9。厂商宣传比例 vs 实际像素存在误差,±5% 容差可以覆盖。
功能说明
- 自动识别横竖屏:无需手动分区,脚本自动判断方向并按对应比例分类
- 20 种常见比例:横屏 12 种 + 竖屏 8 种,全面覆盖主流设备
- 双目录结构:
landscape/和portrait/分开放置,结构清晰 - 自动创建目录:首次运行自动生成对应比例的文件夹
- 原图绝对保护:仅移动文件位置,不修改任何原始图片
- 全格式兼容:支持 webp / jpg / jpeg / png / bmp / jfif
- 详细统计输出:分类完成后输出各目录数量、占比和总数验证
- 异常处理:未匹配图片归入对应方向的 other 目录
- 进度提示:每处理 20 张图片输出一次进度