单目相机测高实战:基于MATLAB从像素坐标到真实高度的几何测量

发布时间:2026/9/28 16:57:22
单目相机测高实战:基于MATLAB从像素坐标到真实高度的几何测量 用一张二维图像去估算场景里某个物体的真实高度这个需求粗听起来很反直觉——毕竟单张照片已经丢了深度信息测高度不是应该先知道距离吗但只要你把一台固定的单目相机架好把地面和相机高度这两个约束用起来事情就变得非常可靠。我这次就用一台 ZED 相机开单目模式对着操场旁边一根已知高度的立杆只靠 MATLAB 把图像当矩阵处理不依赖双目视差也不跑任何深度模型完整做了一遍“从二维像素到真实高度”的测高实验。这个实验本身不难真正麻烦的地方全在于细节相机内参标定、像素坐标选点、坐标转换公式、远距离误差放大。文章里我会把完整流程写出来包括标定步骤、公式推导、可复跑的 MATLAB 代码、一组实测数据和踩坑记录适合刚接触机器视觉、想用单目相机做几何测量的同学参考。如果你手里只有一台普通 USB 摄像头这套流程一样能跑通。1. 先拆思路一张二维图像凭什么能算出真实高度1.1 单目测高不是“玄学”核心是给相机加上几何约束单目相机成像的过程本质上是把三维空间投影到二维平面上这个投影过程中深度信息确实丢了但注意丢掉的只是“绝对值”并没有丢掉“几何关系”。只要你在拍摄场景里补充几个先验条件二维图像里的像素点反而能反推出三维信息。这一步最关键的就是“加约束”。我这次实验只用了三个假设相机是固定安装的光心离地高度 Hc 已知目标站立的地面是平面至少目标底部和相机光心投影点处于同一个水平面相机光轴近似水平或者俯仰角已经通过标定确认。这三个条件一成立事情就变成了两个共线问题目标顶部、光心、顶部像点三点共线目标底部、光心、底部像点也三点共线。像点的像素坐标能从图里直接拿光心位置是已知的地面平面方程也是已知的那么目标顶部和底部的空间位置就被两条视线唯一确定了高度自然算得出来。生活里也有类似的例子。你站在一个固定位置看一个杯子虽然看不出它的绝对距离但只要你知道杯底所在的桌面离你多远、你的眼睛高度多高再根据杯沿在视野里的角度就能用三角函数算出杯子的高度。单目测高就是这个道理只不过把“人眼角度”换成了“像素坐标经过标定后的精确角度”。1.2 两条技术路线的取舍几何比例法 vs 灭点法真正上手做单目测高常见的路线其实有两条。第一条是几何比例法这也是我这次实验采用的方案已知相机高度和水平距离直接通过像素高差按相似三角形比例换算高度。第二条是灭点法它不需要量水平距离而是在画面里寻找平行线的灭点借助参考物高度完成比例恢复。两条路线各有各的适用场景我把它们的差异整理在下面对比项几何比例法灭点法需要的已知量相机高度、目标底部到光心的水平距离或通过底部像素反推画面中一组平行线对应的灭点、一个参考物高度适用场景固定机位、安装高度可控、地面平坦单张照片、自然场景有建筑或球场等平行线精度近距离可以做到几厘米内受参考物高度和灭点位置误差影响较大实现难度低公式简单代码量少中灭点估计和交比计算容易出错自动化程度高可用于批量测高中需要人辅助找平行线或灭点我选几何比例法的主要原因有三点一是实验场景是固定机位控制条件好二是公式简洁便于在 MATLAB 里快速验证三是后面想扩展成自动批量测高几何比例法只需要目标顶部和底部两个点天然适合连通域提取。灭点法更适合“只有一张随手拍的照片、没有任何距离信息”的野路子场景我把它放在第 3 章作为补充方案工程上可以作为兜底。1.3 为什么这次试验选用 ZED 的单目模式这里要特别说清楚一个容易混淆的点我用的设备是 ZED但它是一台双目深度相机而我这次实验并没有用到它的深度输出纯粹只取左目图像当成普通单目相机来用。换句话说整个测高算法和双目视差没有任何关系哪怕换成一台 600 块的 USB 摄像头跑出来的流程也是一样的。为什么非要拿 ZED 来当单目相机用一方面是因为手边正好有这台设备ZED 的左目成像质量确实不错分辨率高、畸变控制相对稳定标定之后内参结果比较干净。另一方面我也想让这套流程具备“可迁移性”既然单目模式下能做那任何只有单目相机的现场环境就都能做不需要额外依赖深度传感器。这个结论在后续我接到很多固定工位测高需求时很有参考价值。还有一点是同步性的问题。ZED 的 SDK 能直接提供干净的左目彩色流不用自己折腾 USB 带宽省了不少事。如果你没有 ZED用一个普通工业相机或者好一点的 USB 摄像头也能复现。关键是相机要固定画面里不要有人为遮挡标定要做扎实。2. 动手前必须先做的一步相机标定与内参获取2.1 内参到底是什么为什么它决定测高精度测高计算的第一步不是打开 MATLAB 写公式而是先把相机“摸清楚”。相机内参包括焦距 fx、fy主点 cx、cy以及径向畸变、切向畸变系数。内参矩阵 K 的常见形式是K [fx, 0, cx; 0, fy, cy; 0, 0, 1]你可以把 fx、fy 理解为“相机这把尺子的刻度”cx、cy 是镜头光轴正对的那个像素位置。如果这把尺子的刻度不准你后续算出来的视线角度就会系统性偏移高度结果自然也是错的。而且这种误差是乘法性的会随着距离增加被放大近距离可能看不出来目标一旦拉远偏差就非常明显。我见过不少人跳过了标定步骤直接用照片 EXIF 里的焦距或者镜头标称焦距去算结果误差大得离谱。原因在于镜头标称焦距是以毫米为单位的物理焦距而像素坐标系里参与计算的是同一个焦距在传感器像素密度下的表达这两个数只有在传感器尺寸和像素尺寸都已知的情况下才能换算。与其折腾换算不如直接用标定出来的像素焦距省心也准确。2.2 用棋盘格自己做标定MATLAB 操作流程标定棋盘格这件事听起来高大上实际做起来就是打印一张棋盘图贴在硬纸板或者亚克力板上然后拿着它在相机前拍一段视频或者一组照片。需要注意的坑有三个。第一棋盘不能折皱。打印纸如果只是平放弯曲会造成角点位置不准确标定出来的畸变参数会有明显偏差最好贴在硬板上。第二照片要覆盖画面各个区域尤其是边缘和四个角因为畸变主要发生在边缘。如果你拍的棋盘全部集中在画面中心标定完边缘区域的畸变补偿基本等于没做。第三拍摄角度要多样化正拍、侧拍、转个角度拍这样才能把镜头的透视变形信息充分暴露给标定算法。MATLAB 里可以用 Camera Calibrator App也可以直接用脚本跑。我更习惯脚本方式方便批量处理。核心代码如下% 假设你已经把棋盘照片放在 calib_imgs 文件夹里 images imageDatastore(fullfile(pwd, calib_imgs)); % 检测棋盘内角点 [imagePoints, boardSize] detectCheckerboardPoints(images.Files); % 输入棋盘格实际边长单位建议用毫米 squareSize 30; % 每个棋盘格边长 30mm % 生成世界坐标系下的角点坐标 worldPoints generateCheckerboardPoints(boardSize, squareSize); % 估计相机参数 cameraParams estimateCameraParameters(imagePoints, worldPoints, ... EstimateSkew, true, NumRadialDistortionCoefficients, 2); % 保存结果后面测高脚本直接加载 save(cameraParams.mat, cameraParams);标定完成后可以检查一下重投影误差一般小于 0.3 像素就算不错。如果某个角度拍得特别差重投影误差会明显偏高可以直接删掉那张照片重新标定不用心疼。2.3 把内参换算成像素单位再写进代码标定出来的 cameraParams 对象里FocalLength 字段就是像素单位的焦距 fx、fyPrincipalPoint 字段是像素单位的主点 cx、cy。这两个字段直接用于测高计算不需要再做任何毫米和像素之间的换算。有一种常见的误解是觉得标定出来的 fx 和 fy 相差一点点无所谓。实际上 fx 和 fy 由于传感器像素不是严格正方形往往会有细微差别虽然差别通常很小但是像素选点如果落在图像边缘这种细小差异也会被镜头畸变放大。写代码时不要偷懒只取一个 f而要 fx 和 fy 分开用垂直方向高度计算严格用 fy水平方向宽度计算严格用 fx这样才不会引入不必要误差。保存好 cameraParams.mat 后所有后续测高脚本都可以统一加载调用。这一步做完后面所有计算都有了可靠的基础。3. 核心换算原理从像素坐标到地面坐标到真实高度3.1 相似三角形推导一句话版本测高原理用一句话说就是目标高度等于相机高度加上目标顶部超出光轴的那段高度而这段高度可以用“水平距离乘以顶部视线相对光轴夹角的正切值”得到。展开成公式就是h Hc D × (cy - yt) / fyHc相机光心离地高度单位米D目标底部到光心在地面投影点的水平距离单位米cy图像主点的纵坐标单位像素yt目标顶部在图像中的纵坐标单位像素fy标定得到的垂直方向焦距单位像素。这里的正负号需要特别注意图像坐标系里 v 轴是向下的所以目标顶部如果高于光轴它在图像上会落在主点上方也就是 yt 小于 cy那么 cy - yt 就是正的目标整体高度就会高于相机高度。反之如果目标顶部低于相机光轴yt 大于 cycy - yt 是负的算出来的高度就低于相机高度。这个符号逻辑搞反了测出来高低会完全颠倒。那 D 怎么来最直接的办法是拿卷尺量。但更好的做法是利用目标底部像素自动反推。当相机光轴水平、地面平坦时底部点、光心、地面投影点形成一个直角三角形。底部视线相对光轴向下偏转的角度 β满足tanβ (yb - cy) / fy Hc / D所以D Hc × fy / (yb - cy)也就是说你只需要在图像里点两个点一个是底部一个是顶部水平距离可以自动算出来连卷尺都不用掏。这也是固定机位测高能做到全自动化的关键。3.2 一个具体的数值示例手算一遍为了让公式落地我拿一组自己实验里标定出来的近似参数走一遍计算过程。假设 ZED 左目图像分辨率 2208×1242标定得到fx 1104.0fy 1098.0像素cx 1104.0cy 621.0像素相机光心高度 Hc 1.50 米图像中目标底部的纵坐标 yb 827 像素目标顶部的纵坐标 yt 437 像素。先反推水平距离D 1.50 × 1098 / (827 - 621) ≈ 1.50 × 5.30 ≈ 7.95 米然后算高度h 1.50 7.95 × (621 - 437) / 1098 ≈ 1.50 7.95 × 0.1676 ≈ 2.83 米如果这根杆的实际高度是 2.80 米那误差大概在 3 厘米左右。这个精度在 8 米距离上已经相当能看了。你可以把数据代回去体会一下底部视线俯角的正切是 1.5 / 7.95 ≈ 0.1887顶部视线仰角的正切是 (621-437)/1098 ≈ 0.1676光轴本身在 1.5 米高度那么目标顶部就是 1.5 0.1676 × 7.95 ≈ 2.83 米整个过程就是一个已知三角形角度求边长的过程。3.3 不量距离也能测灭点法的工程思路在一些场景下你拿到的照片是别人拍的没有任何距离信息连相机高度都不知道这时候几何比例法就行不通了。灭点法可以作为一种兜底。核心思想是空间中的平行线投影到图像上会交于一点这个点叫灭点。比如建筑物竖直的棱在图像里延长后交汇的位置就是竖直灭点。如果你能找到一个已知高度的参考物比如一个 1.8 米高的人或者一扇标准高度的门并且目标底部和参考物底部近似处于同一个地平面上那可以用下面的近似公式h_target h_ref × (yt - vp) / (yr - vp)其中 vp 是竖直灭点的纵坐标yt 是目标顶部纵坐标yr 是参考物顶部纵坐标具体数值都是像素单位。这个公式的原理是在透视投影下同一地平面上竖直物体的高度比等于各自顶部到灭点的像素距离之比。灭点法看起来美好工程上用起来其实容易翻车。一方面灭点位置的微小误差会被除法放大另一方面参考物高度、目标位置、相机姿态都不好评测。如果参考物和目标不在同一个地平面上测出来完全没意义。所以我更愿意把它当成一个应急方案而不是主力方法。常规固定安装的测高场景几何比例法又准又稳优先用它。4. MATLAB 实操图像矩阵、选点与测高代码跑通4.1 图像本来就是矩阵imread 到底读回来了什么很多人对“图像处理”有个误解觉得图像是一张“图片”但其实在 MATLAB 里图像读取进来后就是一个数值矩阵。用 imread 读一张彩色 JPG得到的是一个 H×W×3 的三维数组H 是图像高度W 是宽度3 是 RGB 三个通道。如果你用 rgb2gray 把它转成灰度图得到的就是标准的二维矩阵矩阵每个元素的值是 0 到 255 的灰度。用代码验证一下就非常直观img imread(scene.jpg); size(img) % 比如 1242 2208 3 grayImg rgb2gray(img); size(grayImg) % 1242 2208测高计算里我关心的其实只是这个二维矩阵里的坐标位置。图像里的“一个点”本质上就是矩阵里的某个元素它的行列索引就是像素坐标。选点、提取顶部和底部、批量找连通域这些操作全部是在矩阵上做索引。理解了这一点后面的代码看起来就是一目了然的矩阵运算。4.2 取像素坐标与畸变校正在实际选点之前还有一个步骤别漏掉畸变校正。原始图像在边缘区域会有明显的桶形畸变或枕形畸变如果你的目标恰好出现在画面边缘但标定出来边缘补偿又不做测量误差会迅速扩大。整体校正图像可以用 undistortImage 函数把整张图变成畸变校正后的图然后再选点。这样做最简单人眼看到的画面也更符合真实透视关系。如果不想对整图做校正也可以用 undistortPoints 函数只对选出来的像素点做畸变补偿更适合批量处理大量点的情况。我这次实验用的是整图校正因为单张图处理速度完全来得及。选点我直接用 MATLAB 内置的 ginput 函数。它的交互方式很简单imshow 显示图像后鼠标左键点一下记录一个点。我规定先点目标底部再点目标顶部顺序不能乱否则代码里 yb 和 yt 就搞反了。imshow(I); [x, y] ginput(2); % 依次点击底部、顶部需要提醒的是ginput 返回的 x、y 是浮点数坐标但在图像矩阵索引里最好四舍五入成整数避免下标越界或者出现半像素的逻辑错误。4.3 完整测高代码与一步步说明下面是一个完整的测高脚本包含了加载标定参数、畸变校正、选点、自动反推距离、计算高度全流程。直接复制到你的 MATLAB 环境里替换文件路径就能跑。% 加载相机标定参数 load(cameraParams.mat); % 读取原始图像 img imread(scene.jpg); % 整图去畸变 I undistortImage(img, cameraParams); % 显示图像并手动选取目标底部和顶部 imshow(I); title(先点目标底部再点目标顶部); [x, y] ginput(2); % 提取底部和顶部像素坐标 xb round(x(1)); yb round(y(1)); xt round(x(2)); yt round(y(2)); % 提取相机内参 fx cameraParams.FocalLength(1); fy cameraParams.FocalLength(2); cx cameraParams.PrincipalPoint(1); cy cameraParams.PrincipalPoint(2); % 相机光心离地高度单位米用尺子量出来填进去 Hc 1.50; % 检查底部点是否位于主点下方 if yb cy error(底部点没有位于主点下方请检查相机光轴是否水平或重新选点); end % 通过底部像素反推水平距离 D Hc * fy / (yb - cy); % 通过顶部像素反推目标高度 dyTop (cy - yt) / fy; h Hc D * dyTop; fprintf(底部估算距离 D %.2f m\n, D); fprintf(目标高度 h %.3f m\n, h);代码本身不复杂关键点有两个。第一Hc 必须是“光心”高度而不是三脚架顶部的安装高度。如果要精确要把相机机身厚度考虑进去或者直接在标定后用一个已知高度的目标反推一次 Hc这样更靠谱。第二yb 如果小于 cy说明底部点的视线是向上走的那要么是目标底部不在地面上要么是相机光轴没有水平这时候公式已经不适用要停下来查原因而不是硬算。跑通上面的代码后你不需要再人工量距离只需要一张图和两个鼠标点击就能输出目标高度。4.4 批量测高的自动化思路如果需要测的不是单个目标而是画面里几十个目标手动 ginput 就太累了。这时候可以把选点过程自动化。思路是先提取目标区域取每个区域的最低点作为底部、最高点作为顶部然后把所有点的坐标拼成矩阵一次性代入测高公式。我给出一个简化版的思路代码适用于画面简单、背景干净、目标有明显对比度的场景% 灰度化并自适应二值化 grayImg rgb2gray(I); bw imbinarize(grayImg, adaptive); % 去掉小面积噪声 bw bwareaopen(bw, 200); % 提取连通域 stats regionprops(bw, PixelList); n numel(stats); positions zeros(n, 2); % 每行底部y、顶部y for i 1:n pix stats(i).PixelList; % pix 每行是 [x, y] yMin min(pix(:, 2)); % 最高点对应目标顶部 yMax max(pix(:, 2)); % 最低点对应目标底部 positions(i, :) [yMax, yMin]; end % 后续将 positions 代入测高公式做矩阵运算即可这段代码在实际场景里需要根据目标颜色、背景复杂度做不少调整但它证明了测高逻辑可以高度向量化。图像本身就是矩阵坐标批量提取完之后所有目标的高度计算也就是一两条矩阵运算的事。如果你的场景更复杂目标先要经过目标检测模型框出来再做连通域最高最低点提取逻辑是一样的只是前置处理不同。5. 现场实验记录误差、踩坑与排查清单5.1 一组实测数据8个距离下的测高结果实验目标是一根已知高度为 2.00 米的立杆杆顶贴了一个高反光标记方便选点。相机固定在三脚架上光心高度 1.50 米分别把立杆放在 5 米、8 米、12 米、15 米、20 米的位置各测一次结果如下距离米估算高度米真值米误差米51.982.00-0.0282.012.000.01122.042.000.04152.082.000.08202.152.000.15近距离 5 到 8 米时误差基本落在两三厘米以内完全够日常测量需求。12 米开始误差逐渐放大到 20 米时已经偏高了 15 厘米。原因并不难理解目标越远底部像素越接近主点yb - cy 的值越小反推水平距离时相同的像素量化误差造成的距离误差越大。误差经过距离这个中间变量最后传导到高度结果上自然就放大了。这里也说明了一个实用规则单目测高有一个合理的量程范围。以我的实验参数来看目标距离控制在 10 米以内误差表现非常理想超过 15 米就要认真考虑误差是否符合现场要求了。5.2 误差从哪来五类常见问题第一类是标定残留误差。棋盘格照片数量不够、角度不够广、棋盘边缘没有覆盖画面四角都会让畸变参数不够准。尤其是画面边缘出现明显的测量偏差大概率就是畸变没有校正干净重新做一遍更充分的标定通常能解决。第二类是相机光轴不水平。这一点在固定机位安装里最容易踩坑。三脚架水平气泡显示水平并不代表镜头光轴绝对水平因为云台本身可能存在微小倾斜。光轴只要偏 1 度近距离影响不大远距离测高误差会成倍放大。我试过用手持水平仪贴平相机顶面再测比只靠三脚架气泡靠谱很多。第三类是地面不平。公式里假设目标底部和相机光心投影点在同一水平面。如果地面有坡度比如测量目标是站在斜坡上那么 D 和 Hc 之间的关系就变了测出来的高度肯定不准。第四类是选点误差。目标顶部如果和背景颜色接近人眼选点经常会有几个像素的偏差。具体影响可以用一个式子估算每像素的高度偏差大约等于 D / fy。假设 D20 米fy1098那么 1 个像素偏差对应高度误差约 1.8 厘米。如果连续偏了好几个像素误差就上去了。解决方法是给目标顶部和底部贴高对比度标记或者用亚像素精度的角点检测算法。第五类是底部点不可见。如果目标底部被草丛、栏杆之类的东西挡住yb 是猜出来的整个距离反推就不可靠。这种情况要么用附近已知的地面参考线延展底部位置要么改用灭点法。5.3 常见问题排查速查表现象可能原因处理办法短距离结果可以长距离系统偏高底部像素接近主点距离反推信噪比下降缩短测量距离或改用人工输入水平距离同一目标换位置测误差不稳定相机光轴不水平或地面有坡度重新调平相机或标定外参俯仰角画面边缘目标偏差大畸变校正不足标定时覆盖画面边缘或选点后用 undistortPoints目标顶部与背景难以区分光照、反光、颜色接近加高对比度标记放大图像后选点底部被遮挡无法选点目标底部不可见用地面参考线外推或改用灭点法换了相机支架后参数全乱忘记重新标定每次改变机位或镜头后重新标定并保存这些坑几乎都是我实际踩到过的。尤其是中途换过一次三脚架没有重新标定导致后面测出来的数据整体偏移排查了半天才发现是相机高度变了。固定机位测高的铁律就是相机位置、高度、镜头焦距任何一项变了哪怕只是拧了一下镜头环都必须重新标定一次。我个人在实际操作中最深的体会是单目测高的精度上限不取决于算法而取决于你对场景假设的把控程度。标定这步多花十分钟后面整个实验会省下至少一小时。另外如果打算长期用这套方案建议把标定结果、相机高度、常用距离都存成一个配置文件每次跑脚本自动加载这样最省心。后续我也准备把相机俯仰角的外参标定加进去让这套流程不光适用于水平光轴也能处理更多复杂安装角度。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询