随机
Enter 搜索 ↑↓ 切换 Esc 清空

ffmpeg_classify_ratio

脚本

FFmpeg 图片按宽高比自动分类(自动识别横竖屏)

ffmpeg_classify_ratio

FFmpeg 图片按宽高比自动分类(自动识别横竖屏)

补充说明

本文提供 Windows PowerShell 和 Linux Shell 脚本,用于批量识别图片宽高比和横竖屏方向,自动分类归档。脚本通过 ffprobe 获取每张图片的分辨率,自动判断横竖屏,分别匹配对应的常见比例(±5% 容差),绝不修改原始图片

通用参数说明

  • 源目录:当前目录 .(支持通过参数自定义)
  • 容差范围:±5%(匹配阈值 0.05)
  • 横屏输出:landscape/<比例>/
  • 竖屏输出:portrait/<比例>/
  • 支持格式:webp / jpg / jpeg / png / bmp / jfif

分类逻辑

  1. 扫描当前目录所有图片文件(不递归子目录)
  2. 调用 ffprobe 提取宽高分辨率
  3. 自动判断方向:宽度 > 高度 → 横屏,否则 → 竖屏
  4. 计算对应方向的比例(横屏:宽÷高,竖屏:高÷宽)
  5. 与预定义常见比例逐一比对,取最接近项
  6. 误差小于 5% 归入对应比例目录,否则归入 other 目录
  7. 横屏图片放入 landscape/ 下,竖屏图片放入 portrait/
  8. 输出分类统计结果

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% 容差可以覆盖。


功能说明

相关命令