像素坐标映射
一、技术背景
在大尺寸图像目标检测中,分块推理(Patch-based Inference)是常用策略。将大图像切分为多个小patch分别推理后,需要将各patch内的局部坐标映射回原始图像的全局坐标。
像素坐标映射涉及三个层面的变换:
- Patch内坐标 → 全局坐标:加上patch偏移量
- LetterBox坐标 → 原始图像坐标:减去填充、除以缩放比例
- 掩码坐标 → 原始尺寸:缩放到原始分辨率
二、数学原理
2.1 分块坐标映射
设patch在原始图像中的起始坐标为 (xstart,ystart)(x_{start}, y_{start})(xstart,ystart),patch内检测框坐标为 (xlocal,ylocal)(x_{local}, y_{local})(xlocal,ylocal),全局坐标为:
xglobal=xlocal+xstartx_{global} = x_{local} + x_{start}xglobal=xlocal+xstart
yglobal=ylocal+ystarty_{global} = y_{local} + y_{start}yglobal=ylocal+ystart
2.2 LetterBox坐标还原
设LetterBox缩放增益为 ggg,填充偏移为 (padx,pady)(pad_x, pad_y)(padx,pady),原始坐标计算:
xorig=xletterbox−padxgx_{orig} = \frac{x_{letterbox} - pad_x}{g}xorig=gxletterbox−padx
yorig=yletterbox−padygy_{orig} = \frac{y_{letterbox} - pad_y}{g}yorig=gyletterbox−pady
其中:
g=min(HtargetHorig,WtargetWorig)g = \min\left(\frac{H_{target}}{H_{orig}}, \frac{W_{target}}{W_{orig}}\right)g=min(HorigHtarget,WorigWtarget)
2.3 掩码坐标映射
原型掩码尺寸为 (Hm,Wm)(H_m, W_m)(Hm,Wm),LetterBox尺寸为 (HL,WL)(H_L, W_L)(HL,WL),缩放比例:
scalex=WmWLscale_x = \frac{W_m}{W_L}scalex=WLWm
scaley=HmHLscale_y = \frac{H_m}{H_L}scaley=HLHm
掩码坐标到LetterBox坐标:
xL=xmscalexx_L = \frac{x_m}{scale_x}xL=scalexxm
yL=ymscaleyy_L = \frac{y_m}{scale_y}yL=scaleyym
三、代码实现
3.1 ScaleCoords坐标还原
csharp
// 文件路径: e:\SEM\Yolo11_section\Utils.cs
// 将模型输出的坐标转换为原始图像的坐标
public static SegBoundingBox ScaleCoords(OpenCvSharp.Size letterboxShape, SegBoundingBox box, OpenCvSharp.Size origShape, bool clip = true)
{
// 计算缩放增益(LetterBox使用的缩放比例)
float gain = Math.Min((float)letterboxShape.Height / origShape.Height, (float)letterboxShape.Width / origShape.Width);
// 计算填充偏移量
int padX = (int)Math.Round((letterboxShape.Width - origShape.Width * gain) / 2.0);
int padY = (int)Math.Round((letterboxShape.Height - origShape.Height * gain) / 2.0);
// 坐标还原公式:减去填充偏移,除以增益
int x = (int)Math.Round((box.X - padX) / gain);
int y = (int)Math.Round((box.Y - padY) / gain);
int w = (int)Math.Round(box.Width / gain);
int h = (int)Math.Round(box.Height / gain);
// 可选:裁剪到原始图像边界
if (clip)
{
x = Clamp(x, 0, origShape.Width);
y = Clamp(y, 0, origShape.Height);
w = Clamp(w, 0, origShape.Width - x);
h = Clamp(h, 0, origShape.Height - y);
}
return new SegBoundingBox(x, y, w, h, box.ClassId, box.Score, box.MaskCoefficients);
}
3.2 Postprocess中的坐标映射
csharp
// 文件路径: e:\SEM\Yolo11_section\Utils.cs
// Letterbox恢复比例参数
float gain = Math.Min((float)letterboxSize.Height / origSize.Height, (float)letterboxSize.Width / origSize.Width);
float padW = (letterboxSize.Width - origSize.Width * gain) / 2;
float padH = (letterboxSize.Height - origSize.Height * gain) / 2;
// 掩码坐标缩放比例
float maskScaleX = (float)maskW / letterboxSize.Width;
float maskScaleY = (float)maskH / letterboxSize.Height;
foreach (var idx in nmsIndices)
{
// 将检测框从LetterBox坐标还原到原始图像坐标
var scaledBox = Utils.ScaleCoords(letterboxSize, boxes[idx], origSize);
Rect rectBox = new Rect(scaledBox.X, scaledBox.Y, scaledBox.Width, scaledBox.Height);
// ... 生成掩码 ...
// 裁剪掩码区域,排除padding影响
int x1 = Math.Clamp((int)Math.Round((padW - 0.1f) * maskScaleX), 0, maskW - 1);
int y1 = Math.Clamp((int)Math.Round((padH - 0.1f) * maskScaleY), 0, maskH - 1);
int x2 = Math.Clamp((int)Math.Round((letterboxSize.Width - padW + 0.1f) * maskScaleX), x1, maskW);
int y2 = Math.Clamp((int)Math.Round((letterboxSize.Height - padH + 0.1f) * maskScaleY), y1, maskH);
var cropRect = new Rect(x1, y1, x2 - x1, y2 - y1);
var cropped = new Mat(finalMask, cropRect).Clone();
// 放缩掩码到原始图像尺寸
var resizedMask = new Mat();
Cv2.Resize(cropped, resizedMask, origSize);
}
3.3 分块推理的坐标偏移
csharp
// 文件路径: e:\SEM\Methods\YoloProcessor.cs
public static Result yolo_infer(Mat image, YoloModelContext modelCtx, Rect patchRegion)
{
// ... 推理过程 ...
// ResultProcess中处理patch区域偏移
ResultProcess process = new ResultProcess(factors, 1);
Result result = process.process_seg_result(output_data_det, output_data_pro, patchRegion);
return result;
}
// 在Run方法中收集所有patch的结果并映射到全局
for (int p = 0; p < patches.Count; p++)
{
Patch patch = patches[p];
Result result = yolo_infer(patch.Image, yoloModel, patch.Region);
// 结果已包含全局坐标(通过patchRegion偏移)
for (int i = 0; i < result.length; i++)
{
allBoxes.Add(result.globalrects[i]);
allScores.Add(result.scores[i]);
allmasks.Add(result.masks[i]);
}
}
3.4 PatchHelper分块
csharp
// 文件路径: e:\SEM\Methods\PatchHelper.cs
// 图像分块辅助类(推测实现)
public static List<Patch> CropToPatches(Mat image, int patchWidth, int patchHeight, int strideX, int strideY)
{
var patches = new List<Patch>();
for (int y = 0; y <= image.Rows - patchHeight; y += strideY)
{
for (int x = 0; x <= image.Cols - patchWidth; x += strideX)
{
Rect region = new Rect(x, y, patchWidth, patchHeight);
Mat patchImage = new Mat(image, region).Clone();
patches.Add(new Patch { Image = patchImage, Region = region });
}
}
return patches;
}
四、参数调优
4.1 Patch尺寸选择
| Patch尺寸 | Stride | 适用场景 |
|---|---|---|
| 512×512 | 384 | SEM图像,约25%重叠 |
| 640×640 | 512 | 标准配置 |
| 1024×1024 | 768 | 大目标场景 |
4.2 重叠区域处理
Stride小于Patch尺寸时会产生重叠区域:
- 重叠比例 :Patch−StridePatch\frac{Patch - Stride}{Patch}PatchPatch−Stride
- 512/384配置:重叠约25%
csharp
// 使用IoU和掩码面积决定保留哪个重叠检测
int bestArea = allmasks[i] != null ? Cv2.CountNonZero(allmasks[i]) : allBoxes[i].Width * allBoxes[i].Height;
if (areaJ > bestArea)
{
removed[best] = true; // 保留面积更大的检测
best = j;
}
4.3 边界处理
csharp
// 确保检测框不超出图像边界
roi = roi.Intersect(new Rect(0, 0, origSize.Width, origSize.Height));
if (roi.Width > 0 && roi.Height > 0)
{
resizedMask[roi].CopyTo(finalBinaryMask[roi]);
}
五、常见问题
5.1 坐标偏移误差
问题描述:分块处理后检测框位置不准确。
解决方案:
- 确保patch.Region正确记录起始偏移
- 在ResultProcess中正确应用偏移:
csharp
// globalX = localX + patchRegion.X
// globalY = localY + patchRegion.Y
5.2 掩码与检测框不匹配
问题描述:掩码区域与检测框位置不一致。
解决方案:
csharp
// 仅在检测框区域内应用掩码
var roi = new Rect(seg.BBox.X, seg.BBox.Y, seg.BBox.Width, seg.BBox.Height);
roi = roi.Intersect(new Rect(0, 0, origSize.Width, origSize.Height));
if (roi.Width > 0 && roi.Height > 0)
{
resizedMask[roi].CopyTo(seg.FullMask[roi]);
}
5.3 浮点转整数精度损失
问题描述:坐标四舍五入导致偏差累积。
解决方案:
csharp
// 使用Math.Round进行四舍五入
int x = (int)Math.Round((box.X - padX) / gain);
// 对于关键坐标使用 Clamp 确保边界
x = Clamp(x, 0, origShape.Width);
5.4 跨patch目标分割不完整
问题描述:大目标跨越多个patch时分割不完整。
解决方案:
- 增加重叠区域(减小stride)
- 合并相邻patch的检测结果
- 后处理时对分割结果进行融合
5.5 内存管理
问题描述:分块处理大量patch时内存占用高。
解决方案:
csharp
// 及时释放每个patch处理后的临时对象
for (int p = 0; p < patches.Count; p++)
{
Patch patch = patches[p];
Result result = yolo_infer(patch.Image, yoloModel, patch.Region);
// ... 处理结果 ...
patch.Image.Dispose(); // 释放patch图像
}