进行前思考
让 MaixCAM2 使用板载摄像头,在本地运行自训练的 YOLO 模型,识别不同种类的螺丝、螺母和垫片,估计它们在工作平面上的位置与朝向,再通过 UART 把结果发送给 STM32。
项目应最终实现多类别检测、平面位置与姿态估计、实时通信与控制以及拓展性的科研实验与性能分析。
基础知识构建
IoU 衡量两个框的重叠程度
Intersection over Union,交并比,是目标检测中衡量预测框与真实框重叠程度的核心指标。
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)。在进行标注前应转换为归一化的中心与宽高:
由公式可以很容易实现互相转换:
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 减少重复框:
- 删除分数低于
conf_th的候选。 - 选出剩余候选中分数最高的框,保留它。
- 删除与它同类别且 IoU 超过
iou_th的其他候选。 - 对剩余候选重复上述过程。
需要特别注意的是两个阈值的区别:
| 参数增大 | 直接影响 |
|---|---|
conf_th | 分数筛选更严格,进入 NMS 的候选更少 |
iou_th | 需要重叠更多才删除,NMS 更宽松 |
Python 实现思路:
- 保留
score >= conf_th的候选,按分数降序排列。 - 取出当前最高分框,加入结果。
- 筛选剩余候选:仅删除与当前框同类且 IoU > iou_th 的框。
- 重复,直到没有候选。
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 keptRGB 图像处理
彩色图像可以看作一个数值数组。常见 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 加入非线性,网络才能表达更复杂的关系。
def relu2d(values):
return [[max(0, value) for value in column] for column in values]
RGB 卷积
标准卷积的一个输出通道会:
- 对 R、G、B 分别使用对应的核计算;
- 把三个结果相加;
- 再加一个偏置。
因此,一个输出通道对应的是 3 片核权重。如果需要 8 个输出通道,就学习 8 组这样的权重,而不是输出 24 个通道。
标准卷积的权重形状是:
(输出通道数, 输入通道数, 核高, 核宽)
每个输出通道带一个偏置时,参数量为:
输出通道数 × (输入通道数 × 核高 × 核宽 + 1)