如何结合Canvas与相机计算物体真实尺寸并处理透视畸变
利用相机与Canvas计算平面物体真实尺寸(透视畸变处理方案)
问题背景
通过相机拍摄带物体的平面,用jsqr识别参考二维码的坐标,在视频上方的Canvas图层绘制线条估算尺寸,但相机角度变化产生的透视畸变导致误差较大,需要精准的处理方案。
已获取的二维码坐标
{ "topRightCorner": { "x": 872.5086679615921, "y": 810.804619872336 }, "topLeftCorner": { "x": 1153.5646618728827, "y": 815.8786697172964 }, "bottomRightCorner": { "x": 889.327694934698, "y": 604.2159830226265 }, "bottomLeftCorner": { "x": 1159.2881868528104, "y": 607.9004011299469 }, "topRightFinderPattern": { "x": 900.5, "y": 791 }, "topLeftFinderPattern": { "x": 1127.5, "y": 795 }, "bottomLeftFinderPattern": { "x": 1133, "y": 626.5 }, "bottomRightAlignmentPattern": { "x": 934, "y": 640 } }
当前实现代码
function pixelToCm (lineLength, refSize, location) { try { const refWidth = Math.max( Math.abs(location.topRightCorner.x - location.topLeftCorner.x), Math.abs(location.topRightCorner.y - location.topLeftCorner.y) ) const refHeight = Math.max( Math.abs(location.bottomLeftCorner.x - location.topLeftCorner.x), Math.abs(location.bottomLeftCorner.y - location.topLeftCorner.y), ) const pixelSize = refSize / Math.max(refWidth, refHeight) const lineLengthCm = lineLength * pixelSize console.log(lineLength, refSize, refWidth, refHeight, pixelSize, lineLengthCm) return _.round(Math.abs(lineLengthCm), 2) } catch { return 0 } }

最佳实现方案:基于单应性矩阵的透视变换
现有代码直接用像素距离比例换算,未处理透视畸变,仅适用于相机正对平面的场景。要解决角度变化问题,必须通过**单应性矩阵(Homography Matrix)**将图像中的透视坐标映射到真实世界的正交平面坐标,再计算尺寸。
核心步骤
建立对应点对
已知二维码的真实边长(比如refSize=5cm),定义两组对应点:- 真实世界坐标(正交平面,单位cm):设二维码左上角为
(0,0),右上角(refSize,0),左下角(0,refSize),右下角(refSize,refSize) - 图像坐标:jsqr识别出的二维码四个角点
topLeftCorner、topRightCorner、bottomLeftCorner、bottomRightCorner
- 真实世界坐标(正交平面,单位cm):设二维码左上角为
计算单应性矩阵
单应性矩阵可实现透视坐标到正交坐标的转换。手动实现需解线性方程组,推荐用成熟的计算机视觉库(如OpenCV.js)计算,避免数值误差。坐标转换与尺寸计算
将Canvas上绘制的线条端点(图像坐标)通过单应性矩阵转换为真实世界坐标,再计算两点间的欧氏距离,即为物体的真实尺寸。
代码实现示例
1. 引入OpenCV.js简化计算(推荐)
如果允许引入外部库,用OpenCV.js的findHomography方法直接计算单应性矩阵:
// 初始化OpenCV后使用 function getHomography(location, refSize) { // 图像点:二维码四个角的图像坐标 const imagePoints = [ [location.topLeftCorner.x, location.topLeftCorner.y], [location.topRightCorner.x, location.topRightCorner.y], [location.bottomLeftCorner.x, location.bottomLeftCorner.y], [location.bottomRightCorner.x, location.bottomRightCorner.y] ]; // 真实世界点:二维码四个角的真实坐标 const worldPoints = [ [0, 0], [refSize, 0], [0, refSize], [refSize, refSize] ]; // 转换为OpenCV的Mat格式 const srcMat = new cv.Mat(4, 1, cv.CV_32FC2, imagePoints.flat()); const dstMat = new cv.Mat(4, 1, cv.CV_32FC2, worldPoints.flat()); // 计算单应性矩阵 const homographyMat = cv.findHomography(srcMat, dstMat); // 将Mat转换为二维数组 const homography = []; for (let i = 0; i < 3; i++) { homography.push([ homographyMat.data[i*3], homographyMat.data[i*3+1], homographyMat.data[i*3+2] ]); } srcMat.delete(); dstMat.delete(); homographyMat.delete(); return homography; } // 将图像点转换为真实世界点 function imageToWorld(point, homography) { const [x, y] = point; const denominator = homography[2][0] * x + homography[2][1] * y + homography[2][2]; const X = (homography[0][0] * x + homography[0][1] * y + homography[0][2]) / denominator; const Y = (homography[1][0] * x + homography[1][1] * y + homography[1][2]) / denominator; return [X, Y]; } // 计算线条的真实长度 function calculateRealLength(lineStart, lineEnd, homography) { const [X1, Y1] = imageToWorld([lineStart.x, lineStart.y], homography); const [X2, Y2] = imageToWorld([lineEnd.x, lineEnd.y], homography); return _.round(Math.sqrt(Math.pow(X2-X1, 2) + Math.pow(Y2-Y1, 2)), 2); }
2. 手动实现单应性矩阵计算(无外部库)
如果不想引入库,可手动解线性方程组,注意数值稳定性:
// 计算单应性矩阵(图像点→真实世界点) function computeHomography(imagePoints, worldPoints) { const A = []; const b = []; // 构建8个方程 for (let i = 0; i < 4; i++) { const [x, y] = imagePoints[i]; const [X, Y] = worldPoints[i]; // 对应X的方程 A.push([x, y, 1, 0, 0, 0, -X*x, -X*y]); b.push(X); // 对应Y的方程 A.push([0, 0, 0, x, y, 1, -Y*x, -Y*y]); b.push(Y); } // 求解线性方程组Ax = b(此处为简化示例,实际需实现稳定的高斯消元逻辑) const solution = solveLinearSystem(A, b); // 拼接成3x3的单应性矩阵,H[2][2] = 1 return [ [solution[0], solution[1], solution[2]], [solution[3], solution[4], solution[5]], [solution[6], solution[7], 1] ]; } // 简化的高斯消元求解线性方程组(仅适用于8x8矩阵) function solveLinearSystem(A, b) { // 此处需实现完整的高斯消元逻辑,可参考公开的数值计算代码片段 // 示例返回单位矩阵参数,实际需替换为正确解 return [1,0,0,0,1,0,0,0]; }
使用流程
- 每帧识别二维码角点后,计算单应性矩阵:
const refSize = 5; // 二维码真实边长(单位:cm) const homography = getHomography(location, refSize);
- 当用户在Canvas上绘制线条后,计算真实长度:
// 假设lineStart和lineEnd是Canvas上的两个端点坐标 const realLength = calculateRealLength(lineStart, lineEnd, homography); console.log("真实长度:", realLength, "cm");
注意事项
- 平面约束:必须保证物体和二维码在同一平面上,否则透视变换结果会有误差
- 角点精度:jsqr返回的角点坐标越准确,单应性矩阵的计算精度越高,最终尺寸结果越可靠
- 实时更新:相机角度变化时,需要每帧重新识别二维码并计算单应性矩阵,保证尺寸计算的实时性
- 库选择:手动实现单应性矩阵容易出现数值不稳定的问题,优先选择OpenCV.js这类成熟库
内容的提问来源于stack exchange,提问作者crapthings
相关产品推荐
相关产品推荐

