像素坐标映射

像素坐标映射

一、技术背景

在大尺寸图像目标检测中,分块推理(Patch-based Inference)是常用策略。将大图像切分为多个小patch分别推理后,需要将各patch内的局部坐标映射回原始图像的全局坐标。

像素坐标映射涉及三个层面的变换:

  1. Patch内坐标全局坐标:加上patch偏移量
  2. LetterBox坐标原始图像坐标:减去填充、除以缩放比例
  3. 掩码坐标原始尺寸:缩放到原始分辨率

二、数学原理

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 坐标偏移误差

问题描述:分块处理后检测框位置不准确。

解决方案

  1. 确保patch.Region正确记录起始偏移
  2. 在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时分割不完整。

解决方案

  1. 增加重叠区域(减小stride)
  2. 合并相邻patch的检测结果
  3. 后处理时对分割结果进行融合

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图像
}
相关推荐
程序员正茂2 小时前
Android studio中使用OpenCV C++库
android·c++·opencv·mobile
吃好睡好便好2 小时前
MATLAB中图像格式的转换
开发语言·图像处理·学习·计算机视觉·matlab
zh路西法3 小时前
【双目相机 深度估计】PixelXYZ 3D 双目相机标定与实时深度估计全流程
python·opencv·相机标定·深度估计·双目相机·sgbm
不会计算机的g_c__b4 小时前
Python + OpenCV 实现文档扫描与OCR识别系统
python·opencv·ocr
fai厅的秃头姐!15 小时前
OpenCV——进阶
人工智能·opencv·计算机视觉
DFT计算杂谈21 小时前
交错磁研究进展材料物性与交叉应用
数据库·人工智能·python·opencv·算法
小许同学记录成长1 天前
相对辐射定标技术文档
图像处理·算法
小保CPP1 天前
OpenCV C++基于极值区域滤波算法的场景文本检测(OCR)
c++·人工智能·opencv·算法·计算机视觉·ocr
吃好睡好便好2 天前
MATLAB中图像的读取、写入和显示
开发语言·图像处理·学习·计算机视觉·matlab