项目实践 - 使用 MaixCAM 2 通过 YOLO 11 实现五金紧固件精确识别

进行前思考

让 MaixCAM2 使用板载摄像头,在本地运行自训练的 YOLO 模型,识别不同种类的螺丝、螺母和垫片,估计它们在工作平面上的位置与朝向,再通过 UART 把结果发送给 STM32。

项目应最终实现多类别检测、平面位置与姿态估计、实时通信与控制以及拓展性的科研实验与性能分析。

基础知识构建

IoU 衡量两个框的重叠程度

Intersection over Union,交并比,是目标检测中衡量预测框与真实框重叠程度的核心指标。

IoU=交集面积A面积+B面积−交集面积\mathrm{IoU} = \frac{\text{交集面积}}{\text{A面积} + \text{B面积} - \text{交集面积}}

MaixCAM 2 识别的框可以转换为(x, y, w, h),而在使用 IoU 公式前,应先把(x, y, w, h)转成两个角点:

左上角:(x, y)
右下角:(x + w, y + h)

由此可得两个框的交集边界是:

左边界 = 两个左边界的较大值
上边界 = 两个上边界的较大值
右边界 = 两个右边界的较小值
下边界 = 两个下边界的较小值

因此,交集的宽高分别是

交集宽 = max(0, 右边界 - 左边界)
交集高 = max(0, 下边界 - 上边界)

由公式可得,IoU 在 0 到 1 之间,0 表示没有面积重叠,1 表示两个正面积框完全重合。它只衡量位置重叠,不判断类别是否正确。

  • 两个框的 IoU 很高,能否证明预测类别正确?为什么?

不能,IoU 只计算框的位置和面积,不读取类别,所以高 IoU 不能证明类别正确,也不意味着标注错误。

实现方法:

def iou_xywh(a, b):
    # a, b: (x, y, w, h)
    ax, ay, aw, ah = a
    bx, by, bw, bh = b

    # transfer
    ax1, ay1 = ax, ay
    ax2, ay2 = ax + aw, ay + ah
    bx1, by1 = bx, by
    bx2, by2 = bx + bw, by + bh

    # intersection border
    left_border = max(ax1, bx1)
    top_border = max(ay1, by1)
    right_border = min(ax2, bx2)
    botton_border = min(ay2, by2)

    # intersection area
    inter_w = max(0.0, right_border - left_border)
    inter_h = max(0.0, botton_border - top_border)

    inter_area = inter_h * inter_w

    # IoU
    a_area = aw * ah
    b_area = bw * bh
    union_area = a_area + b_area - inter_area

    if union_area == 0:
        return 0.0
    else:
        return inter_area / union_area

归一化坐标

在之前使用的框是像素左上角与宽高(x, y, w, h)。在进行标注前应转换为归一化的中心与宽高:

cx′=x+w/2W,cy′=y+h/2H,w′=wW,h′=hHc_x'=\frac{x+w/2}{W},\quad c_y'=\frac{y+h/2}{H},\quad w'=\frac{w}{W},\quad h'=\frac{h}{H}

由公式可以很容易实现互相转换:

def xywh_to_normalize(box, image_w, image_h):
    x, y, w, h = box

    c_x = (x + w / 2) / image_w
    c_y = (y + h / 2) / image_h
    w_1 = w / image_w
    h_1 = h / image_h

    return c_x, c_y, w_1, h_1

def normalize_to_xywh(box, image_x, image_h):
    c_x, c_y, w_1, h_1 = box
    
    w = w_1 * image_w
    h = h_1 * image_h
    x = c_x * image_w - w / 2
    y = c_y * image_h - h / 2

    return x, y, w, h

置信度筛选与 NMS

一个物体可能产生多个相近的候选框。我们先按分数筛选,再用 NMS 减少重复框:

  1. 删除分数低于 conf_th 的候选。
  2. 选出剩余候选中分数最高的框,保留它。
  3. 删除与它同类别且 IoU 超过 iou_th的其他候选。
  4. 对剩余候选重复上述过程。

需要特别注意的是两个阈值的区别:

参数增大直接影响
conf_th分数筛选更严格,进入 NMS 的候选更少
iou_th需要重叠更多才删除,NMS 更宽松

Python 实现思路:

  1. 保留 score >= conf_th 的候选,按分数降序排列。
  2. 取出当前最高分框,加入结果。
  3. 筛选剩余候选:仅删除与当前框同类且 IoU > iou_th 的框。
  4. 重复,直到没有候选。
def nms(detections, conf_th=0.5, iou_th=0.5):
    candidates = [
        det for det in detections
        if det["score"] >= conf_th
    ]
    candidates.sort(key=lambda det: det["score"], reverse=True)

    kept = []

    while candidates:
        current = candidates.pop(0)
        kept.append(current)

        remaining = []
        for det in candidates:
            same_class = det["class_id"] == current["class_id"]
            if same_class and iou_xywh(current["box"], det["box"]) > iou_th:
                continue
            remaining.append(det)

        candidates = remaining
    
    return kept

RGB 图像处理

彩色图像可以看作一个数值数组。常见 RGB 图像有三个通道,每个像素包含红、绿、蓝三个数值。

两种排列方式是:

布局形状访问方式
HWC高、宽、通道pixels[y][x][c]
CHW通道、高、宽pixels[c][y][x]

例如一张宽 640、高 480 的 RGB 图像,HWC 形状是 (480,640,3),CHW 是 (3,480,640)。重排布局不会改变颜色和像素位置,只改变数据的组织方式。

如果再增加“这一批有几张图片”的维度 N,就得到 NCHW。深度学习中,这样的多维数值数组称为张量。

HWC 和 CHW 互转一般采用三层嵌套 for 循环:

def hwc_to_chw(pixels):
    h = len(pixels)
    w = len(pixels[0])
    c = len(pixels[0][0])

    chw = []
    for ch in range(c):
        channel = []
        for y in range(h):
            row = []
            for x in range(w):
                row.append(pixels[y][x][ch])
            channel.append(row)
        chw.append(channel)

    return chw

卷积 把局部像素组合成特征

卷积核

卷积核是一小块权重矩阵。它在图像上滑动,每到一个位置,就把窗口内的数值与权重对应相乘,再求和,得到一个输出值。所有位置的输出组成一张“特征图”。

一个简单的卷积算法:

def conv2d_valid(image, kernel):
    out_h = len(image) - len(kernel) + 1
    out_w = len(image[0]) - len(kernel[0]) + 1

    kh = len(kernel)
    kw = len(kernel[0])

    result = []
    for y in range(out_h):
        row = []
        for x in range(out_w):
            total = 0
            for ky in range(kh):
                for kx in range(kw):
                    total += image[y + ky][x + kx] * kernel[ky][kx]
            row.append(total)
        result.append(row)

    return result
  • 为什么输出可以是负数?同一个卷积核在各个位置重复使用,有什么好处?

卷积输出是加权求和。因此可以为负,也可以超出 [-1,1],例如 10−1=9。同一个核重复使用称为权重共享:它减少需要学习的参数,让同一种局部模式出现在不同位置时,都能由同一套权重检测。它并不保证准确率一定增加。

激活函数

激活函数 ReLU,通过逐元素处理,负数变成 0,正数保持不变,数组形状不变。仅堆叠卷积这样的仿射运算,组合后仍是仿射运算;通过 ReLU 加入非线性,网络才能表达更复杂的关系。

ReLU(z)=max⁡(0,z){ReLU}(z) = \max(0, z)
def relu2d(values):
    return [[max(0, value) for value in column] for column in values]

RGB 卷积

标准卷积的一个输出通道会:

  1. 对 R、G、B 分别使用对应的核计算;
  2. 把三个结果相加;
  3. 再加一个偏置。

因此,一个输出通道对应的是 3 片核权重。如果需要 8 个输出通道,就学习 8 组这样的权重,而不是输出 24 个通道。

标准卷积的权重形状是:

(输出通道数, 输入通道数, 核高, 核宽)

每个输出通道带一个偏置时,参数量为:

输出通道数 × (输入通道数 × 核高 × 核宽 + 1)
© 2026 ChinoKoyuki