最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Python的重復(fù)圖片刪除工具

 更新時(shí)間:2025年12月22日 09:34:54   作者:feeday  
刪除文件夾中相似圖片的任務(wù)比較復(fù)雜,因?yàn)樾枰x“相似”的標(biāo)準(zhǔn),并且這通常涉及到圖像內(nèi)容分析,下面是一個(gè)基本的步驟說明,展示如何使用Python和這些庫來識(shí)別并刪除相似圖片,需要的朋友可以參考下

Python 刪除重復(fù)圖片(MD5 + pHash + ORB 圖像識(shí)別)最終版:改目錄直接運(yùn)行(支持子文件夾/中文路徑/導(dǎo)出清單/先移動(dòng)再刪)

很多人“刪除重復(fù)圖片”只會(huì)用 MD5:確實(shí)能刪完全一樣的文件,但遇到這些情況就失效:

  • 同一張圖被壓縮過(清晰度變了)
  • 分辨率不同(縮放過)
  • 截圖邊緣裁了一點(diǎn)點(diǎn)(很像,但不是同文件)

所以這篇給你一套更穩(wěn)、更接近“圖像識(shí)別”的去重方案

? MD5:刪“文件內(nèi)容完全相同”的重復(fù)圖(最快最準(zhǔn))
? pHash(感知哈希):快速篩出“看起來很像”的候選
? ORB 圖像識(shí)別(二次確認(rèn)):關(guān)鍵點(diǎn)特征匹配,減少 pHash 誤判(更像“識(shí)圖”)

并且腳本做到:

  • 單文件:改目錄即可跑
  • 依賴寫在 py 里:缺庫自動(dòng) pip 安裝(也支持設(shè)置國內(nèi)鏡像)
  • 安全:默認(rèn)不直接刪,先移動(dòng)到 _duplicates/,并導(dǎo)出 dedup_report.csv(可回溯)
  • 支持中文路徑:Windows 下中文目錄/中文文件名可讀

1)使用方法(最簡單)

第一步:保存腳本

把本文下面整段代碼保存為:dedup_images_run.py

第二步:只改一個(gè)地方

打開文件,修改:

ROOT_DIR = r"D:\imgs"

改成你的圖片目錄。

第三步:直接運(yùn)行

python dedup_images_run.py

2)腳本會(huì)輸出什么?

運(yùn)行結(jié)束后,你會(huì)在 ROOT_DIR 下看到:

  • dedup_report.csv:每一條重復(fù)判定記錄(保留圖、重復(fù)圖、原因、距離/匹配度、移動(dòng)到哪)
  • _duplicates/:被判定為重復(fù)的圖片(默認(rèn)移動(dòng)到這里,保持原目錄結(jié)構(gòu))

確認(rèn)無誤后,你再?zèng)Q定是否要把 ACTION 改成 delete 直接刪除。

3)閾值怎么調(diào)(最常見問題)

pHash 閾值PHASH_TH

  • 越小越嚴(yán)格(誤判少,但漏判多)
  • 常用范圍:6~10
  • 推薦默認(rèn):8

ORB 圖像識(shí)別閾值(更關(guān)鍵)

ORB 用兩個(gè)條件同時(shí)判斷“足夠相似”:

  • ORB_MIN_GOOD_MATCHES:好匹配點(diǎn)數(shù)量(建議 15~60)
  • ORB_SIMILARITY_THgood_matches / min(kp1, kp2) 的比例(建議 0.08~0.20)

如果你感覺重復(fù)圖沒抓到(太嚴(yán)格)

  • ORB_MIN_GOOD_MATCHES: 25 → 15
  • ORB_SIMILARITY_TH: 0.12 → 0.08

如果你感覺誤判太多(太寬松)

  • ORB_MIN_GOOD_MATCHES: 25 → 40
  • ORB_SIMILARITY_TH: 0.12 → 0.18
  • 同時(shí)把 PHASH_TH: 8 → 6

4)完整代碼(單文件可運(yùn)行,含自動(dòng)安裝依賴 + 圖像識(shí)別 ORB)

說明:依賴安裝“寫在 py 里”并不等于不需要聯(lián)網(wǎng)。第一次運(yùn)行如果缺庫,腳本會(huì)自動(dòng) pip 安裝;如果機(jī)器完全離線,請(qǐng)?zhí)崆把b好依賴或把依賴 wheel 放到本地。

# -*- coding: utf-8 -*-
"""
dedup_images_run.py
Python 批量刪除重復(fù)圖片(MD5 + pHash + ORB 圖像識(shí)別)—— 單文件可直接運(yùn)行版
? 你只需要改 ROOT_DIR,然后 python 運(yùn)行即可
? 不用手動(dòng) pip install:腳本會(huì)自動(dòng)檢測缺少的庫并安裝
? 支持子文件夾遞歸
? 支持中文路徑(Windows)
? 默認(rèn)不直接刪除:先移動(dòng)到 _duplicates/(更安全)
? 導(dǎo)出 dedup_report.csv,方便回溯
去重策略(推薦):
1) MD5:完全重復(fù)(內(nèi)容一模一樣)——最快最準(zhǔn)
2) pHash:近似重復(fù)(縮放/壓縮/輕微變化)——快速篩選候選
3) ORB 圖像識(shí)別:關(guān)鍵點(diǎn)特征匹配——二次確認(rèn),減少誤判,更像“識(shí)圖”
依賴(自動(dòng)安裝):
- opencv-python(cv2)
- numpy
- tqdm
注意:
- 第一次自動(dòng)安裝 opencv-python 可能較慢
- 如在國內(nèi)網(wǎng)絡(luò)環(huán)境,可設(shè)置 PIP_INDEX_URL 為鏡像
"""
 
import os
import sys
import csv
import time
import shutil
import hashlib
import subprocess
import importlib.util
from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional
 
 
# =========================================================
# 1) 配置區(qū):你只需要改 ROOT_DIR
# =========================================================
ROOT_DIR = r"C:\feeday\imgTest"          # <<< 改成你的圖片目錄
RECURSIVE = True               # 是否遞歸子文件夾
 
# 模式:
# - "md5"              只做完全重復(fù)
# - "phash"            只做近似重復(fù)(可能誤判多一點(diǎn))
# - "md5+phash"        先md5后phash(推薦)
# - "md5+phash+orb"    先md5后phash,再用ORB圖像識(shí)別二次確認(rèn)(強(qiáng)烈推薦)
MODE = "md5+phash+orb"
 
PHASH_TH = 8                   # pHash 漢明距離閾值:常用 6~10,默認(rèn) 8
 
# ORB 圖像識(shí)別參數(shù)(用于“確認(rèn)相似”)
USE_ORB = True                 # MODE 包含 orb 時(shí)會(huì)自動(dòng)啟用,這里留著方便你手動(dòng)開關(guān)
ORB_NFEATURES = 1200           # ORB 特征點(diǎn)數(shù)量:800~2000 常用
ORB_RATIO_TEST = 0.75          # 0.70~0.80,越小越嚴(yán)格
ORB_MIN_GOOD_MATCHES = 25      # 至少多少個(gè)“好匹配”才算重復(fù)(建議 15~60)
ORB_SIMILARITY_TH = 0.12       # good_matches / min(kp1,kp2) 的比例閾值(建議 0.08~0.20)
ORB_MAX_IMAGE_EDGE = 1200      # 識(shí)別時(shí)把大圖等比縮小到最長邊(提速)
 
ACTION = "move"                # "move" / "delete" / "none"
MOVE_TO = "_duplicates"        # move 時(shí)重復(fù)圖片存放目錄(相對(duì) ROOT_DIR)
REPORT_NAME = "dedup_report.csv"  # 報(bào)告文件名(相對(duì) ROOT_DIR)
DRY_RUN = False                # True=只生成報(bào)告不移動(dòng)/不刪除
 
PREFER_KEEP = "area"           # 保留策略: "area"(分辨率優(yōu)先) / "size" / "newest" / "oldest"
 
# 自動(dòng)安裝依賴
AUTO_INSTALL = True
 
# 如在國內(nèi)網(wǎng)絡(luò)環(huán)境可設(shè)置鏡像(可選)
# 例如:PIP_INDEX_URL = "https://mirrors.tencent.com/pypi/simple/"
PIP_INDEX_URL = ""
 
 
# =========================================================
# 2) 自動(dòng)安裝依賴(寫在 py 里)
# =========================================================
def _module_exists(module_name: str) -> bool:
    return importlib.util.find_spec(module_name) is not None
 
def ensure_packages():
    """
    檢測并自動(dòng)安裝依賴庫:opencv-python / numpy / tqdm
    """
    if not AUTO_INSTALL:
        return
 
    need_install = []
 
    req = [
        ("cv2", "opencv-python"),
        ("numpy", "numpy"),
        ("tqdm", "tqdm"),
    ]
 
    for mod, pkg in req:
        if not _module_exists(mod):
            need_install.append(pkg)
 
    if not need_install:
        return
 
    print("?? 檢測到缺少依賴,將自動(dòng)安裝:", ", ".join(need_install))
    cmd = [sys.executable, "-m", "pip", "install", "--upgrade"] + need_install
 
    if PIP_INDEX_URL.strip():
        cmd += ["-i", PIP_INDEX_URL.strip()]
 
    print("?? 執(zhí)行:", " ".join(cmd))
    try:
        subprocess.check_call(cmd)
        print("? 依賴安裝完成")
    except subprocess.CalledProcessError as e:
        print("? 自動(dòng)安裝失?。?, e)
        print("你可以手動(dòng)執(zhí)行:")
        if PIP_INDEX_URL.strip():
            print(f'  {sys.executable} -m pip install -U {" ".join(need_install)} -i {PIP_INDEX_URL.strip()}')
        else:
            print(f'  {sys.executable} -m pip install -U {" ".join(need_install)}')
        sys.exit(1)
 
ensure_packages()
 
import numpy as np
import cv2
from tqdm import tqdm
 
 
# =========================================================
# 3) 主邏輯
# =========================================================
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
 
 
def imread_unicode(path: str) -> Optional[np.ndarray]:
    """
    OpenCV 兼容中文路徑讀取
    """
    try:
        data = np.fromfile(path, dtype=np.uint8)
        img = cv2.imdecode(data, cv2.IMREAD_COLOR)
        return img
    except Exception:
        return None
 
 
def get_image_hw(path: str) -> Tuple[int, int]:
    img = imread_unicode(path)
    if img is None:
        return 0, 0
    h, w = img.shape[:2]
    return int(h), int(w)
 
 
def file_md5(path: str, chunk_size: int = 1024 * 1024) -> str:
    md5 = hashlib.md5()
    with open(path, "rb") as f:
        while True:
            b = f.read(chunk_size)
            if not b:
                break
            md5.update(b)
    return md5.hexdigest()
 
 
def phash_64(path: str) -> Optional[int]:
    """
    pHash 64-bit:灰度->32x32->DCT->取8x8低頻->與中位數(shù)比較->64bit
    """
    img = imread_unicode(path)
    if img is None:
        return None
    try:
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        gray = cv2.resize(gray, (32, 32), interpolation=cv2.INTER_AREA)
        gray = np.float32(gray)
 
        dct = cv2.dct(gray)
        dct_low = dct[:8, :8].copy()
 
        flat = dct_low.flatten()
        flat_no_dc = flat[1:]  # 去掉 DC
        median = np.median(flat_no_dc)
 
        bits = (flat > median).astype(np.uint8)
 
        h = 0
        for b in bits:
            h = (h << 1) | int(b)
        return int(h)
    except Exception:
        return None
 
 
def hamming_distance_64(a: int, b: int) -> int:
    return int((a ^ b).bit_count())
 
 
# -----------------------------
# ORB 圖像識(shí)別(關(guān)鍵點(diǎn)特征匹配)
# -----------------------------
_orb = None
_bf = None
_orb_cache: Dict[str, Tuple[int, Optional[np.ndarray]]] = {}
 
def _get_orb():
    global _orb
    if _orb is None:
        _orb = cv2.ORB_create(nfeatures=int(ORB_NFEATURES))
    return _orb
 
def _get_bf():
    global _bf
    if _bf is None:
        _bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
    return _bf
 
def _resize_for_orb(img: np.ndarray) -> np.ndarray:
    h, w = img.shape[:2]
    m = max(h, w)
    if m <= ORB_MAX_IMAGE_EDGE:
        return img
    scale = ORB_MAX_IMAGE_EDGE / float(m)
    new_w = max(1, int(w * scale))
    new_h = max(1, int(h * scale))
    return cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
 
def orb_signature(path: str) -> Tuple[int, Optional[np.ndarray]]:
    """
    返回:(keypoints_count, descriptors)
    descriptors 可能為 None(例如純色圖/讀圖失?。?
    """
    if path in _orb_cache:
        return _orb_cache[path]
 
    img = imread_unicode(path)
    if img is None:
        _orb_cache[path] = (0, None)
        return (0, None)
 
    try:
        img = _resize_for_orb(img)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        orb = _get_orb()
        kps, des = orb.detectAndCompute(gray, None)
        kp_count = 0 if kps is None else len(kps)
        _orb_cache[path] = (kp_count, des)
        return _orb_cache[path]
    except Exception:
        _orb_cache[path] = (0, None)
        return (0, None)
 
def orb_similarity(path_a: str, path_b: str) -> Tuple[int, float]:
    """
    計(jì)算 ORB 相似度:
    - good_matches:通過 ratio test 的好匹配數(shù)量
    - similarity:good_matches / min(kp_count_a, kp_count_b)
    """
    kp_a, des_a = orb_signature(path_a)
    kp_b, des_b = orb_signature(path_b)
 
    if des_a is None or des_b is None:
        return 0, 0.0
    if kp_a <= 0 or kp_b <= 0:
        return 0, 0.0
 
    try:
        bf = _get_bf()
        matches = bf.knnMatch(des_a, des_b, k=2)
 
        good = 0
        ratio = float(ORB_RATIO_TEST)
        for m_n in matches:
            if len(m_n) < 2:
                continue
            m, n = m_n[0], m_n[1]
            if m.distance < ratio * n.distance:
                good += 1
 
        denom = max(1, min(kp_a, kp_b))
        sim = good / float(denom)
        return good, float(sim)
    except Exception:
        return 0, 0.0
 
 
@dataclass
class ImgInfo:
    path: str
    size: int
    w: int
    h: int
 
    @property
    def area(self) -> int:
        return self.w * self.h
 
 
def choose_keep(a: ImgInfo, b: ImgInfo, prefer: str) -> Tuple[ImgInfo, ImgInfo]:
    if prefer == "area":
        if a.area != b.area:
            return (a, b) if a.area > b.area else (b, a)
        if a.size != b.size:
            return (a, b) if a.size > b.size else (b, a)
        return (a, b)
 
    if prefer == "size":
        if a.size != b.size:
            return (a, b) if a.size > b.size else (b, a)
        if a.area != b.area:
            return (a, b) if a.area > b.area else (b, a)
        return (a, b)
 
    if prefer in {"newest", "oldest"}:
        ta = os.path.getmtime(a.path)
        tb = os.path.getmtime(b.path)
        if prefer == "newest":
            return (a, b) if ta >= tb else (b, a)
        else:
            return (a, b) if ta <= tb else (b, a)
 
    return choose_keep(a, b, "area")
 
 
def iter_images(root: str, recursive: bool) -> List[str]:
    root = os.path.abspath(root)
    out = []
    if recursive:
        for dp, _, fns in os.walk(root):
            for fn in fns:
                ext = os.path.splitext(fn)[1].lower()
                if ext in IMAGE_EXTS:
                    out.append(os.path.join(dp, fn))
    else:
        for fn in os.listdir(root):
            p = os.path.join(root, fn)
            if os.path.isfile(p):
                ext = os.path.splitext(fn)[1].lower()
                if ext in IMAGE_EXTS:
                    out.append(p)
    return out
 
 
def ensure_dir(p: str) -> None:
    os.makedirs(p, exist_ok=True)
 
 
def safe_move(src: str, dst: str) -> str:
    base, ext = os.path.splitext(dst)
    final = dst
    i = 1
    while os.path.exists(final):
        final = f"{base}({i}){ext}"
        i += 1
    ensure_dir(os.path.dirname(final))
    shutil.move(src, final)
    return final
 
 
def write_report_csv(report_path: str, rows: List[dict]) -> None:
    ensure_dir(os.path.dirname(report_path) if os.path.dirname(report_path) else ".")
    fieldnames = [
        "time", "mode",
        "keep_path", "dup_path",
        "reason", "value",
        "phash_distance",
        "orb_good_matches", "orb_similarity",
        "keep_w", "keep_h", "keep_size",
        "dup_w", "dup_h", "dup_size",
        "dup_new_path",
    ]
    with open(report_path, "w", newline="", encoding="utf-8-sig") as f:
        w = csv.DictWriter(f, fieldnames=fieldnames)
        w.writeheader()
        for r in rows:
            w.writerow(r)
 
 
def dedup_by_md5(paths: List[str], prefer_keep: str) -> List[Tuple[str, str, str]]:
    """
    返回重復(fù)對(duì):(dup_path, keep_path, md5)
    """
    md5_map: Dict[str, ImgInfo] = {}
    dups: List[Tuple[str, str, str]] = []
 
    for p in tqdm(paths, desc="MD5 掃描", unit="img"):
        try:
            h = file_md5(p)
        except Exception:
            continue
 
        try:
            size = os.path.getsize(p)
        except Exception:
            size = 0
        hh, ww = get_image_hw(p)
        cur = ImgInfo(path=p, size=size, w=ww, h=hh)
 
        if h not in md5_map:
            md5_map[h] = cur
        else:
            old = md5_map[h]
            keep, drop = choose_keep(old, cur, prefer_keep)
            md5_map[h] = keep
            dups.append((drop.path, keep.path, h))
 
    return dups
 
 
def dedup_by_phash_with_orb(
    paths: List[str],
    prefer_keep: str,
    phash_th: int,
    use_orb: bool
) -> List[Tuple[str, str, str, int, int, float]]:
    """
    返回重復(fù)對(duì):
      (dup_path, keep_path, phash_hex, phash_dist, orb_good, orb_sim)
    說明:
    - 先用 pHash 快速篩選候選(phash_dist <= phash_th)
    - 若啟用 ORB,則對(duì)候選進(jìn)行二次“圖像識(shí)別”確認(rèn)
    """
    hashes: Dict[str, int] = {}
    infos: Dict[str, ImgInfo] = {}
 
    for p in tqdm(paths, desc="pHash 計(jì)算", unit="img"):
        ph = phash_64(p)
        if ph is None:
            continue
        hashes[p] = ph
        try:
            size = os.path.getsize(p)
        except Exception:
            size = 0
        hh, ww = get_image_hw(p)
        infos[p] = ImgInfo(path=p, size=size, w=ww, h=hh)
 
    # 分桶加速:用高16位分桶
    buckets: Dict[int, List[str]] = {}
    for p, ph in hashes.items():
        key = (ph >> 48) & 0xFFFF
        buckets.setdefault(key, []).append(p)
 
    keep_map: Dict[str, str] = {p: p for p in hashes.keys()}
    dups: List[Tuple[str, str, str, int, int, float]] = []
 
    for key in tqdm(list(buckets.keys()), desc="pHash 分桶比對(duì)", unit="bucket"):
        group = buckets[key]
        if len(group) <= 1:
            continue
 
        for i in range(len(group)):
            a = group[i]
            a_keep = keep_map.get(a, a)
            if a_keep not in hashes:
                continue
 
            for j in range(i + 1, len(group)):
                b = group[j]
                b_keep = keep_map.get(b, b)
                if b_keep not in hashes:
                    continue
 
                ha = hashes[a_keep]
                hb = hashes[b_keep]
                dist = hamming_distance_64(ha, hb)
 
                # pHash 先篩候選
                if dist > phash_th:
                    continue
 
                orb_good = 0
                orb_sim = 0.0
                if use_orb:
                    orb_good, orb_sim = orb_similarity(a_keep, b_keep)
 
                    # ORB 二次確認(rèn)門檻:兩個(gè)條件都滿足才算重復(fù)
                    if orb_good < int(ORB_MIN_GOOD_MATCHES):
                        continue
                    if orb_sim < float(ORB_SIMILARITY_TH):
                        continue
 
                ia = infos.get(a_keep)
                ib = infos.get(b_keep)
                if ia is None or ib is None:
                    continue
 
                keep, drop = choose_keep(ia, ib, prefer_keep)
                keep_map[keep.path] = keep.path
                keep_map[drop.path] = keep.path
 
                ph_hex = f"{hashes[keep.path]:016x}"
                dups.append((drop.path, keep.path, ph_hex, dist, orb_good, orb_sim))
 
    return dups
 
 
def main():
    root = os.path.abspath(ROOT_DIR)
    if not os.path.isdir(root):
        print(f"? 目錄不存在:{root}")
        return
 
    t0 = time.time()
    paths = iter_images(root, RECURSIVE)
    if not paths:
        print("?? 未找到圖片文件。")
        return
 
    duplicates_dir = os.path.join(root, MOVE_TO)
    report_path = os.path.join(root, REPORT_NAME)
 
    mode = MODE.strip().lower()
    use_orb = (("orb" in mode) and USE_ORB)
 
    print("=================================================")
    print("?? 根目錄:", root)
    print("?? 圖片數(shù)量:", len(paths))
    print(f"?? 模式:{MODE} | pHash閾值:{PHASH_TH} | 保留策略:{PREFER_KEEP}")
    print(f"?? 動(dòng)作:{ACTION} | dry-run:{DRY_RUN}")
    if use_orb:
        print("?? ORB 圖像識(shí)別:啟用")
        print(f"   ORB_NFEATURES={ORB_NFEATURES}, RATIO={ORB_RATIO_TEST}, MIN_GOOD={ORB_MIN_GOOD_MATCHES}, SIM_TH={ORB_SIMILARITY_TH}")
    else:
        print("?? ORB 圖像識(shí)別:未啟用")
    print("=================================================")
 
    report_rows: List[dict] = []
    processed = 0
 
    # 1) MD5
    md5_dups: List[Tuple[str, str, str]] = []
    remain_after_md5 = paths
 
    if mode in {"md5", "md5+phash", "md5+phash+orb"} or ("md5" in mode):
        md5_dups = dedup_by_md5(paths, PREFER_KEEP)
        dup_set = set([d[0] for d in md5_dups])
        remain_after_md5 = [p for p in paths if p not in dup_set]
 
    # 2) pHash (+ ORB)
    phash_dups: List[Tuple[str, str, str, int, int, float]] = []
    if ("phash" in mode):
        phash_dups = dedup_by_phash_with_orb(
            remain_after_md5,
            prefer_keep=PREFER_KEEP,
            phash_th=PHASH_TH,
            use_orb=use_orb
        )
 
    def do_action(dup_path: str) -> str:
        nonlocal processed
        if DRY_RUN or ACTION == "none":
            return ""
        if not os.path.exists(dup_path):
            return ""
 
        if ACTION == "delete":
            try:
                os.remove(dup_path)
                processed += 1
                return ""
            except Exception:
                return ""
 
        if ACTION == "move":
            try:
                rel = os.path.relpath(dup_path, root)
                dst = os.path.join(duplicates_dir, rel)
                newp = safe_move(dup_path, dst)
                processed += 1
                return newp
            except Exception:
                return ""
 
        return ""
 
    # 記錄 MD5
    for dup_path, keep_path, md5v in md5_dups:
        if not os.path.exists(dup_path):
            continue
 
        keep_h, keep_w = get_image_hw(keep_path)
        dup_h, dup_w = get_image_hw(dup_path)
        keep_size = os.path.getsize(keep_path) if os.path.exists(keep_path) else 0
        dup_size = os.path.getsize(dup_path) if os.path.exists(dup_path) else 0
 
        new_path = do_action(dup_path)
 
        report_rows.append({
            "time": time.strftime("%Y-%m-%d %H:%M:%S"),
            "mode": "md5",
            "keep_path": keep_path,
            "dup_path": dup_path,
            "reason": "MD5 完全重復(fù)",
            "value": md5v,
            "phash_distance": "",
            "orb_good_matches": "",
            "orb_similarity": "",
            "keep_w": keep_w, "keep_h": keep_h, "keep_size": keep_size,
            "dup_w": dup_w, "dup_h": dup_h, "dup_size": dup_size,
            "dup_new_path": new_path,
        })
 
    # 記錄 pHash(+ORB)
    for dup_path, keep_path, ph_hex, ph_dist, orb_good, orb_sim in phash_dups:
        if not os.path.exists(dup_path):
            continue
 
        keep_h, keep_w = get_image_hw(keep_path)
        dup_h, dup_w = get_image_hw(dup_path)
        keep_size = os.path.getsize(keep_path) if os.path.exists(keep_path) else 0
        dup_size = os.path.getsize(dup_path) if os.path.exists(dup_path) else 0
 
        new_path = do_action(dup_path)
 
        reason = "pHash 近似重復(fù)"
        if use_orb:
            reason = "pHash + ORB 圖像識(shí)別確認(rèn)"
 
        report_rows.append({
            "time": time.strftime("%Y-%m-%d %H:%M:%S"),
            "mode": "phash+orb" if use_orb else "phash",
            "keep_path": keep_path,
            "dup_path": dup_path,
            "reason": reason,
            "value": ph_hex,
            "phash_distance": ph_dist,
            "orb_good_matches": orb_good if use_orb else "",
            "orb_similarity": f"{orb_sim:.4f}" if use_orb else "",
            "keep_w": keep_w, "keep_h": keep_h, "keep_size": keep_size,
            "dup_w": dup_w, "dup_h": dup_h, "dup_size": dup_size,
            "dup_new_path": new_path,
        })
 
    write_report_csv(report_path, report_rows)
 
    dt = time.time() - t0
    print("")
    print("? 完成")
    print(f"?? 報(bào)告:{report_path}")
    if ACTION == "move":
        print(f"?? 重復(fù)文件目錄:{duplicates_dir}(保持原目錄結(jié)構(gòu))")
    print(f"?? 識(shí)別重復(fù)條目:{len(report_rows)}")
    if DRY_RUN or ACTION == "none":
        print("?? dry-run/none:未移動(dòng)/未刪除任何文件")
    else:
        print(f"?? 實(shí)際處理文件數(shù)(移動(dòng)/刪除):{processed}")
    print(f"? 用時(shí):{dt:.2f} 秒")
 
 
if __name__ == "__main__":
    main()

到此這篇關(guān)于基于Python的重復(fù)圖片刪除工具的文章就介紹到這了,更多相關(guān)Python重復(fù)圖片刪除內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Python從零開始訓(xùn)練AI模型的實(shí)用教程

    Python從零開始訓(xùn)練AI模型的實(shí)用教程

    本文介紹了如何使用Python從零開始訓(xùn)練自己的AI模型,包括確定問題和數(shù)據(jù)集、數(shù)據(jù)預(yù)處理、構(gòu)建模型、訓(xùn)練模型、評(píng)估和調(diào)優(yōu)模型以及部署和應(yīng)用模型等步驟
    2025-02-02
  • Python開發(fā)之Nginx+uWSGI+virtualenv多項(xiàng)目部署教程

    Python開發(fā)之Nginx+uWSGI+virtualenv多項(xiàng)目部署教程

    這篇文章主要介紹了Python系列之-Nginx+uWSGI+virtualenv多項(xiàng)目部署教程,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-05-05
  • Python入門篇之列表和元組

    Python入門篇之列表和元組

    Python包含6種內(nèi)建序列:列表、元組、字符串、Unicode字符串、buffer對(duì)象、xrange對(duì)象。本篇主要討論最常用的兩種類型:列表、元組
    2014-10-10
  • Python內(nèi)存溢出問題解決

    Python內(nèi)存溢出問題解決

    本文主要介紹了Python內(nèi)存溢出問題解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2026-02-02
  • Django框架設(shè)置cookies與獲取cookies操作詳解

    Django框架設(shè)置cookies與獲取cookies操作詳解

    這篇文章主要介紹了Django框架設(shè)置cookies與獲取cookies操作,結(jié)合實(shí)例形式詳細(xì)分析了Django框架針對(duì)cookie操作的各種常見技巧與操作注意事項(xiàng),需要的朋友可以參考下
    2019-05-05
  • Python實(shí)現(xiàn)字符串中某個(gè)字母的替代功能

    Python實(shí)現(xiàn)字符串中某個(gè)字母的替代功能

    小編想實(shí)現(xiàn)這樣一個(gè)功能:將輸入字符串中的字母 “i” 變成字母 “p”。想著很簡單,怎么實(shí)現(xiàn)呢?下面小編給大家?guī)砹薖ython實(shí)現(xiàn)字符串中某個(gè)字母的替代功能,感興趣的朋友一起看看吧
    2019-10-10
  • 在paddle中安裝python-bidi出錯(cuò)問題及解決

    在paddle中安裝python-bidi出錯(cuò)問題及解決

    在Paddle中安裝python-bidi時(shí)遇到Rust和Cargo缺失的問題,通過安裝舊版本的python-bidi(0.4.0)解決了問題
    2025-02-02
  • Python多線程實(shí)現(xiàn)支付模擬請(qǐng)求過程解析

    Python多線程實(shí)現(xiàn)支付模擬請(qǐng)求過程解析

    這篇文章主要介紹了python多線程實(shí)現(xiàn)支付模擬請(qǐng)求過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-04-04
  • python基于pyppeteer制作PDF文件

    python基于pyppeteer制作PDF文件

    這篇文章主要介紹了python基于pyppeteer制作PDF文件的方法,幫助大家更好的理解和學(xué)習(xí)使用python,感興趣的朋友可以了解下
    2021-03-03
  • Python神器之使用watchdog監(jiān)控文件變化

    Python神器之使用watchdog監(jiān)控文件變化

    這篇文章主要為大家詳細(xì)介紹了Python中的神器watchdog以及如何使用watchdog監(jiān)控文件變化,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下
    2023-12-12

最新評(píng)論

托里县| 鄂托克前旗| 张家港市| 巴青县| 延吉市| 塔城市| 江北区| 阜城县| 南召县| 华池县| 宣武区| 邵武市| 仁化县| 岳池县| 泾源县| 禄劝| 兰坪| 深水埗区| 肃南| 罗江县| 天等县| 安陆市| 谢通门县| 高陵县| 宜州市| 安庆市| 柳林县| 瓮安县| 五指山市| 尉氏县| 龙州县| 锡林郭勒盟| 红河县| 夏津县| 太和县| 浪卡子县| 乐业县| 怀宁县| 昭苏县| 莱州市| 琼海市|