鱼体尺寸测定的算法演示

华中农业大学工学院王永江

测试前的准备工作

将相机对准某一平面(例如桌面、测试台)固定后,将相机校准使用的测试象棋格摆放到不同角度和位置,拍摄图片,准备校准。下面展示一下获取的校准图片。
numImages = 9;
files = cell(1, numImages);
 
for i = 1:numImages
files{i} = sprintf('image%d.jpg', i);
end
figure;
for k = 1:numImages
img = imread(files{k});
subplot(3, 3, k);
imshow(img);
title(files{k}, 'Interpreter', 'none', 'FontSize', 8);
end

基于图片对相机进行校正,主要是应对鱼眼相机、广角相机等产生的图片变形问题

% 对图片中每个黑白格的顶点进行“像素”定位
[imagePoints, boardSize] = detectCheckerboardPoints(files);
 
% 根据方格的实际尺寸,制作world coordinates,左上角为(0,0).
squareSize = 29; % 单位是毫米
worldPoints = patternWorldPoints("checkerboard",boardSize, squareSize);
 
% 基于实际像素点的位置和世界坐标进行相机校准
imageSize = [size(img, 1), size(img, 2)];
cameraParams = estimateCameraParameters(imagePoints, worldPoints, ...
ImageSize = imageSize);
下面对 cameraParams 结构中各字段的含义逐条用中文进行解释,帮助你理解相机标定结果。
1. 整体概述
2. Camera Intrinsics(相机内参)
  1. 焦距(FocalLength)[fx, fy],表示在横轴和纵轴方向上的等效像素焦距。
  2. 主点(PrincipalPoint)[cx, cy],即图像坐标系中的光心位置(以像素为单位)。
  3. 径向畸变系数(RadialDistortion):如 [k1, k2, ...],用于描述镜头的桶形或枕形畸变。
  4. 切向畸变系数(TangentialDistortion):通常是 [p1, p2],用于刻画镜头镜片与感光面不平行带来的畸变。
  5. 畸变中心(DistortionCenter)(有时可选):如果没有特别指定,一般与主点相同。
3. Camera Extrinsics(相机外参)
4. Accuracy of Estimation(标定精度评估)
  1. MeanReprojectionError(平均重投影误差)

图片变形明显的话,需要对变形图片进行“回直”

[img_new, newIntrinsics] = undistortImage(img, cameraParams, OutputView = "full");
 
figure;
imshowpair(img, img_new, 'montage');

分离图像中的待测量物体

可以是传统的图像处理算法,也可以是复杂的yolo算法等。
% 即有饱和度的HSV格式图像
imHSV = rgb2hsv(img_new);
 
% 饱和度可以作为分离之关键指标
saturation = imHSV(:, :, 2);
 
 
t = graythresh(saturation);
imCoin = (saturation > t);
 
imshow(imCoin)
blobAnalysis = vision.BlobAnalysis(AreaOutputPort = true,...
CentroidOutputPort = false,...
BoundingBoxOutputPort = true,...
MinimumBlobArea = 200, ExcludeBorderBlobs = true);
[areas, boxes] = step(blobAnalysis, imCoin);
 
 
[~, idx] = sort(areas, "Descend");
 
boxes = double(boxes(idx(1:2), :));
 
 
% Insert labels for the coins.
imDetectedCoins = insertObjectAnnotation(uint8(imCoin)*255, "rectangle", ...
boxes, "penny");
figure; imshow(imDetectedCoins);
 
xlim([1199 2653])
ylim([152 1123])

测量硬币的尺寸

% 使用校准后的图片重新获取黑白格顶点坐标
[imagePoints, boardSize] = detectCheckerboardPoints(img_new);
 
% 新的相机内参
camIntrinsics = cameraParams.Intrinsics;
 
% 看看新的相机内参和老的相机内参差别有多大
newOrigin = camIntrinsics.PrincipalPoint - newIntrinsics.PrincipalPoint;
imagePoints = imagePoints + newOrigin; % adds newOrigin to every row of imagePoints
 
% 计算新的外参
camExtrinsics = estimateExtrinsics(imagePoints, worldPoints, camIntrinsics);
 
% 原点重新计算
boxes = boxes + [newOrigin, 0, 0];
 
% 开始测量第一个
box1 = double(boxes(1, :));
imagePoints1 = [box1(1:2); ...
box1(1) + box1(3), box1(2)];
worldPoints1 = img2world2d(imagePoints1, camExtrinsics, camIntrinsics);
 
% 计算直径
d = worldPoints1(2, :) - worldPoints1(1, :);
diameterInMillimeters = hypot(d(1), d(2));
fprintf("第一个硬币的直径 = %0.2f mm\n", diameterInMillimeters);
第一个硬币的直径 = 19.00 mm
box2 = double(boxes(2, :));
imagePoints2 = [box2(1:2); ...
box2(1) + box2(3), box2(2)];
worldPoints2 = img2world2d(imagePoints2, camExtrinsics, camIntrinsics);
 
d = worldPoints2(2, :) - worldPoints2(1, :);
diameterInMillimeters = hypot(d(1), d(2));
fprintf("第二个硬币的直径 = %0.2f mm\n", diameterInMillimeters);
第二个硬币的直径 = 18.86 mm
 
% 获取第一个硬币的圆心
center1_image = box1(1:2) + box1(3:4)/2;
 
center1_world = img2world2d(center1_image, camExtrinsics, camIntrinsics);
 
center1_world = [center1_world 0];
 
% 根据近大远小的几何关系,计算距离
cameraPose = extr2pose(camExtrinsics);
cameraLocation = cameraPose.Translation;
distanceToCamera = norm(center1_world - cameraLocation);
fprintf("计算第一个硬币相对于相机的距离 = %0.2f mm\n", ...
distanceToCamera);
计算第一个硬币相对于相机的距离 = 719.53 mm
 
% 获取第2个硬币的圆心
center2_image = box2(1:2) + box2(3:4)/2;
 
center2_world = img2world2d(center2_image, camExtrinsics, camIntrinsics);
 
center2_world = [center2_world 0];
 
% 根据近大远小的几何关系,计算距离
cameraPose = extr2pose(camExtrinsics);
cameraLocation = cameraPose.Translation;
distanceToCamera = norm(center2_world - cameraLocation);
fprintf("计算第2个硬币相对于相机的距离 = %0.2f mm\n", ...
distanceToCamera);
计算第2个硬币相对于相机的距离 = 854.93 mm