4.2 图像专线:CV 数据采集 本节摘要:CV 数据采集的三道特有工序是图像去重、版权过滤、质量过滤。本节用感知哈希(dHash)实现近似去重并标定阈值,用 Crawl4AI 的媒体抽取管线收集图片,给出版权三层动作与完整的产品图集案例。 先看一个反例开场。某团队给商品分类模型采了八十万张图,训练指标漂亮,上线后长尾品类全趴窝。排查发现:八十万里有二十一万张是同一批主图的缩放裁切版本——模型把"见过这张图"当成了"认识这个品类"。图像与文本最大的差别就在这:文本的重复看得见,图像的重复藏在像素级别的小改动里,URL 不同不代表内容不同。 图像专线的三道特有工序 文本专线的清洗去重逻辑搬不过来,图像专线有自己的三道工序。
本节摘要:CV 数据采集的三道特有工序是图像去重、版权过滤、质量过滤。本节用感知哈希(dHash)实现近似去重并标定阈值,用 Crawl4AI 的媒体抽取管线收集图片,给出版权三层动作与完整的产品图集案例。
先看一个反例开场。某团队给商品分类模型采了八十万张图,训练指标漂亮,上线后长尾品类全趴窝。排查发现:八十万里有二十一万张是同一批主图的缩放裁切版本——模型把"见过这张图"当成了"认识这个品类"。图像与文本最大的差别就在这:文本的重复看得见,图像的重复藏在像素级别的小改动里,URL 不同不代表内容不同。
文本专线的清洗去重逻辑搬不过来,图像专线有自己的三道工序。去重要识别"同一张图的不同版本":缩放、裁切、加水印、转格式都会改字节但保感知内容,感知哈希为此而生。版权过滤是图像独有的重头戏:一张图的授权链条比一段文字复杂得多,来源标记、授权核验、禁用清单三层动作缺一不可。质量过滤按模型口径筛:分辨率下限、宽高比约束、清晰度(模糊图对检测任务是负资产)、灰度图与彩图的口径统一。
收集侧,Crawl4AI 的抓取结果自带媒体清单,图片链接按内外分组、带描述文本:
import asyncio from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, CacheMode async def collect_images(url: str, min_w: int = 200, min_h: int = 200): """抓取页面并过滤出合格图源(尺寸口径由下游模型定)""" cfg = CrawlerRunConfig(cache_mode=CacheMode.BYPASS) async with AsyncWebCrawler() as crawler: result = await crawler.arun(url, config=cfg) kept = [] for img in result.media.get("images", []): # result.media 提供链接与描述;尺寸需下载后核验,这里先按描述与格式粗筛 src = img.get("src", "") desc = img.get("alt", "") or img.get("desc", "") if not src or src.startswith("data:"): continue # 内联小图与无源图直接放弃 if any(k in desc for k in ("logo", "icon", "avatar", "banner")): continue # 描述暴露的功能图不入集 kept.append({"src": src, "desc": desc}) return kept imgs = asyncio.run(collect_images("https://product.example.com/list")) print("候选图片:", len(imgs)) # 输出:候选图片: 156 print(imgs[0]) # 输出:{'src': 'https://cdn.example.com/p/81.jpg', 'desc': '商品主图'}
粗筛的启发式值得说明:alt 描述里带 logo、icon、avatar、banner 的图,几乎一定是功能图而非内容图,这层过滤零成本砍掉三成噪声。
字节哈希(MD5 一类)只能拦一模一样的文件,改一个像素就漏网。感知哈希的思路是把图缩到极小、比结构不比像素——图像内容的"骨架"变了哈希才变。dHash 是最易实现的一种:缩到九乘八灰度图,比较相邻像素亮度得到六十四位指纹:
from PIL import Image def dhash(path: str, size: int = 9) -> int: """差异哈希:横向相邻像素亮度比较,合成64位指纹""" img = Image.open(path).convert("L").resize((size + 1, size)) pixels = list(img.getdata()) bits = 0 for row in range(size): for col in range(size): left = pixels[row * (size + 1) + col] right = pixels[row * (size + 1) + col + 1] bits = (bits << 1) | (1 if left > right else 0) return bits def hamming_hex(a: int, b: int) -> int: return bin(a ^ b).count("1") def dedup_images(paths: list[str], threshold: int = 10) -> list[str]: """按 dHash 汉明距离去重,保留每组第一张""" kept, fps = [], [] for p in paths: fp = dhash(p) if all(hamming_hex(fp, k) > threshold for k in fps): kept.append(p) fps.append(fp) return kept paths = ["img_a.jpg", "img_a_resized.png", "img_a_watermarked.jpg", "img_b.jpg"] # img_a 的缩放版与水印版指纹距离分别约2与5,img_b 距离约30 print("去重后保留:", dedup_images(paths)) # 输出:去重后保留: ['img_a.jpg', 'img_b.jpg']
阈值 10 是常见起点,标定方法拿真实数据扫:同类变体对的距离分布与不同图对的距离分布拉开最宽的那条线就是好阈值。水印密集、裁切激进的源要放宽,但超过 16 的阈值基本失效——那已经在删不同内容的图了。
背景:家居电商要为"风格分类"模型建图库,候选源有厂商素材站、设计师社区、图片分享平台,目标十万张带风格标签的图。
操作:版权三层动作贯穿采集全程。第一层来源标记:每张图记录来源域名与页面 URL,进入 3.4 节的分区键;第二层授权核验:素材站按其授权条款筛选可用范围,分享平台默认只收明确开放授权的子集;第三层禁用清单:法务给的受限来源清单在入库前硬过滤。去重管线在下载后立即执行,八万一千张候选砍到五万九千张。
def license_gate(img_meta: dict, banned_domains: set[str], open_license_marks: tuple = ("cc0", "cco", "public domain")) -> str: """版权三层判定:禁用清单最硬,其次授权标记,默认存疑""" domain = img_meta["src"].split("/")[2] if any(d in domain for d in banned_domains): return "拒绝" # 第三层:法务禁用清单 if img_meta.get("license", "").lower() in open_license_marks: return "入库" # 第二层:明确开放授权 if "素材站" in img_meta.get("source_type", ""): return "入库" # 第二层:按素材站条款允许的范围 return "人工复核" # 存疑进复核池,不默认放行 meta = {"src": "https://cdn.share.example/img/1.jpg", "license": "CC0", "source_type": "分享平台"} print(license_gate(meta, banned_domains={"restricted.example"})) # 输出:入库
结果:五万九千张入库图里,人工复核池只占百分之七,两个月后模型上线,长尾风格准确率比第一版反例方案高出三十一分——差距主要来自数据纯净度而非模型结构。
解读:案例里真正的功臣是两道闸:感知去重砍掉了三分之一的水分,版权判定让"人工复核"成为有界的池子而不是无限的债务。图像专线的成本结构里,这两道闸的投入回报率最高。
变式:视频专线可以复用同一套思路——关键帧抽帧后按 dHash 去重,镜头切换检测换成帧间指纹距离突变判定。
图像专线收工。下一节的行为专线面对的是另一种数据:它不在页面上,而在页面之间的轨迹里。