简介:一款专为摄影师设计的本地AI选片工具,基于VisionCull Pro源码与部署文档打包,面向需要快速筛除跑焦、闭眼等废片的摄影从业者。工具完全在本地完成视觉计算,无需联网或上传照片,保护商业摄影隐私;内置Laplacian方差检测,可自动识别失焦与动态模糊画面,并通过三档动态阈值适应不同拍摄场景。压缩包共38个文件,核心由Python分析脚本、Node.js主进程、Vite/React前端(jsx/js/css)及tailwind、manifest等配置组成,整体仅108KB,结构清晰,便于二次开发或直接部署。已有96人学习下载,资源内含README安装部署教程、可运行的完整前后端源码、测试照片及XMP星级标签分发逻辑,能够帮助读者快速搭建属于自己的本地智能选片工作台,并在不破坏源文件的前提下自动归档合格照片。
1. 本地AI选片工具VisionCull:全量废片粗筛,比肉眼快一个量级
拍一场婚礼,两千多张RAW往电脑里一倒,回看时发现跑焦的、闭眼的、抓拍糊掉的人像占了快两成。以前我只能咬牙把图在Lightroom里过三遍,第一遍删废片、第二遍定初选、第三遍才真正修图。VisionCull这套本地AI选片工具解决的就是第一遍的粗筛:用深度学习模型批量判定合焦状态、闭眼状态,再按规则给照片写入兼容Lightroom的XMP星级标签,原图一个字都不动。跑焦的、闭眼的直接降星,预留备选的在Topaz里救一救,剩下的才是你真正需要坐下来的照片。适合婚礼跟拍、活动记录、影楼选片这种单次产出量很大的场景,全程本地推理,不用把照片传给别人看。
2. 废片判定逻辑与XMP兼容原理:先搞懂工具在做什么
2.1 跑焦不是“模糊”两个字能概括的:空间域与频域的两种检测思路
跑焦和手抖模糊在视觉上都是“糊”,但成因不同。手抖是全局运动模糊,跑焦是焦点平面偏移,主体锐度掉下去、焦外还是清晰的。VisionCull源码里默认的检测器是轻量卷积模型,输入一张下采样到224x224的图,输出一个对焦置信度分数,阈值低于设定值就打上“跑焦”标记。
想验证模型行为的时候,我一般会打开源码里的传统检测开关,它会同时算拉普拉斯方差。这个算子把图像和3x3拉普拉斯核做卷积,响应的方差越大、边缘越锐利、画面越“合焦”。代码路径长这样:
import cv2 import numpy as np def laplacian_variance(img_path): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (1024, 768), interpolation=cv2.INTER_AREA) return cv2.Laplacian(img, cv2.CV_64F).var() score = laplacian_variance("DSC01001.CR2.jpg") print(f"laplacian variance: {score:.2f}")上面这段先把原始RAW的预览图转灰度、统一缩放到1024宽,再用Laplacian算子算方差。cv2.CV_64F保证输出不做饱和截断,方差值越大画面越锐利。对室外人像大概的经验线是:方差低于80基本就是跑焦或严重噪点涂抹,高于180算清晰。这个传统分数在VisionCull里不是最终依据,而是和模型置信度加权融合,用来给“模型拿不准”的区间做兜底。
2.2 XMP旁车文件:为什么选片结果能无损进Lightroom
Lightroom不把星级、关键字、调色参数写进RAW原始数据里,而是写进一个以.xmp结尾的旁车文件。旁边放着一个DSC01001.CR2,就应该配一个DSC01001.xmp,里面是一段RDF格式的XML,记录元数据与编辑历史。VisionCull写入的评级字段是xmp:Rating,值域1到5,Lightroom的五颗星对应就是这里同一份数据。
<?xml version="1.0" encoding="UTF-8"?> <x:xmpmeta xmlns:x="adobe:ns:meta/"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description xmlns:xmp="http://ns.adobe.com/xap/1.0/"> <xmp:Rating>3</xmp:Rating> <cr photoshop="http://ns.adobe.com/photoshop/1.0/"> <photoshop:SidecarForExtension>CR2</photoshop:SidecarForExtension> </cr> </rdf:Description> </rdf:RDF> </x:xmpmeta>上面是我手动拆过的最小可用模板。段落里只保留和评级相关的节点就够了,SidecarForExtension标明了这个旁车文件挂在哪个格式的原图上。VisionCull导出XMP时如果检测到原文件旁边已有XMP,会先解析已有内容再合并,不会把之前的手动调整覆盖掉。因为写的不是原图而是旁车文件,RAW像素零改动,在Lightroom里导入这批照片后按星级排序,选片结果直接在自己熟悉的界面里呈现。
2.3 闭眼检测的工程取舍:人脸关键点还是二分类模型
闭眼检测在选片场景比想象中敏感。一次快门里两个人一个睁眼一个闭眼,闭眼的那个人是废片,但照片里另外一个人物构图很好,整体不想删,所以工具必须做到“按人脸出标记”而不是“按照片出结论”。
VisionCull的默认做法是:先用OpenCV DNN人脸检测器框出人脸的box,再送入一个轻量的闭眼二分类网络,输出睁眼/闭眼概率,闭眼置信度超过阈值就把这张照片的该区域标为闭眼废片。另一种常见方案是用dlib的68点人脸关键点算EAR,眼睛上下眼睑距离的纵横比,闭眼时EAR值趋近于0.1以下,睁眼一般在0.25以上。
from scipy.spatial import distance as dist def eye_aspect_ratio(eye_landmarks): vertical = dist.euclidean(eye_landmarks[1], eye_landmarks[5]) + \ dist.euclidean(eye_landmarks[2], eye_landmarks[4]) horizontal = dist.euclidean(eye_landmarks[0], eye_landmarks[3]) return vertical / (2.0 * horizontal)这段代码展示了EAR的计算方式:分子是眼睛上下睑两对点的欧氏距离之和,分母是内眼角到外眼角的宽度,EAR值对面部缩放有较好的不变性。但注意,EAR方案在戴墨镜、低头、侧脸时会明显失真,VisionCull默认不采用它作为主判据,二分类网络用数据驱动方式去学“闭眼”在像素层面的表现,对墨镜遮挡也更鲁棒。工程上我建议保留两种指标,模型负责判定、EAR做置信度的辅助校准,单张照片上有超过三张人脸且大多被模型判为闭眼时,更大可能是合影中的低头瞬间,而不是相机故障。
3. 安装部署:从源码包到跑通第一遍的全流程
3.1 环境准备:Python版本、依赖清单与CPU/GPU权衡
VisionCull的源码包解压后,核心是visioncull/目录、requirements.txt和run.py。依赖不算多,只做选片推理的话用CPU也能跑,但一次处理上千张RAW的预览图,CPU推理的时间成本会很明显。我自己用的环境是Python 3.10 64位、一个8GB显存的显卡,推理后端选了ONNX Runtime GPU版。把requirements装齐是第一步:
python -m venv visioncull_env source visioncull_env/bin/activate pip install -r requirements.txt pip install onnxruntime-gpu上面这条命令先建虚拟环境,再装需求清单。requirements.txt里锁定了opencv-python、numpy、Pillow、tqdm、onnxruntime、scikit-image等基础依赖,最后一条单独装GPU版ONNX Runtime。如果电脑没有NVIDIA显卡,就别装onnxruntime-gpu,用CPU版即可,效果不变只是慢。装完跑一句python run.py --help,能打出参数列表就说明环境通了。
3.2 用命令行跑第一遍筛选:参数逐个解释
工具走的是命令行入口,没有图形界面,这对批量处理反而是优点。第一次跑建议带--dry_run先只扫描不动文件,确认没有误伤习惯后再正式写入。
python run.py \ --input ./RAW_Import \ --output_dir ./selected_previews \ --rating_threshold 3 \ --blur_threshold 0.35 \ --eye_close_threshold 0.80 \ --face_min_size 32 \ --workers 4 \ --batch_size 16--blur_threshold是模糊判定的置信度阈值,0.35以上判断为跑焦,这个值越高越严格,对风光片可以调到0.5而对于弱光室内的婚礼跟拍0.35更合理。--eye_close_threshold是闭眼判定的置信度阈值,默认0.8比较保守,只有模型很确定“闭眼”才标记废片。--face_min_size是人脸检测的最小边长,小于这个像素的脸不参与闭眼判定,便于避开远景中占屏幕比例很小的人头。--batch_size控制推理时一次丢给模型的图像数量,默认16在8GB显存上够用,显存小就调成4或8。
跑完会在--output_dir下生成一个CSV清单,列出每张照片的合焦置信度、闭眼标记、人脸数量和计算得出的星级,XMP文件则按原文件路径直接写在照片旁边。第一次完整跑完后用Lightroom导入原目录,按星级筛选,能和CSV里的结果逐一对应上。
3.3 目录约定与批量照片的扫描逻辑
工具扫描目录时不是无差别递归的,默认只进一级子目录,避免把Lightroom的预览缓存目录也扫进去。目录层级浅一点,RAW文件平铺在一层里,速度和安全都更可控。源码里扫描逻辑有一个--max_depth参数可以调整,处理多日分目录的照片时,把这一天拍的内容单独放进一个文件夹,输出时自动按输入路径保留层级,之后在Lightroom按文件夹导入,选完片原目录结构不变,后续归档更省事。
VisionCull不直接读取CR2或RAF这类RAW的多层字节,它靠依赖库解码RAW的嵌入式预览JPEG来做推理,速度和兼容性都因此稳定。如果原图目录里混着JPEG原片,它也能直接以原图为输入。这带来的一个实际好处是:不用先导出DNG或全尺寸JPEG,选片处理和Lightroom目录库互不干扰。扫描过程中它会把目录里的非图像文件直接跳过,包括.xmp、.lrprev和缩略图数据库。
4. 参数调优与工作流接入:让工具真正适配你的选片习惯
4.1 星级分布策略:废片剔除、备选保留与精选五星
VisionCull写入XMP的星级不是模型直接吐出的分数映射,而是一套可自定义的规则表。默认逻辑是:跑焦且闭眼的照片给1星,只跑焦或只闭眼的给2星,清晰且人脸闭眼置信度在边界区间的给3星,全部正常但构图没有额外判定的给4星,模型对焦置信度极高且所有人脸都清晰睁眼的给5星。这个策略对婚礼跟拍这种“出片率本来就不高”的场景是合适的。
| 星级 | 判定组合 | 在Lightroom里的用途 |
|---|---|---|
| 1星 | 跑焦 + 闭眼 | 直接隐藏,基本不救 |
| 2星 | 跑焦 或 闭眼 | 先删,除非画面内容罕见 |
| 3星 | 边界模糊或侧脸 | 备选库,二次回看时再定 |
| 4星 | 主体清晰人脸正常 | 进入初选池 |
| 5星 | 高置信清晰全脸睁眼 | 直接进修图队列 |
如果拍的是风光或静物,闭眼检测的作用为零,这时候应该把闭眼判定的权重关掉,同时把--blur_threshold调高,让模糊判定变得更严格。我在源码里通常的做法是复制一份config_sets的配置:在推理前按拍摄类型加载不同配置,人像套用上面的表,风光把enable_eye_detection设为False,星级5的判定条件改为画面全局锐度,用拉普拉斯方差替代人脸数量。
4.2 和Lightroom的导入导出衔接:XMP写入时机与文件名兼容
Lightroom默认不会自动读取所有旁车XMP,需要留意一下“目录设置”里元数据那一栏的选项。通常我建议的处理顺序是:先用VisionCull跑完整个导出的文件夹、XMP生成完毕,再让Lightroom导入这批照片。Lightroom导入时如果检测到同文件名的XMP,会自动把星级和关键字合进目录。已经导入完了才跑VisionCull也可以,在Lightroom里全选照片,执行“从文件读取元数据”,效果等价,但多一步操作。
文件名匹配是这套流程里最需要重视的兼容点。Lightroom识别旁车文件靠的是主文件名完全一致,DSC01001.CR2对应DSC01001.xmp,缩写成DSC1001.xmp就读不出来。VisionCull内部对文件名的处理做了规范化,遇到大小写不一致的扩展名时会按原文件名精确拼接sidecar后缀,不会改成全大写。如果你自己动手改这部分代码,要注意RAW扩展名在Lightroom里识别为.CR2和.cr2是同一格式,但旁车文件名必须和磁盘上的实际文件名逐字节一致。
4.3 大批量文件夹的并行处理与显存占用
上千张照片的推理会持续很久,workers数和batch_size要配合调。工具默认的workers=4是数据读取线程数,不等于同时推理的图像数,真正的显存压力来自batch_size。我见过把batch_size设成64然后8GB显存直接溢出的情况,原因是模型本身是动态输入尺寸,解码后的预览图如果边长超过1920,显存占用会成倍上涨。
调参会有一个明显的“甜点区间”:8GB显存、batch_size=16、workers=4,单张图平均耗时大概在90毫秒左右。显存小于4GB就调成batch_size=8,CPU推理则保持batch_size=1,workers按物理核心数的一半来设,超过这个值反而是线程切换开销大于并行收益。处理过程中工具在控制台打印每批的进度百分比,ETA是按最近十批的平均耗时外推出来的,前面的几十批不准,等跑到总进度15%以后才大致可信。
我习惯把一次全量选片拆成两轮:第一轮带默认参数跑全量,生成XMP和CSV;第二轮只对1星和2星的照片做一次复核,带--review_mode只输出新的星级建议而不覆盖原有评级。这个模式在源码里是通过读取已有XMP里的xmp:Rating来跳过已定级照片的,第一次跑完、第二次再跑相同目录时,不会重复消耗推理时间,只对没有XMP的新增照片做计算。
5. 避坑与排查:我踩过的几个实际坑
5.1 现象:XMP写进了文件,Lightroom里星级却纹丝不动
第一次用这个工具,跑完满怀期待打开Lightroom,照片上什么星都没有。检查旁车文件,XMP安然躺在旁边,XML结构也没问题。原因出在Lightroom的设置:默认情况下它不自动读取旁车XMP,需要在目录设置里勾选“自动将更改写入XMP”并把“读取元数据”的行为改为“启动时读取”,或者手动全选照片后执行“从文件读取元数据”。解决方式是:批量场景下先把Lightroom完全退出,用VisionCull生成XMP,再启动Lightroom执行导入,导入时它会主动读取已经存在的旁车文件。从那以后我养成了习惯,选片机器上装完Lightroom第一件事就是先把元数据读写选项全部打开。
5.2 现象:闭眼检测在人逆光拍摄的照片上疯狂误报
逆光人像,人脸区域在光源强烈时会过曝或出现大面积高光,闭眼模型把高光下的“眼睛区域”误判成了闭眼状态。原因是模型训练样本里极少包含这种高光比场景,推理输入又做了归一化,高光区域丢失了纹理,和闭眼舒缓的纹理很接近。解决方式是调整--eye_close_threshold,默认0.8在逆光场景提到0.9,模型必须在极高分下才敢判闭眼,虽然会漏掉一部分真正的闭眼,但整体保住了出片率。更彻底的办法是给模型增加逆向光样本微调,需要额外收集约两百张这类环境的照片做模型训练。
5.3 现象:所有大光圈照片全被判成跑焦
有段时间我用85mm f/1.4拍了一组街拍,背景全部是圆形弥散斑,焦点集中在极浅的纵深上,模型的模糊置信度分数普遍偏高,因为图片整体纹理能量偏弱。拉普拉斯方差的传统判据更明显,低纹理背景直接把方差拉低到40以下。这个坑的教训是:全局锐度判据对浅景深题材天然不友好,不能只看均值。VisionCull里我后来给模糊检测加了一个“中心区域加权”选项,只取画面中心40%的区域算锐度,服从三分法构图时这里往往是对焦主体。对于人像,检修逻辑还会把人脸检测框区域单独算一个锐度分数,把那块区域作为合焦判定的主要依据。
5.4 现象:同一批照片跑了两次,之前手动定的4星被覆盖
第一次全量跑完,我在Lightroom里手动给几十张特别满意的照片追加上5星,之后为了新到的几十张照片重新跑了工具,结果之前手动定的5星全部回到原始判定值。原因是第二次运行时工具扫描到没有XMP的新照片,但也把已有XMP的老照片重新读了出来,星级判定逻辑是“始终重算并覆盖”,而不是“仅在无评级时写入”。解决的路径在源码里已经存在:开启--keep_existing_rating后,检测到已有xmp:Rating大于1的照片时跳过推理,直接保留原评级。从那以后我的固定动线是:每批照片先跑一次自动选片,再手动精修星级,之后无论如何重复跑,自动流程都不会动已经人工定过的结果。
5.5 现象:跑到一半显存OOM,进程直接退出
批量处理大型婚礼跟拍时,模型推理到了几百张照片之后程序中断,报错信息是CUDA out of memory。原因是每张图预览尺寸不一,有一个超大尺寸的全景接片预览边长超过6000像素,在batch里被等比放大后,单个batch的显存需求翻了数倍。解决方式是两招并用:把--batch_size从16降到8,并打开--limit_long_edge 2048的选项,让工具先对预览图做边长限制再做批次推理。这个参数会损失一点对焦置信度的计算精度,但换来的是批处理稳定不崩。
6. 进阶:给VisionCull加一个“过曝检测器”,扩展废片规则
单纯依赖跑焦和人脸闭眼,还漏掉了一类在现场很常见的废片:严重过曝的人像高光,脸部区域已经完全没有层次,肤色变成一块纯白。这类照片在Lightroom里后期几乎无法拉回,除非本来就想要高调效果,否则应该从一开始就标为低星。
VisionCull的判分器结构允许按顺序叠自定义检测器。我在visioncull/detectors/下新建了一个exposure_detector.py,核心逻辑是读取预览图转HSV通道,把亮度V通道的像素直方图拿出来,统计纯白像素占比。纯白区域占了画面10%以上,并且这部分白的位置和人脸检测框有重叠,就判定为过曝废片。代码如下:
import cv2 import numpy as np def overexposure_score(img_bgr, face_boxes, overlap_threshold=0.2): img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) v_channel = img_hsv[:, :, 2] white_mask = v_channel > 250 for box in face_boxes: x1, y1, x2, y2 = box face_region = white_mask[y1:y2, x1:x2] if face_region.size == 0: continue overlap = face_region.mean() if overlap > overlap_threshold: return 1.0, True return white_mask.mean(), False逻辑是先对整个画面算白色像素占比,再逐个人脸框计算人脸区域内的白色像素比例。虽然有一个人脸框内超过20%像素是纯白,就返回过曝标志True。这段代码在VisionCull里的接入办法是,找到main_runner里检测器列表注册的位置,把exposure_detector追加进去,它的返回分数会参与最终星级计算:过曝标记会强制把星级上限压到2星。
这个新增的检测器还有一批更细的用法:可以设置white_mask的阈值在不同的光比环境下调整,影棚里背景纯白时,背景中的白色不应该被算入过曝,所以基于人脸框的重叠计算是必要的。另外把overlap_threshold调到0.5,就只对高光彻底溢出的脸部做标记,适合保留鎏金轮廓光等等有创作意图的画面。
用这套思路,还可以继续扩展“欠曝检测”和“偏色检测”,前者看暗部像素比例、后者比较灰卡区域的颜色通道均值。VisionCull的源码里检测器接口定义清楚了输入输出的约定,新增一个检测器只需要实现同样的返回签名,然后注册到列表里,整个管线会自动把新旧检测器的分数做加权融合。从那以后我每次接一个新场景拍摄,都会在正式开拍前先拿现场的预览图跑一遍选片工具,确认检测器组合在当前光线条件下没有系统性误判,再带着这套配置去处理整个项目。希望帮到你。
本文还有配套的精品资源,点击获取