工业视觉踩坑实录(七):12个摄像头拼接做选矿厂全景监控,最后没做出来

摘要:选矿厂要做一个露天全景监控系统,12台低空全景拼接摄像机,拍完拼成一张大图。听起来不难——相机自带硬件拼接,分辨率8160×3616@24fps,参数很硬。但现场一装,问题全来了:没做标定直接硬拼、安装间距太大重叠不够、分辨率太高算不过来、传不过来、露天光照一天变好几次。最后这个项目没做出来。这篇文章复盘一下为什么。
在这里插入图片描述

关于作者

我接触视觉整整10年

机器视觉、烟草、煤矿等行业都有深度开发经验。从硬件选型、算法开发、模型训练,到上位机开发及部署,都在一线磨过。

之前是多家公司人工智能团队的技术负责人。现在自己创业了,还在继续做视觉落地这件事。


作者说

前面六篇,踩的坑都踩过去了,项目最终交付了。

这篇不一样。

这个项目没做出来

选矿厂全景监控,12台低空全景拼接摄像机,要把整个选矿区域拍成一张全景图。相机参数很硬,方案看起来也没问题。但到了现场,一堆问题一起涌上来,最后没能交付。

失败的项目比成功的更有价值。因为成功的项目你会选择性遗忘踩过的坑,失败的项目你会记住每一个细节。

这是第七篇。


01 场景:选矿厂露天全景监控

1.1 需求

一个选矿厂,要做露天区域的全景监控。目的是在大屏上看整个选矿区域的实时画面——哪里有设备故障、哪里有人违规进入、哪里堆料异常。

甲方选了低空全景拼接摄像机,单台内置4镜头硬件拼接,直接输出全景画面。

项目 规格
传感器 1/1.8" Progressive Scan CMOS
全景分辨率 8160×3616(3200万像素)@24fps
视场角 水平180°,垂直90°
压缩格式 H.265/H.264/MJPEG
网络 千兆网口 + SFP光模块(单模单纤20km)
防护等级 IP67 + IK10
供电 DC12V 或 PoE+(最大24W)
工作温度 -40℃~60℃

单台参数很硬。12台级联覆盖整个选矿区域,理论上行得通。

1.2 计划

12台摄像机沿选矿区域周边安装,后端接收12路全景视频流,二次拼接成一张完整全景图,显示在监控大屏上。

计划是这么想的。


02 第一个坑:现场安装不到位,重叠不够

2.1 拼接的基本前提

无论用什么拼接算法,有一个铁律:相邻摄像头之间至少要有30%的视野重叠。

重叠区域是拼缝的位置,也是特征点匹配的素材。没有重叠,就没有办法对齐。

2.2 现场的实际情况

现场安装是由甲方的工程队做的,不是我们的人。

我们给了安装指导书,写清楚了:每两个相邻摄像头的水平间距不能超过X米,垂直高度差不能超过Y米,确保至少30%重叠。

但到了现场一看——间距比我们要求的大了将近一倍

原因很现实:

  • 选矿厂的立柱不是我们设计的,间距固定
  • 有些位置没有合适的安装点,只能就近安装
  • 工程队理解"30%重叠"有偏差,觉得"能看到一点就够了"

结果是:很多相邻摄像头之间几乎没有重叠,有的甚至有盲区——两台摄像头之间的区域,谁也没拍到。

没有重叠,拼接无从谈起。


03 第二个坑:没做标定,直接硬拼

3.1 为什么没做标定?

时间紧,甲方催得急。而且用的是自带硬件拼接的全景摄像机,我们想当然地认为:单台内部已经拼接好了,多台之间应该也能直接拼。

“直接用stitch拼一下就行了。”

这是最大的误判。

3.2 单应性矩阵为什么不能省

多台摄像机之间,每台有自己的位姿(位置和朝向)。要拼在一起,必须知道它们之间的几何关系——这就是单应性矩阵(Homography Matrix)。

求单应性矩阵的方法:

import cv2
import numpy as np

# 在重叠区域找特征点
sift = cv2.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)

# 匹配
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
good = [m for m, n in matches if m.distance < 0.75 * n.distance]

# 算单应性矩阵
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)

# 用H做透视变换
result = cv2.warpPerspective(img1, H, (img2.shape[1] + img1.shape[1], img2.shape[0]))

这段代码的前提是:两张图之间有足够的重叠区域和特征点

我们现场的实际情况:

  • 重叠不够 → 特征点匹配数量少
  • 露天环境 → 光照变化大,特征点不稳定
  • 选矿厂场景 → 大面积岩石和矿堆,纹理重复度高,容易匹配错

直接硬拼的结果:画面错位、物体断裂、拼缝扭曲。完全没法看。


04 第三个坑:分辨率太高,算不过来

4.1 数据量有多大?

单台摄像机输出8160×3616@24fps,3200万像素。

12台同时传输,总数据量:

项目 数值
单台分辨率 8160×3616 ≈ 2950万像素
单台帧率 24fps
单台原始数据量 2950万×24 ≈ 7亿像素/秒
12台原始数据量 85亿像素/秒
H.265压缩后(估算) ≈ 500-800Mbps

4.2 传输带宽

12台千兆网口传输,理论上限12Gbps。但实际上:

  • 网线不是理想环境,实际有效带宽约70-80%
  • 交换机背板有瓶颈
  • 光纤链路虽然支持20km传输,但带宽是1.25G

H.265压缩后的500-800Mbps,勉强能传,但裕量很小。如果网络稍有波动,就会丢帧。

4.3 算力瓶颈

后端要做的事情:

  1. 接收12路视频流(解码)
  2. 做图像配准(特征点匹配/光流跟踪)
  3. 做图像融合(金字塔融合/羽化)
  4. 拼接后显示在大屏上

光解码12路8160×3616@24fps的H.265视频,就需要一张很强的GPU。

然后用OpenCV做特征点匹配和融合——单对8160×3616的图像,SIFT特征点检测就要几百毫秒。12对同时做,算力直接爆炸。

我们试过把分辨率降下来

分辨率 单帧处理时间 能否实时
8160×3616(原始) ~800ms
4096×1808(降50%) ~200ms 勉强
2048×904(降25%) ~50ms

降到25%分辨率才能实时,但25%分辨率的全景图在大屏上就是一团马赛克。

甲方要的是高清全景,不是低清全景。


05 第四个坑:露天光照一天变好几次

5.1 选矿厂的光照地狱

选矿厂是全露天的。这意味着:

早晨:太阳从东方升起,东边过曝,西边正常

中午:太阳直射,整个画面高亮度,矿堆表面反光严重

下午:太阳西移,西边过曝,东边开始正常

傍晚:整体偏暗,自动切换红外模式,画面变成黑白

阴天:整体偏暗偏灰,对比度低

一天之内,同一台摄像头拍出来的画面,颜色、亮度、对比度变化非常大。

拼接最怕的就是这个——上午调好的参数,下午就废了。

5.2 直方图匹配有用,但不够

我们在后端做了直方图匹配,试图让相邻摄像头的亮度一致:

def histogram_match(img, ref):
    """直方图匹配"""
    hist_img, bins = np.histogram(img.flatten(), 256, [0, 256])
    hist_ref, _ = np.histogram(ref.flatten(), 256, [0, 256])
    cdf_img = np.cumsum(hist_img) / hist_img.sum()
    cdf_ref = np.cumsum(hist_ref) / hist_ref.sum()
    lut = np.interp(cdf_img, cdf_ref, bins[:-1]).astype(np.uint8)
    return lut[img]

有一定效果,但解决不了根本问题:

  • 直方图匹配只能管亮度分布,管不了色温变化
  • 露天的色温变化范围太大(从3200K日落到6500K正午),算法校准跟不上
  • 矿堆表面是灰色/棕色为主,饱和度本来就低,色温一变整个画面色调全偏

5.3 宽动态有帮助,但不是银弹

摄像机自带数字宽动态,单台内部的高对比度场景能处理。但多台拼接时,每台的宽动态参数不同,拼出来的画面亮度还是不均匀。

露天的光照变化是时间维度的问题,不是空间维度的问题。空间上的直方图匹配解决不了时间上的光照变化。


06 最后为什么没做出来

把所有问题摆在一起:

问题 严重程度 是否可解决
安装重叠不够(<10%) 致命 需重新安装
没做标定直接硬拼 致命 需重新标定
分辨率太高算不过来 严重 降分辨率但客户不接受
传输带宽不够 严重 需升级网络设备
露天光照变化大 中等 算法补偿+硬件辅助
现场安装条件受限 严重 需和甲方协调改造

致命问题有两个:安装重叠不够、没做标定。这两个不解决,拼接本身就不成立。

但重新安装意味着甲方要出钱改造立柱和布线,重新标定意味着要停产。甲方不愿意再投入了。

项目就此搁浅。


07 如果重来,我会怎么做

7.1 方案设计阶段就要考虑安装条件

最大的失误是:方案设计时没有去现场看安装条件。

选矿厂的立柱间距、安装高度、视野遮挡——这些决定了摄像头能不能装到要求的位置。我们在办公室画的方案图,到了现场发现很多位置根本装不了。

教训:方案设计之前,必去现场。不然后面全是返工。

7.2 用仿真模拟验证重叠率

在设计阶段,就应该用摄像头的视场角参数(水平180°、垂直90°)和安装位置,做一个简单的仿真:

import numpy as np

def check_overlap(cam1_pos, cam2_pos, h_fov=180, v_fov=90, cam_height=6):
    """估算两台摄像头的地面视野重叠率"""
    # 简化模型:假设地面平坦,计算每台摄像头的地面覆盖范围
    dx = abs(cam1_pos - cam2_pos)
    
    # 水平视场在地面的覆盖半径(简化计算)
    half_fov_rad = np.radians(h_fov / 2)
    cover_radius = cam_height * np.tan(half_fov_rad)
    
    # 重叠区域
    overlap = max(0, 2 * cover_radius - dx)
    overlap_ratio = overlap / (2 * cover_radius) if cover_radius > 0 else 0
    
    return overlap_ratio

# 示例:间距10米,安装高度6米
ratio = check_overlap(cam1_pos=0, cam2_pos=10, cam_height=6)
print(f"重叠率: {ratio:.1%}")

这个仿真不需要多精确,只要能确认"这个间距能不能保证30%重叠"就够了。

7.3 标定必须做,而且要做在线标定

不能再想当然地跳过标定。

而且露天环境的标定会随温度和光照漂移,必须设计在线标定机制——每隔一段时间自动检测标定是否失效,失效了自动提示重新标定。

7.4 分辨率策略:分层处理

不需要所有场景都用最高分辨率。

  • 全景浏览模式:降采样到25%分辨率,保证流畅
  • 重点区域放大模式:切换到原始分辨率
  • 检测分析模式:只对感兴趣区域做高分辨率处理

这样既保证了大屏上的流畅显示,又保留了关键区域的细节。


08 踩坑总结

错误 后果 正确做法
方案设计没去现场 安装条件不满足,重叠不够 方案前必去现场
甲方安装不听指导 间距过大,无重叠 关键工序必须自己人盯
没做标定直接硬拼 画面错位,完全不可用 标定是前提,不能省
全部用最高分辨率 算力爆炸、传输带宽不够 分层处理,按需切换
忽视露天光照变化 一天之内参数全废 算法+硬件双重补偿

写在最后

这是我职业生涯里少数几个没做成的项目之一。

写下这篇文章的时候,我反复在想:如果当时多做了一步——去现场看一眼安装条件,是不是结果就不一样?

答案是:很可能不一样。

因为去现场看了,就会发现立柱间距的问题,就会在方案阶段调整摄像头数量或位置,就不会出现"装上去发现重叠不够"的低级失误。

"去现场"这三个字,在工业视觉项目里,值得说一百遍。

失败不是终点。失败是最好的教材。

如果你也在做类似的拼接项目,希望我的失败经验能帮你少走一段弯路。

*本文所有代码均为示意,核心思路可复现,具体参数需根据实际场景调整。

📎 相关专栏

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐