1. 项目概述与整体思路
第一次把两个月前的同一块空地航拍图叠在电脑屏幕上时,我整个人是懵的:密密麻麻的板房、吊车、钢管和刚浇筑的地基,全挤在几张普通相机根本拍不全的大场景里。人工用肉眼比对,少说也得盯半小时,还容易漏。这个练手项目的核心思路很直接——用YOLOv5配合图片差异对比,把无人机巡检中反复出现的“哪里有变化”和“变化是什么”两个问题一起解决,最后把疑似违建的建筑直接框出来,省掉人工翻图的功夫。
我把它定位成一个偏向工程落地的实践项目,适合正在做无人机巡检、工地进度管理、甚至环保督察辅助的朋友参考。整套流程拆开看并不复杂:图像预处理、特征点配准、像素差异求取、YOLOv5目标识别、交叉筛选和结果标记,每一步都有成熟工具可用。你不需要从零训练一个全新的CV模型,重点是怎么把已有能力组合成一个能直接跑起来的流水线,并且保证误报率在可控范围内。
1.1 无人机巡检到底在找什么
无人机巡检这些年已经不是什么新鲜事,但真正做一线业务的人都知道,困扰大家的往往不是“飞不飞得起来”,而是“拍回来的几百张图怎么处理”。以违章建筑巡查为例,同一片区域可能每隔几天就飞一次,目标可能是新增的彩钢瓦屋顶、临时搭建的工棚、楼顶加盖,或者是原本空地上冒出来的活动板房。这些东西的共同点是:它们要么体量不大,要么颜色和周围环境接近,光靠人眼在几十张高分辨率图里找,效率低且容易疲劳。
我做的这套方案,本质上就是给无人机图做一个“变化提醒”:先把两期影像在空间上对齐,再用像素差把变化区域粗筛出来;接着让YOLOv5在当前影像里识别具体目标,最后把变化区域和检测结果做一次交叉判断。只有两个条件都满足的区域,才会被标记为“疑似新增违建”。这样既能回答“哪里变了”,也能回答“变出来的东西是什么”,比单纯做一次目标检测靠谱很多。
1.2 技术路线怎么组合
很多刚接触图像变化检测的朋友会问:是不是直接用OpenCV的absdiff做差就行?或者干脆只跑一轮YOLOv5检测,看看图上有没有建筑?这两种做法单独用都会踩坑。
先看纯像素差异。无人机两次拍摄的航线不可能完全一致,光照、阴影、季节植被变化都会让absdiff产生一堆噪声。直接把前后两帧做差,往往整张图都是“变化区域”,根本没法用。所以我加了两个关键环节:第一步是特征点配准,把两张图尽量拉到同一空间坐标系下;第二步是形态学过滤和面积阈值过滤,把噪声造成的零星像素点去掉,只保留连成片的可疑区域。
再看纯目标检测。YOLOv5可以识别出“这里有建筑”,但它不知道这栋建筑以前有没有。如果拿一个区域的历史影像和当前影像同时做检测,都需要先训练一个能识别建筑类别的模型,而且依然无法判断“是不是新增”。所以正确做法是把两者串起来:差异对比负责缩小范围,目标检测负责确认目标类型,最后再用交集筛选来减少误报。这个“由粗到细”的处理顺序,是整套项目能落地的最主要原因。
1.3 为什么不单独用检测或单独用差异
单独用差异最让人头疼的是无法对“变化”定性。一片荒地上多了一块蓝色铁皮棚,像素差肯定很明显,但这块铁皮到底是违建还是临时覆盖物?是新增工地还是地质灾害?纯图像差分回答不了。单独用YOLOv5检测也有类似问题——模型识别能力再强,也只输出“是什么”,不输出“是否为新增”。
把两者结合后,行为逻辑就清晰了:假设某一期的影像作为基准,新一期的影像作为待检测对象,正常情况下基线区域的建筑已经存在,那它在YOLOv5中的检测结果应当被标记为“存量建筑”,不做报警。但凡是出现在差异掩码中的检测框,说明这个目标在两期之间有明显变化,再结合人工复核或业务规则,就有充分理由怀疑是新建违建。这个交叉筛选的思路相当于给检测结果加了一层“按时间维度过滤”的闸门。
2. 环境准备与YOLOv5模型部署
2.1 环境依赖与安装
YOLOv5对硬件要求不算苛刻,我用一台装了RTX 3060的台式机就能跑得比较顺,CPU模式也能跑,只是速度会慢不少。环境搭建建议用Anaconda,特别是需要同时尝试多个PyTorch版本时,虚拟环境能省去很多麻烦。
conda create -n yolo python=3.8 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这里有个容易踩坑的地方:PyTorch版本要和你的CUDA驱动匹配。别上来就最新版,先跑一句nvidia-smi查看驱动支持的CUDA版本,再选择对应车轮包。如果只是CPU推理,直接pip install torch torchvision默认版本就行。之后克隆YOLOv5仓库并安装依赖:
git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txtWindows用户如果发现pycocotools装不上,多半是缺少Visual C++构建工具,也可以直接安装预编译包:
pip install pycocotools-windows另外建议把opencv-python升级到4.8以上,因为后面做图像配准和形态学处理时,新版接口更稳定。我在旧版4.1上遇到过cv2.findContours返回值数量不一致的问题,排查了半天才发现是版本差异。
2.2 模型权重与推理验证
YOLOv5官方预训练权重分s/m/l/x几档。我这个项目用的是yolov5s,模型小、推理快,对建筑这类大目标也够用;如果实际场景里需要识别小目标,比如楼顶小屋、碎小工棚,建议上yolov5m甚至yolov5l,但对应推理耗时也会增加。首次运行会在线下载权重,可以先把权重文件放到yolov5/weights目录下,离线环境也能用。
# 在yolov5目录下执行 python detect.py --weights yolov5s.pt --source data/images --device 0如果能看到图片上成功画出检测框,说明环境基本没问题。跑通之后,我习惯先拿真实航拍图试一遍,看一下COCO预训练模型对普通建筑、卡车、人的识别效果。需要说明的是,COCO里并没有“违章建筑”这个类别,所以我在业务场景里一般会把目标类别迁移成更具体的建筑构件,比如“彩钢棚”“活动板房”“吊车”“裸土堆”,这样做出来的检测结果才更有业务价值。
2.3 训练自定义数据集:从COCO到“违建类别”
如果只检测一般建筑,用预训练模型勉强能用;但要真的用于违建标记,最好还是训练自己的YOLOv5模型。自己训练有几个好处:一是可以把类别设置得贴合业务,比如就定义illegal_building、shed、crane等;二是可以针对无人机俯拍视角做特化,因为COCO模型大多是从水平视角训练出来的,对俯拍建筑错检漏检比大家想象中严重。
数据准备上,传统标注流程是:用LabelImg或AnyLabeling对俯拍图片框出目标,保存为YOLO格式的txt文件,每行是class_id x_center y_center width height。目录结构建议这样组织:
datasets/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/然后写一个data.yaml:
train: datasets/images/train val: datasets/images/val nc: 3 names: ['shed', 'crane', 'illegal_building']训练命令:
python train.py --img 640 --batch 8 --epochs 100 --data data.yaml --weights yolov5s.pt --device 0训练完成后,runs/train/exp/weights/best.pt就是可以用在推理流程里的模型。别忘了同时记录验证集上的mAP,如果mAP低于0.5,可能需要调整标注质量、增加同场景数据或改变输入分辨率。
3. 无人机图片差异对比模块的实现
3.1 图像配准:两次航拍怎么对齐
做差异对比最基础、也最关键的一步是图像配准。无人机两次飞行的姿态、高度、角度很难完全一致,如果直接把两张图按像素做差,误差会大得惊人。最开始我把这个问题想简单了,用普通相机的连续帧做差效果还行,换成无人机的正射图就翻车,原因是两期影像之间不仅有平移,还有旋转和尺度变化。
常用方案是提取特征点再进行仿射或透视变换。SIFT特征在航拍图上有较强的抗旋转和尺度变化能力,但计算量稍大;ORB算得快但对弱纹理区域效果差。如果图片本身带有地理位置信息,比如DJI相机的XMP坐标或者正射影像的GeoTransform参数,也可以直接用仿射变换对齐。我的做法是先尝试SIFT,匹配效果不够再用地理坐标辅助。
核心代码如下:
import cv2 import numpy as np def align_images(img_ref, img_new, max_features=5000): gray_ref = cv2.cvtColor(img_ref, cv2.COLOR_BGR2GRAY) gray_new = cv2.cvtColor(img_new, cv2.COLOR_BGR2GRAY) sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(gray_ref, None) kp2, des2 = sift.detectAndCompute(gray_new, None) matcher = cv2.BFMatcher() raw_matches = matcher.knnMatch(des1, des2, k=2) good = [] for m, n in raw_matches: if m.distance < 0.75 * n.distance: good.append(m) src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) h, w = img_ref.shape[:2] aligned_new = cv2.warpPerspective(img_new, H, (w, h)) return aligned_new, H在特征匹配时,knnMatch配合Lowe比例检验是最常用的粗筛方式,0.75是经验值,越小于匹配越严。配准质量如何判断?把两张图叠成半透明看,也可以直接计算对齐后图像的重叠区域像素差异均值,数值越低说明对齐越好。
3.2 差异提取:把像素差变成“可疑区域”
配准完成后,就可以用像素差粗筛变化区域。我会把原图转为灰度,计算两张图的灰度差,再用一个阈值把差图二值化。这个阈值是整套流程的重要超参数,调得太低会把光照和相机噪声全部圈进来,调得太高又会漏掉真正新增的小建筑。
我一般从threshold=40开始调,针对2亿像素级别的大图可以先降到30试试,但最终还要以实际目标尺寸为准。二值化之后一定要做形态学处理:先用开运算把零星噪点去掉,再用闭运算把断开的建筑边缘连起来。注意形态学结构核大小也是跟分辨率挂钩的,对每像素约5厘米的无人机图,我用5x5的核,如果单片影像面积更大,可以改成7x7甚至9x9。
def create_diff_mask(img_ref, img_aligned, threshold=40, min_area=3000): gray_ref = cv2.cvtColor(img_ref, cv2.COLOR_BGR2GRAY) gray_aligned = cv2.cvtColor(img_aligned, cv2.COLOR_BGR2GRAY) diff = cv2.absdiff(gray_ref, gray_aligned) _, mask = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) region_mask = np.zeros_like(mask) for cnt in contours: if cv2.contourArea(cnt) >= min_area: cv2.drawContours(region_mask, [cnt], -1, 255, -1) return region_mask轮廓筛选时,min_area这个值需要仔细权衡。如果遥感影像地面分辨率只有20厘米,一栋几十平方米的活动板房面积也有上千像素;如果是普通DJI无人机低空拍的图,分辨率更高,面积阈值就可以同步放大。我的习惯是先画几组不同阈值的掩码图对比效果,再确定一个能包含目标又不引入大量碎片的数值。
3.3 减少光照与阴影干扰的实用技巧
做航拍差异对比绕不开光照问题。早上和中午飞同一片区域,影子方向和长度完全不同,导致整片绿地、建筑物侧面全是假变化。这里分享几个亲测有效的处理习惯。
第一种是颜色空间过滤。很多假变化来自绿色植被的长势变化,可以把RGB转到HSV空间,把绿色像素对应的饱和度通道提取出来,生成植被掩码,再去差异图里把这些区域抹掉。第二种是梯度图替代灰度图。用cv2.Sobel或者cv2.Laplacian提取梯度,再做absdiff,这样对光照变化的敏感度会降低不少。第三种是分块处理。大图两次拍摄如果局部有云影或相机自动曝光差异,整张图共用同一个阈值会不均衡,按网格分块后分别计算差异阈值,能明显改善效果。
除了这些,阴影方向造成的建筑物屋顶亮暗变化,在纯像素层面几乎无法完全消除。所以我在流程里始终把差异掩码当作“候选区域”,而不是最终结果,后面必须用YOLOv5检测框再判断一次。经过这一步,误报率已经能降一个数量级。
4. 违建检测与标记模块的实现
4.1 YOLOv5检测流程与参数设置
差异掩码圈出来的区域可能包含很多噪声,比如道路颜色变化、阴影移动、车辆移位。要判断这些变化里到底有没有真正的建筑目标,还得靠YOLOv5。推理阶段我不推荐临时去改官网脚本,直接写一个可复用的加载函数更顺手。
import torch def load_model(weight_path="yolov5s.pt", device="0"): model = torch.hub.load("ultralytics/yolov5", "custom", path=weight_path, force_reload=False) model.conf = 0.25 model.iou = 0.45 model.max_det = 200 if device != "cpu": model.model.to(torch.device("cuda")) return model关于推理参数,我把置信度设置在0.25,IoU阈值0.45。如果后续发现漏检多,就把conf下调到0.15,同时接受更多假阳性;如果发现同类目标大量误报,则上调conf。更稳妥的做法是同时输出漏检和误检案例,根据业务方反馈不断调整。
4.2 检测框与差异掩码的交叉筛选
这一步是整个实战的精华部分。检测框只能说明“这里有一个目标”,差异掩码能说明“这里的图像内容产生了变化”,把两者相交,才可能筛选出“变化后发现的目标”。我选择用一个比值来判断:计算每个检测框内差异掩码像素面积占框总面积的比率。如果比率超过某个经验阈值,就认为这个检测框对应的目标是新增或发生明显变化。
def mask_overlay_ratio(box, mask): x1, y1, x2, y2 = [int(v) for v in box] w = max(x2 - x1, 1) h = max(y2 - y1, 1) box_area = w * h crop = mask[y1:y2, x1:x2] overlay = cv2.countNonZero(crop) return overlay / box_area阈值方面,我在多数无人机正射影像场景中取0.15到0.25之间。取0.15会使系统更敏感,凡是框内有超过15%区域发生变化就会报警;取0.25则更严格,但可能漏掉那些新建物被周边遮挡的情况。如果你运行后发现“存量建筑”被频繁误报,建议提高这个比值;反之如果很多真正的违建没被标记,就降低它。
交叉筛选后会产生三类结果:第一类是有检测框且差异比率达标的,标记为“疑似新增违建”;第二类是有检测框但差异比率很低的,标记为“存量建筑”;第三类是没有检测框但差异掩码面积很大的,标记为“疑似未识别变化”,交由人工复核。这个分类信息对后续业务处理极有价值,能直接帮助人工排查。
4.3 绘制标记与输出结构化报告
筛选出最终结果后,需要把信息呈现给用户。我习惯把原图和检测结果做得尽量直观:疑似新增违建框用红色绘制,并写上类别、置信度和差异比率;存量建筑框用蓝色绘制,便于对照。标题注明这是“图片来源、拍摄时间、处理时间”等信息,这样即使输出几十张图也不会乱。
绘制中文标签时需要注意,OpenCV内置的cv2.putText不支持中文,直接写会出现乱码。我一般用PIL的ImageDraw配合中文字体文件来渲染,先转RGB,画完再转回BGR。如果你觉得麻烦,也可以直接在检测框上写英文标签,报告文件里再对应中文。
结构化报告可以输出成CSV,包含图片名、目标类别、置信度、坐标、差异比率和是否疑似违建。如果后续要接GIS系统或生成Excel,这个CSV非常方便。我还会额外写一张文本汇总:总检测数量、疑似新增数量、存量数量、待复核区域数量,这样业务方不用打开图片就能知道整体情况。
5. 完整代码整合与运行说明
5.1 主流程代码
这段代码我把前面的模块串起来,直接保存为drone_inspect.py即可运行。假设你已经有训练好的权重,或者直接用COCO预训练的yolov5s.pt,都能跑通。
import cv2 import numpy as np import torch import csv from pathlib import Path # ----------------- 模型加载 ----------------- def load_model(weight_path="yolov5s.pt", device="0"): model = torch.hub.load("ultralytics/yolov5", "custom", path=weight_path, force_reload=False) model.conf = 0.25 model.iou = 0.45 model.max_det = 200 return model # ----------------- 图像配准 ----------------- def align_images(img_ref, img_new): gray_ref = cv2.cvtColor(img_ref, cv2.COLOR_BGR2GRAY) gray_new = cv2.cvtColor(img_new, cv2.COLOR_BGR2GRAY) sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(gray_ref, None) kp2, des2 = sift.detectAndCompute(gray_new, None) matcher = cv2.BFMatcher() raw_matches = matcher.knnMatch(des1, des2, k=2) good = [] for m, n in raw_matches: if m.distance < 0.75 * n.distance: good.append(m) src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2) H, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) h, w = img_ref.shape[:2] aligned_new = cv2.warpPerspective(img_new, H, (w, h)) return aligned_new, H # ----------------- 差异掩码 ----------------- def create_diff_mask(img_ref, img_aligned, threshold=40, min_area=3000): gray_ref = cv2.cvtColor(img_ref, cv2.COLOR_BGR2GRAY) gray_aligned = cv2.cvtColor(img_aligned, cv2.COLOR_BGR2GRAY) diff = cv2.absdiff(gray_ref, gray_aligned) _, mask = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=3) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) region_mask = np.zeros_like(mask) for cnt in contours: if cv2.contourArea(cnt) >= min_area: cv2.drawContours(region_mask, [cnt], -1, 255, -1) return region_mask # ----------------- 检测框与掩码交叉 ----------------- def mask_overlay_ratio(box, mask): x1, y1, x2, y2 = [int(v) for v in box] w = max(x2 - x1, 1) h = max(y2 - y1, 1) crop = mask[y1:y2, x1:x2] overlay = cv2.countNonZero(crop) return overlay / (w * h) # ----------------- 主流程 ----------------- def run_inspection(img_old_path, img_new_path, weight_path="yolov5s.pt", threshold=40, min_area=3000, ratio_thresh=0.15, device="0"): img_ref = cv2.imread(img_old_path) img_new = cv2.imread(img_new_path) if img_ref is None or img_new is None: raise ValueError("图片读取失败,请检查路径") aligned_new, H = align_images(img_ref, img_new) region_mask = create_diff_mask(img_ref, aligned_new, threshold, min_area) model = load_model(weight_path, device) dets = model(aligned_new) # 对配准后的新影像检测,坐标与差异掩码对齐 result_img = aligned_new.copy() rows = [] for det in dets.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf, cls_id = det cls_name = model.names[int(cls_id)] ratio = mask_overlay_ratio([x1, y1, x2, y2], region_mask) if ratio >= ratio_thresh: color = (0, 0, 255) status = "suspect" else: color = (255, 0, 0) status = "existing" cv2.rectangle(result_img, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) label = f"{cls_name} {conf:.2f} ratio={ratio:.2f}" cv2.putText(result_img, label, (int(x1), max(int(y1) - 8, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) rows.append([Path(img_new_path).name, cls_name, round(float(conf), 4), int(x1), int(y1), int(x2), int(y2), round(ratio, 4), status]) with open("inspection_report.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["image", "class", "conf", "x1", "y1", "x2", "y2", "diff_ratio", "status"]) writer.writerows(rows) cv2.imwrite("diff_mask.png", region_mask) cv2.imwrite("result.png", result_img) print(f"完成,共 {len(rows)} 个检测目标,详见 inspection_report.csv") if __name__ == "__main__": run_inspection("old.png", "new.png")如果你已经有本地YOLOv5仓库,可以把torch.hub.load("ultralytics/yolov5", "custom", ...)改成引用本地路径,避免在离线服务器上卡在网络请求。改法是:
model = torch.hub.load("你的本地yolov5目录", "custom", path=weight_path, source="local")5.2 运行参数调整
实际运行时,最影响结果的三组参数是差异阈值、轮廓最小面积和检测置信度。我把它们的关系整理成一张表,方便你快速定位问题:
| 现象 | 优先调整参数 | 调整方向 |
|---|---|---|
| 差异图噪声多,全是碎点 | threshold | 增大到50或60 |
| 真正的新建筑没进掩码 | min_area | 适当调小 |
| 检测框太多误报 | model.conf | 上调到0.35或0.4 |
| 小目标建筑漏检 | model.img_size 或推理分辨率 | 从640上调到960或1280 |
| 存量建筑被误标为疑似 | ratio_thresh | 上调到0.25以上 |
这些参数之间存在联动,改其中一个经常会牵连其他参数表现。我建议在跑全量数据前先选3到5张有代表性的图做网格搜索,把几组参数各跑一遍,对比标记效果后再定最终组合。
5.3 边缘设备部署扩展
很多巡检项目需要在现场快速出结果,不一定背着台式机。YOLOv5部署到树莓派5或Jetson系列上,是最近被问得比较多的问题。如果算力有限,建议先做几步:第一,把模型导出成TorchScript或ONNX,去掉PyTorch动态图的额外开销;第二,把输入分辨率从640降到480,配合yolov5n或yolov5s;第三,只对差异掩码框出的固定区域做裁剪检测,而不是整图推理。
我记得在Jetson Nano上用TensorRT加速yolov5s,推理时间能从几百毫秒压到几十毫秒,配合边缘端图片缓存,现场跑完一轮巡检完全可行。树莓派5的CPU性能比前代强不少,但跑yolov5s仍显得吃力,更适合用yolov5n加小尺寸输入。如果要用树莓派实时处理视频流,建议只做差异检测用于粗筛,再定期跑模型,没必要每一帧都调用推理。
6. 常见问题与排查技巧
6.1 环境安装类问题
先说环境,因为这块卡住人的频率最高。常见问题包括:pycocotools编译失败、torch和torchvision版本不匹配、opencv-python版本过低导致部分接口不存在。围绕YOLOv5本身,网上讨论最多的是官网下载、环境配置和超参数调整。我的经验是不要盲目追求最新版本,确定一个稳定组合后就固定下来,别频繁升级。下面梳理一份速查:
| 常见错误 | 原因 | 解决方案 |
|---|---|---|
No module named 'pycocotools' | Windows缺编译环境 | pip install pycocotools-windows |
ImportError: libGL.so.1 | Linux缺OpenCV系统库 | sudo apt update && sudo apt install libgl1 |
torch.cuda.is_available()返回False | PyTorch和驱动不匹配 | 重装对应CUDA版本的PyTorch |
SIFT不可用 | OpenCV编译不含contrib | 升级opencv-python到4.8以上,或改用ORB/SURF |
6.2 配准与差异误报类问题
配准失败是最常见的问题。特征点匹配不足会导致单应性矩阵不稳定,最终两张图错位严重。遇到这种情况,先加大max_features,把SIFT特征点从5000提高到10000;其次把比例检验的阈值从0.75调到0.7,让匹配更严格;如果还是没有足够匹配,考虑先用GIS信息做一个粗略的平移对齐,再跑特征点匹配。
差异图频繁出现大面积块状误报,要警惕季节变化和阴影。我处理过一批松树林区域的航拍图,两次拍摄相隔三个月,树冠的绿色度差异很大,差异掩码几乎把整个林区都圈了进去。后来我在HSV空间做了一遍绿色像元掩码,把植被覆盖区域从差异掩码中占比超过60%的斑块直接剔除,误报才降下来。
6.3 检测与标记类问题
YOLOv5检测结果不理想,要分两种情况看。一种是漏检:目标明明在图上,模型没框出来。常见原因包括目标太小、对比度低、训练数据没有类似视角。解决办法是提高推理分辨率,或者把大图切块后按patch检测,同时把conf阈值降低试一下。另一种是误检:模型把阴影、屋顶设备、地面标志当成建筑目标。这种情况优先看训练数据是否覆盖了这些负样本,而不是盲目调阈值。给模型提供足够多的负样本,比反复调conf有效得多。
标记阶段最让人头疼的是中文标签乱码。如果你不想引入PIL字体模块,可以偷个懒,绘制时用英文标签,比如把“疑似新增违建”写成SUSPECT_NEW,报告CSV里再映射成中文。这个方案虽然显示上不够直观,但胜在稳定。
6.4 性能与部署类问题
如果图片太大,比如无人机正射影像,单张就达到50MB甚至上百MB,这时候先缩放到一个合理的处理尺寸再跑流程。整张resize到3000像素宽度并不会丢失太多建筑级别的特征,但速度能快几倍。GPU显存不足时,可以把输入宽度设到1280以下,或者采用滑窗切块推理。切块时要保留一定的重叠区域,避免建筑正好被切成两半。建议重叠率设为20%到30%,并在合并检测结果时用NMS去重。
边缘部署还有一个容易被忽略的问题:模型在PC上精度很好,转换到ONNX或TensorRT后输出结果出现波动。这通常是归一化方式和缩放细节没对齐。YOLOv5官方仓库的export.py已经处理了大部分转换逻辑,尽量别自己手写预处理,直接用官方导出脚本能省不少事。
7. 实践总结与可扩展方向
7.1 项目的适用边界
这套组合流程最适合的场景有两个特点:一是影像覆盖范围稳定,能拿到同一区域不同时间的高重叠度图片;二是要判断的“违建”具备明显物理形态,比如屋顶加盖、彩钢棚、临时板房。如果目标是很隐蔽的地下施工或者仅仅通过颜色变化才能识别的早期违建,只靠光学影像和YOLOv5会非常吃力,需要结合多光谱、激光雷达或人工核查。
在技术路线上也不要被YOLOv5框死。随着检测模型迭代,换成YOLOv8、RT-DETR完全没问题,核心的差异对比加目标筛选思想是不变的。哪怕只做像素差异不跑检测,把变化区域按面积和位置聚类,也能给人工复核提供很大帮助。关键是先把流程跑通,再看瓶颈在哪里,而不是上来就堆最新模型。
7.2 后续还能怎么升级
我建议三个方向。第一,把“两期对比”升级成“多期时间序列”,一旦发现某块区域连续多期都在变化,就自动提高风险等级。这样比单期对比更容易发现持续施工的违建。第二,引入地块边界和规划数据,检测框与违建红线做空间叠加,只有落入敏感区域的检测结果才自动上报,能极大降低人工复核量。第三,给疑似违建加上面积估算,利用无人机飞行高度、焦距或正射影像的地面分辨率,把像素面积换算成平米,输出包含面积信息的报告。
最后说一个我个人的经验:技术流程做得再顺,也不要让系统完全替代人工审核。违建认定涉及很强的业务规则和合规要求,系统更适合做“优先排序”和“自动标记”,把决策权和最终确认权留给熟悉现场情况的人。这样既发挥了YOLOv5和图像差异对比的效率优势,又守住了关键环节的质量底线。