260612

好吧,在回家之前,在学校这边事可能会一直都比较多。

我虽然喜欢在学校里待着,但是仅限教学楼,宿舍里还是太差了。

嘛,尽力学吧,反正其实也不差这两天。

我们继续后续的步骤。

接下来我们要接入前向相机和图像桥接。

首先接入前向相机,在 src/kibot_one_sim/models/kibot_one_base/model.sdf 找到 lidar_joint 后,在它后面加上一个独立的 camera_link:

    <link name="camera_link">
      <pose>0.29 0 0.24 0 0 0</pose>
      <sensor name="front_camera" type="camera">
        <topic>/camera/image_raw</topic>
        <update_rate>15</update_rate>
        <always_on>true</always_on>
        <visualize>true</visualize>
        <gz_frame_id>camera_link</gz_frame_id>
        <camera>
          <horizontal_fov>1.047</horizontal_fov>
          <image>
            <width>320</width>
            <height>240</height>
            <format>R8G8B8</format>
          </image>
          <clip>
            <near>0.05</near>
            <far>8.0</far>
          </clip>
        </camera>
      </sensor>
    </link>

其他的不用多做解释,我们这里的相机设置了水平视场角为 1.047。

这个水平视场角的作用是,这个值越大,能看到的就越宽,但是远处的东西看起来就越小。

具体就是像这样的:

然后把它固定到我们的 base_link 上:

    <joint name="camera_joint" type="fixed">
      <parent>base_link</parent>
      <child>camera_link</child>
    </joint>

接着把我们的图像在 Gazebo 和 ROS 中进行桥接起来,在 src/kibot_one_sim/config/ros_gz_bridge.yaml 中补下面的内容:

- ros_topic_name: "/camera/image_raw"
  gz_topic_name: "/camera/image_raw"
  ros_type_name: "sensor_msgs/msg/Image"
  gz_type_name: "gz.msgs.Image"
  direction: "GZ_TO_ROS"

桥接和相机、传感器的部分完成了,我们接下来实现红色旗帜检测的核心代码。

我们先创建src/kibot_one_control/kibot_one_control/flag_detection_core.py,放入所需的结果类型:

from dataclasses import dataclass

@dataclass(frozen=True)
class ColorDetection:
    detected: bool
    center_x: float
    center_y: float
    pixel_count: int
    confidence: float

接下来我们接入对图像进行编码的函数:

def _channels_for_encoding(encoding: str) -> int:
    normalized = encoding.lower()
    if normalized in {"rgb8", "bgr8"}:
        return 3
    if normalized in {"rgba8", "bgra8"}:
        return 4
    return 0


def _rgb_components(pixel: bytes, encoding: str) -> Tuple[int, int, int]:
    normalized = encoding.lower()
    if normalized in {"rgb8", "rgba8"}:
        return pixel[0], pixel[1], pixel[2]
    return pixel[2], pixel[1], pixel[0]


def _empty_detection() -> ColorDetection:
    return ColorDetection(False, 0.0, 0.0, 0, 0.0)

然后实现我们的检测函数:

def detect_red_flag_pixels(
    image_data: bytes,
    width: int,
    height: int,
    encoding: str,
    min_red: int = 120,
    red_margin: int = 45,
    min_pixel_count: int = 80,
) -> ColorDetection:
    channels = _channels_for_encoding(encoding=encoding)
    if width <= 0 or height <= 0 or channels == 0:
        return _empty_detection()
  
    expected_size = width * height * channels
    if len(image_data) < expected_size:
        return _empty_detection()
  
    red_count = 0
    x_sum = 0
    y_sum = 0
    for index in range(0, expected_size, channels):
        pixel = image_data[index:index + channels]
        red, greeen, blue = _rgb_components(pixel=pixel, encoding=encoding)
        if red >= min_red and red >= greeen + red_margin and red >= blue + red_margin:
            pixel_index = index // channels
            x_sum += pixel_index % width
            y_sum += pixel_index // width
            red_count += 1

    if red_count < min_pixel_count:
        return ColorDetection(False, 0.0, 0.0, red_count, red_count / float(width * height))
  
    return ColorDetection(
        True,
        center_x=x_sum / red_count,
        center_y=y_sum / red_count,
        pixel_count=red_count,
        confidence=min(1.0, red_count / float(width * height))
    )

我来解释一下这个函数。

我们先计算这个图片的通道数,如果编码不受支持或者图片字节长度小于预期的字节长度,都认为是非法的图片而直接返回检测失败。

然后以通道数为步长遍历字节流,每次取出一个像素的若干字节,我们就可以得到每个像素每个通道的值。

然后计算每个像素的 RGB 值,如果红色通道值明显大于蓝色和绿色通道、且大于预期的红色最小值,则偏红色像素 +1,且累计偏红色像素的 x, y 坐标。

最后通过计算偏红色像素的总量,如果小于最小的预期像素数量,则也认为是噪声而直接返回检测失败,否则返回检测成功,并返回检测到的红色像素质心、红色像素的总量以及置信度。

然后我们来增加一个单元测试吧 src/kibot_one_control/test/test_flag_detection_core.py:


from kibot_one_control.flag_detection_core import detect_red_flag_pixels


def _rgb_image(width: int, height: int, fill: tuple[int, int, int]) -> bytearray:
    image = bytearray()
    for _ in range(width * height):
        image.extend(fill)
    return image

def test_detect_red_flag_pixels_reports_center_and_count() -> None:
    width = 10
    height = 8
    image = _rgb_image(width, height, (20, 20, 20))
    for y in range(2, 6):
        for x in range(3, 7):
            offset = (y * width + x) * 3
            image[offset:offset + 3] = bytes((220, 20, 20))

    detection = detect_red_flag_pixels(bytes(image), width, height, "rgb8", min_pixel_count=4)

    assert detection.detected is True
    assert detection.pixel_count == 16
    assert detection.center_x == 4.5
    assert detection.center_y == 3.5

def test_detect_red_flag_pixels_rejects_small_red_noise() -> None:
    image = _rgb_image(8, 8, (10, 10, 10))
    image[0:3] = bytes((230, 10, 10))

    detection = detect_red_flag_pixels(bytes(image), 8, 8, "rgb8", min_pixel_count=4)

    assert detection.detected is False
    assert detection.pixel_count == 1

def test_detect_red_flag_pixels_supports_bgr8() -> None:
    image = bytearray()
    for _ in range(12):
        image.extend((10, 20, 210))

    detection = detect_red_flag_pixels(bytes(image), 4, 3, "bgr8", min_pixel_count=3)

    assert detection.detected is True
    assert detection.center_x == 1.5
    assert detection.center_y == 1.0
(.venv) jese--ki@KiBall:~/Projects/dev/KiBots/KiBotTwo$ python -m pytest -q src/kibot_one_control/test/test_flag_detection_core.py
...                                                                                                                                                                                   [100%]
3 passed in 0.01s

全部通过,符合预期。