#识别装甲板

整体思路如下:

  • 图像预处理:
    • 选取颜色通道阈值区间
    • 形态学操作
    • 边缘检测
  • 筛选矩形:
    • 查找合适灯条,注意两两灯条之间的距离,是否垂直,角度问题(因为车在转的时候不同侧的灯条容易被错误识别为装甲板)
    • 筛选除去部分矩形(有些灯条组合需要删除)
  • 识别装甲板:
    • 绘图(远距离,近距离分别处理)
  • 判断是否是装甲板:
    • 遍历所有矩形,两两组合判断

##具体实现方法

方法一:hsv色彩分离

  1. 输入视频帧,读取每一帧图像
  2. 颜色空间转换 BGR → HSV
  3. 颜色阈值分割
    • 设定蓝色阈值范围(lower_blue,upper_blue)
    • 使用cv2.inRange()提取蓝色区域
  4. 形态学处理
    • 结构元素(3x3矩形核)
    • 开运算:去噪点
    • 闭运算:填充小孔
  5. 轮廓提取
    • cv2.findContours()检测外轮廓
  6. 灯条筛选
    • 计算轮廓外接矩形 (x, y, w, h)
    • 过滤:宽高比例 h/w > 3
    • 过滤:轮廓面积 > 50
  7. 灯条配对
    • 遍历所有灯条两两组合
    • 过滤条件:
      • 高度差 < 20% 最大高度
      • 灯条中心纵坐标差 < 最大高度50%
      • 中心横向距离在 0.5~5 倍最大高度范围内
  8. 装甲板绘制
    • 根据配对灯条计算矩形框
    • 绘制红色矩形标记
  9. 结果显示与退出
    • cv2.imshow展示
    • 按q退出循环

方法二:通道相减法(但是未使用)

  • 第一步:图像的预处理,从RGB的角度考虑去识别蓝色灯条,也就是说如果蓝色通道的分量要比 其他两个通道都要大,就能说明物体足够蓝,因此一般选择蓝色通道减去红色通道,经过此番操作后, 会发现原图中的蓝色区域仍然有着较高的亮度。
  • 第二步:二值化并取交集,以根据不同的阈值得到不同效果的图片进行不断地调整最后得到一个 最优的阈值
  • 第三步:形态学操作,先用滤波过滤图像,然后一般可以使用闭操作,先膨胀后腐蚀,来增强轮廓的表现。
  • 第四步:寻找轮廓并筛选拟合出的对象

利弊分析与整合(detect.py)

在实际操作中,我发现在近距离使用hsv色彩空间的效果比较好,可能是因为近距离的灯条比较明显,远距离下 灯条的亮度比较暗,使用bgr色彩空间的效果比较好,虽然牺牲了一部分的识别准确率,但是提高了识别的准确率 是真的,然后我分了两个不同的部分进行处理,也就是远距离和近距离采取的方法不同,不同之处见下:

  • 近距离检测(detect_near_distance):
    • 转为 HSV,设定蓝色阈值提取 mask
    • 提取轮廓,筛选为灯条
    • 成对配对灯条,构成装甲板候选对
  • 远距离检测(detect_far_distance):
    • 提取蓝色通道,并二值化
    • 模糊、膨胀、开闭运算增强连通区域
    • 拟合椭圆
    • 两两椭圆进行配对,限制角度、长度、距离、斜率等条件
    • 对配对结果使用NMS进行过滤去重

整体代码:

import cv2
import numpy as np

#创建灯条类对象
class LightDescriptor:
    def __init__(self, rotated_rect):
        self.width = rotated_rect[1][0]
        self.length = rotated_rect[1][1]
        self.center = rotated_rect[0]
        self.angle = rotated_rect[2]
        self.area = self.width * self.length

def distance(p1, p2):
    return np.linalg.norm(np.array(p1) - np.array(p2))

#近距离情况下
def detect_near_distance(frame, lower_blue, upper_blue):
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, lower_blue, upper_blue)
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

    #开运算,轻松get装甲板超绝下颌线
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    light_bars = []
    for cnt in contours:#之前提取到的蓝色灯条矩形框
        x, y, w, h = cv2.boundingRect(cnt) #矩形框的左上角坐标和宽高
        if w == 0 or h == 0:
            continue
        ratio = h / w #高宽比
        area = cv2.contourArea(cnt)
        if ratio > 3 and area > 50: #排除错误框的一种办法,可以提高识别的准确率
            light_bars.append((x, y, w, h))  #初步得到可能的装甲板集合

    paired_armor = [] #在初步筛选出可能的装甲板之后再次筛选并进行存放
    for i in range(len(light_bars)):
        for j in range(i + 1, len(light_bars)): #灯条匹配
            bar1 = light_bars[i]
            bar2 = light_bars[j]

            center_y1 = bar1[1] + bar1[3] / 2#垂直方向是否对齐判断
            center_y2 = bar2[1] + bar2[3] / 2
            center_diff_y = abs(center_y1 - center_y2)

            height_diff = abs(bar1[3] - bar2[3]) #灯条高度差
            max_height = max(bar1[3], bar2[3])
            center_y_thresh = max_height * 0.5

            if height_diff / max_height > 0.2: #如果灯条高度差过大就pass
                continue
            if center_diff_y > center_y_thresh: #如果垂直方向不对其就pass
                continue

            center_x1 = bar1[0] + bar1[2] / 2 #水平间距
            center_x2 = bar2[0] + bar2[2] / 2
            dist = abs(center_x1 - center_x2)
            if dist < max_height * 0.5 or dist > max_height * 5:
                continue

            # 构造轮廓用于拟合角度
            rect1_cnt = np.array([[bar1[0], bar1[1]],
                                  [bar1[0] + bar1[2], bar1[1]],
                                  [bar1[0] + bar1[2], bar1[1] + bar1[3]],
                                  [bar1[0], bar1[1] + bar1[3]]], dtype=np.int32)
            rect2_cnt = np.array([[bar2[0], bar2[1]],
                                  [bar2[0] + bar2[2], bar2[1]],
                                  [bar2[0] + bar2[2], bar2[1] + bar2[3]],
                                  [bar2[0], bar2[1] + bar2[3]]], dtype=np.int32)

            rect1 = cv2.minAreaRect(rect1_cnt)#把灯条的边界框转成四点轮廓,就能得到角度查
            rect2 = cv2.minAreaRect(rect2_cnt)

            angle1 = rect1[2]
            angle2 = rect2[2]

            if angle1 < -45:
                angle1 += 90
            if angle2 < -45:
                angle2 += 90

            angle_diff = abs(angle1 - angle2)
            if angle_diff > 15:
                continue

            paired_armor.append((bar1, bar2))
    return paired_armor

#远距离情况:使用椭圆拟合的办法
def detect_far_distance(frame):
    b, g, r = cv2.split(frame)
    _, binary = cv2.threshold(b, 220, 255, cv2.THRESH_BINARY)

    #开操作:去除闭点
    kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    #比操作:填补空洞,修复灯条的断裂情况
    binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_open)
    binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel_close)

    blur = cv2.GaussianBlur(binary, (5, 5), 0) #高斯模糊去噪
    dilated = cv2.dilate(blur, kernel_close) #膨胀增强

    contours, _ = cv2.findContours(dilated, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

    light_infos = []
    h_img, w_img = frame.shape[:2]

    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area < 40 or len(cnt) < 5:
            continue

        ellipse = cv2.fitEllipse(cnt)
        w, h = ellipse[1]
        if w == 0 or h == 0:
            continue
        ratio = max(w, h) / min(w, h)
        if ratio > 2.5:
            continue
        cx, cy = ellipse[0]
        if not (w_img * 0.1 < cx < w_img * 0.9 and h_img * 0.1 < cy < h_img * 0.9):
            continue

        light_infos.append(LightDescriptor(ellipse))

    paired_armor = [] #储存合格灯条,这轮筛选和近距离的思路差不多,也是判断面积,宽高比,水平方向,垂直方向是否符合一定的比例,然后提取出的装甲板框的角度是否正常,
    for i in range(len(light_infos)):
        for j in range(i + 1, len(light_infos)):
            left = light_infos[i]
            right = light_infos[j]
            if left.center[0] >= right.center[0]:
                continue

            angle_diff = abs(left.angle - right.angle)
            len_gap_ratio = abs(left.length - right.length) / max(left.length, right.length)
            dis = distance(left.center, right.center)
            mean_len = (left.length + right.length) / 2
            y_gap_ratio = abs(left.center[1] - right.center[1]) / mean_len
            x_gap_ratio = abs(left.center[0] - right.center[0]) / mean_len
            dis_ratio = dis / mean_len
            delta_x = abs(left.center[0] - right.center[0])
            delta_y = abs(left.center[1] - right.center[1])
            slope = delta_y / (delta_x + 1e-5)

            if angle_diff > 10 or len_gap_ratio > 0.8 or y_gap_ratio > 1.2 \
               or x_gap_ratio > 2.0 or x_gap_ratio < 0.6 or dis_ratio > 2.8 or dis_ratio < 0.7 \
               or slope > 1.0:
                continue

            paired_armor.append((left, right))
    return paired_armor

def rotated_iou(rect1, rect2):#计算两个旋转矩形之间的近似交并比,用于去除重复框
    box1 = cv2.boxPoints(rect1)
    box2 = cv2.boxPoints(rect2)
    box1 = np.intp(box1)
    box2 = np.intp(box2)

    x1, y1, w1, h1 = cv2.boundingRect(box1)
    x2, y2, w2, h2 = cv2.boundingRect(box2)

    inter_x1 = max(x1, x2)
    inter_y1 = max(y1, y2)
    inter_x2 = min(x1 + w1, x2 + w2)
    inter_y2 = min(y1 + h1, y2 + h2)

    inter_w = max(0, inter_x2 - inter_x1)
    inter_h = max(0, inter_y2 - inter_y1)
    inter_area = inter_w * inter_h

    area1 = w1 * h1
    area2 = w2 * h2

    return inter_area / (area1 + area2 - inter_area + 1e-5)

def nms_rotated_boxes(light_pairs, iou_thresh=0.3):
    boxes = []
    for left, right in light_pairs:
        center = ((left.center[0] + right.center[0]) / 2,
                  (left.center[1] + right.center[1]) / 2)
        dis = distance(left.center, right.center)
        mean_len = (left.length + right.length) / 2
        angle = (left.angle + right.angle) / 2
        rect = (center, (dis, mean_len), angle)
        boxes.append((rect, cv2.boxPoints(rect)))

    areas = [r[0][1][0] * r[0][1][1] for r in boxes]
    indices = np.argsort(areas)[::-1]
    keep = []

    while len(indices) > 0:
        current = indices[0]
        keep.append(current)
        remove_indices = [0]
        for i in range(1, len(indices)):
            iou_val = rotated_iou(boxes[current][0], boxes[indices[i]][0])
            if iou_val > iou_thresh:
                remove_indices.append(i)
        indices = np.delete(indices, remove_indices)
    return [light_pairs[i] for i in keep]

def draw_near_armor(frame, paired_armor):
    for bar1, bar2 in paired_armor:
        x1, y1, w1, h1 = bar1
        x2, y2, w2, h2 = bar2
        top_left = (int(min(x1, x2)), int(min(y1, y2)))
        bottom_right = (int(max(x1 + w1, x2 + w2)), int(max(y1 + h1, y2 + h2)))
        cv2.rectangle(frame, top_left, bottom_right, (0, 0, 255), 2)

def draw_far_armor(frame, paired_armor):
    for left, right in paired_armor:
        center = ((left.center[0] + right.center[0]) / 2,
                  (left.center[1] + right.center[1]) / 2)
        dis = distance(left.center, right.center)
        mean_len = (left.length + right.length) / 2
        rect = (center, (dis, mean_len), (left.angle + right.angle) / 2)
        box = cv2.boxPoints(rect)
        box = np.intp(box)
        cv2.drawContours(frame, [box], 0, (0, 0, 255), 2)

def main():
    lower_blue = np.array([80, 120, 100])
    upper_blue = np.array([140, 255, 255])
    video = cv2.VideoCapture(r'D:\Users\39834\PycharmProjects\rm\task_1\zhuangjiaban.mp4')

    while True:
        ret, frame = video.read()
        if not ret:
            break

        near_results = detect_near_distance(frame, lower_blue, upper_blue)
        far_results = detect_far_distance(frame)
        far_results = nms_rotated_boxes(far_results) #使用nms去除重复框

        draw_near_armor(frame, near_results)
        draw_far_armor(frame, far_results)

        cv2.imshow('armor_detection_fusion', frame)
        if cv2.waitKey(20) & 0xFF == ord('q'):
            break

    video.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

后期增益:

可以增加用匈牙利算法+卡尔曼滤波进行装甲板的跟踪

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐