K230 CanMV 的 sensor.snapshot 只有约33FPS出帧

Viewed 142

问题描述


新手求教
在使用K230做简单视觉算法时发现,无论视频流帧速率是多少,经sensor.snapshot都只有约33FPS出帧。请问应该如何提高帧数?需要换镜像吗?
谢谢

代码如下


import time

import image

from media.sensor import (
    Sensor,
    CAM_CHN_ID_0,
    CAM_CHN_ID_1,
)
from media.display import Display
from media.media import MediaManager

import config
from vision.debug_overlay import DebugOverlay
from vision.interrupts import reraise_if_stop


_PIXFORMAT_NAME_TO_ENUM = {
    "YUV420SP": Sensor.YUV420SP,
    "RGB888": Sensor.RGB888,
    "RGB565": Sensor.RGB565,
    "GRAYSCALE": Sensor.GRAYSCALE,
}

_CHN_ID = {0: CAM_CHN_ID_0, 1: CAM_CHN_ID_1}


class Camera:
    def __init__(self, cfg=None):
        self.cfg = cfg if cfg is not None else config

        self._sensor = None
        self._osd = None
        self._media_inited = False
        self._display_inited = False
        self._sensor_running = False

        self._algo_chn = _CHN_ID[self.cfg.ALGO_CHN]
        self._display_chn = _CHN_ID[self.cfg.DISPLAY_CHN]

        # 帧统计
        self._frame_count = 0
        self._debug_overlay = DebugOverlay(self.cfg)
        # sensor.snapshot() 在 K230 上偶发 RuntimeError(如 failed(3))时按丢帧处理。
        self._snapshot_error_log_remaining = 5

    # ------------------------------------------------------------------ #
    # 诊断
    # ------------------------------------------------------------------ #
    def log_configured_modes(self):
        """打印本次请求的 sensor mode 与各通道实际输出尺寸。

        通道编号取自 config(``DISPLAY_CHN``/``ALGO_CHN``),不是 hardcode 的
        "CHN0/CHN1"——swap 时也不会失真。
        """
        if self._sensor is None:
            return
        print(
            "[camera] request: CSI%d sensor=%dx%d@%d, "
            "display→CHN%d(%dx%d)=%s, algo→CHN%d=%s"
            % (
                self.cfg.SENSOR_ID,
                self.cfg.SENSOR_REQ_WIDTH,
                self.cfg.SENSOR_REQ_HEIGHT,
                self.cfg.SENSOR_NOMINAL_FPS,
                self.cfg.DISPLAY_CHN,
                self.cfg.SENSOR_DISPLAY_W,
                self.cfg.SENSOR_DISPLAY_H,
                self.cfg.DISPLAY_PIXFORMAT,
                self.cfg.ALGO_CHN,
                self.cfg.ALGO_PIXFORMAT,
            )
        )
        for role, chn in (("display", self._display_chn),
                          ("algo   ", self._algo_chn)):
            try:
                print(
                    "[camera] CHN%d (%s): %dx%d"
                    % (
                        self.cfg.DISPLAY_CHN if role.strip() == "display"
                        else self.cfg.ALGO_CHN,
                        role,
                        self._sensor.width(chn=chn),
                        self._sensor.height(chn=chn),
                    )
                )
            except Exception as e:
                print("[camera] query %s failed: %s" % (role, e))

    # ------------------------------------------------------------------ #
    # 生命周期
    # ------------------------------------------------------------------ #
    def init(self, pre_media_hook=None):
        """构造 sensor、绑定 CHN0 到 LCD、配置 CHN1 算法通道、初始化 Display + Media。

        CHN0 显示通道输出不得超过传感器原生分辨率(SENSOR_DISPLAY_W/H),
        与 LCD 物理尺寸(DISPLAY_WIDTH/HEIGHT)解耦,由 config 单独控制。

        :param pre_media_hook: 可选回调,在 sensor0 配置完成后、Display.init /
            MediaManager.init 之前调用。用于双摄场景下在此时机初始化 sensor1
            (K230 SDK 要求所有 sensor 在 MediaManager.init 之前配置完毕)。
        """
        self._sensor = Sensor(
            id=self.cfg.SENSOR_ID,
            width=self.cfg.SENSOR_REQ_WIDTH,
            height=self.cfg.SENSOR_REQ_HEIGHT,
            fps=self.cfg.SENSOR_NOMINAL_FPS,
        )
        self._sensor.reset()
        self._sensor.set_hmirror(False)
        self._sensor.set_vflip(False)

        # ---- CHN0:显示通道 ----
        # 输出尺寸上限为传感器原生分辨率(SENSOR_DISPLAY_W/H),不能超过
        # SENSOR_REQ_WIDTH/HEIGHT;LCD 物理尺寸(DISPLAY_WIDTH/HEIGHT)
        # 用于 Display.init 和 OSD,两者独立。
        lcd_src = getattr(self.cfg, "LCD_VIDEO_SOURCE", "csi0")
        if lcd_src == "csi0":
            self._sensor.set_framesize(
                chn=self._display_chn,
                width=self.cfg.SENSOR_DISPLAY_W,
                height=self.cfg.SENSOR_DISPLAY_H,
            )
            self._sensor.set_pixformat(
                _PIXFORMAT_NAME_TO_ENUM[self.cfg.DISPLAY_PIXFORMAT],
                chn=self._display_chn,
            )
            bind_info = self._sensor.bind_info(chn=self._display_chn)
            Display.bind_layer(**bind_info, layer=Display.LAYER_VIDEO1)
        else:
            self._sensor.set_framesize(
                chn=self._display_chn,
                width=self.cfg.SENSOR_DISPLAY_W,
                height=self.cfg.SENSOR_DISPLAY_H,
            )
            self._sensor.set_pixformat(
                _PIXFORMAT_NAME_TO_ENUM[self.cfg.DISPLAY_PIXFORMAT],
                chn=self._display_chn,
            )
            print(
                "[camera] LCD_VIDEO_SOURCE=%s, CSI0 CHN0 configured but not "
                "bound (no display drain)" % lcd_src
            )

        # ---- CHN1:算法通道 ----
        self._sensor.set_framesize(
            chn=self._algo_chn,
            width=self.cfg.ALGO_WIDTH,
            height=self.cfg.ALGO_HEIGHT,
        )
        self._sensor.set_pixformat(
            _PIXFORMAT_NAME_TO_ENUM[self.cfg.ALGO_PIXFORMAT],
            chn=self._algo_chn,
        )

        # 加深输出 buffer 队列(变更 C),默认 K230D 为 3,增至 6 消除 p95 长尾
        trk_buf = getattr(self.cfg, "TRK_OUTPUT_BUFFER_NUM", 0)
        if trk_buf > 0:
            chn_idx = self.cfg.ALGO_CHN
            self._sensor._chn_attr[chn_idx].buffer_num = trk_buf

        # ---- 双摄挂接点 ----
        # K230 SDK 要求所有 sensor 在 Display.init / MediaManager.init 之前完成
        # 构造与通道配置。此 hook 供 AimCamera 等第二路 sensor 在此时机初始化。
        if pre_media_hook is not None:
            pre_media_hook()

        # ---- Display ----
        display_type = getattr(Display, self.cfg.DISPLAY_TYPE)
        Display.init(
            display_type,
            width=self.cfg.DISPLAY_WIDTH,
            height=self.cfg.DISPLAY_HEIGHT,
            to_ide=self.cfg.DISPLAY_TO_IDE,
        )
        self._display_inited = True

        MediaManager.init()
        self._media_inited = True

        # ---- OSD 缓冲 ----
        if self.cfg.DEBUG_DISPLAY:
            self._osd = image.Image(
                self.cfg.DISPLAY_WIDTH,
                self.cfg.DISPLAY_HEIGHT,
                image.ARGB8888,
            )
            self._osd.clear()
        self._debug_overlay.set_osd(self._osd)

        # ---- 调试 overlay:计算坐标(不再预分配 blit/mask 中间图)---- #
        self._debug_overlay.setup()

        return self

    def _apply_focus(self):
        """在 sensor.run() 之后尝试软件调焦。
        """
        try:
            caps = self._sensor.focus_caps()
        except Exception as e:
            print("[camera] focus_caps() error (non-fatal):", e)
            return

        # caps = (isSupport, minPos, maxPos)
        if not caps or not caps[0]:
            print(
                "[camera] focus: VCM not supported by firmware "
                "(caps=%s) — adjust lens barrel physically" % str(caps)
            )
            return

        min_pos, max_pos = int(caps[1]), int(caps[2])
        if self.cfg.SENSOR_AUTOFOCUS:
            try:
                self._sensor.auto_focus(True)
                print("[camera] focus: auto")
            except Exception as e:
                print("[camera] auto_focus failed (non-fatal):", e)
        else:
            pos = max(min_pos, min(max_pos, self.cfg.SENSOR_FOCUS_POS))
            try:
                self._sensor.focus_pos(pos)
                print(
                    "[camera] focus: manual pos=%d (range %d~%d)"
                    % (pos, min_pos, max_pos)
                )
            except Exception as e:
                print("[camera] focus_pos failed (non-fatal):", e)

    def start(self):
        """启动 sensor 数据流。MediaManager.init 之后调用(参见 Sensor 文档)。

        焦点配置在 sensor.run() **之后**应用:sensor fd 由内核在 run() 启动
        数据流时才真正打开,MediaManager.init() 只建立媒体图,fd 尚未就绪。
        """
        if self._sensor is None:
            raise RuntimeError("Camera.init() must be called before start()")
        self._sensor.run()
        self._sensor_running = True
        self._apply_focus()
        self._debug_overlay.reset_fps_window(time.ticks_ms())
        return self

    def stop(self):
        """严格逆序释放:sensor.stop → Display.deinit → MediaManager.deinit。

        每个步骤都被保护,确保即使前一步抛异常也不会导致后续资源泄漏。
        """
        try:
            if self._sensor_running and isinstance(self._sensor, Sensor):
                self._sensor.stop()
        except Exception as e:
            print("[camera] sensor.stop failed:", e)
        finally:
            self._sensor_running = False

        if self._display_inited:
            try:
                Display.deinit()
            except Exception as e:
                print("[camera] Display.deinit failed:", e)
            finally:
                self._display_inited = False

        # 让底层有时间归还 buffer
        time.sleep_ms(50)

        if self._media_inited:
            try:
                MediaManager.deinit()
            except Exception as e:
                print("[camera] MediaManager.deinit failed:", e)
            finally:
                self._media_inited = False

    # ------------------------------------------------------------------ #
    # 帧读取
    # ------------------------------------------------------------------ #
    def read_algo_frame(self, timeout=None):
        """从 CHN1 拉一帧算法输入。返回 ``image.Image`` 或 ``None``。

        :param timeout: ``None`` 时使用 ``config.SNAPSHOT_TIMEOUT_MS``。
        """
        if timeout is None:
            timeout = self.cfg.SNAPSHOT_TIMEOUT_MS
        try:
            img = self._sensor.snapshot(chn=self._algo_chn, timeout=timeout)
        except RuntimeError as e:
            reraise_if_stop(e)
            if self._snapshot_error_log_remaining > 0:
                self._snapshot_error_log_remaining -= 1
                print("[camera] snapshot RuntimeError:", e)
            return None
        except Exception as e:
            reraise_if_stop(e)
            if self._snapshot_error_log_remaining > 0:
                self._snapshot_error_log_remaining -= 1
                print("[camera] snapshot failed:", e)
            return None
        if img is not None:
            self._frame_count += 1
        return img

    def read_algo_frame_fast(self, timeout=None):
        """变更 D:``dump_frame=True`` 跳过 sensor.py 格式检测,手动构造 Image。

        预设 GRAYSCALE 格式(``ALGO_PIXFORMAT="GRAYSCALE"``,ISP 输出
        YUV420 → Image 仅取 Y 平面)。若 py_video_frame_info 属性访问
        失败则自动 fallback 到 :meth:`read_algo_frame`。
        """
        if self._sensor is None:
            return None
        if timeout is None:
            timeout = self.cfg.SNAPSHOT_TIMEOUT_MS
        try:
            vf = self._sensor.snapshot(
                chn=self._algo_chn, timeout=timeout, dump_frame=True,
            )
        except RuntimeError as e:
            reraise_if_stop(e)
            if self._snapshot_error_log_remaining > 0:
                self._snapshot_error_log_remaining -= 1
                print("[camera] fast snapshot RuntimeError:", e)
            return None
        except Exception as e:
            reraise_if_stop(e)
            if self._snapshot_error_log_remaining > 0:
                self._snapshot_error_log_remaining -= 1
                print("[camera] fast snapshot failed:", e)
            return None
        if vf is None:
            return None
        try:
            img = image.Image(
                self.cfg.ALGO_WIDTH, self.cfg.ALGO_HEIGHT,
                image.GRAYSCALE,
                alloc=image.ALLOC_VB,
                phyaddr=vf.v_frame.phys_addr[0],
                virtaddr=vf.v_frame.virt_addr[0],
                poolid=vf.pool_id,
            )
        except Exception as e:
            if self._snapshot_error_log_remaining > 0:
                self._snapshot_error_log_remaining -= 1
                print("[camera] fast Image wrap failed, fallback:", e)
            return self.read_algo_frame(timeout)
        if img is not None:
            self._frame_count += 1
        return img

    def release_algo_frame(self):
        """提前释放 CHN1 的 VB buffer(变更 B)。

        受 ``config.TRK_EARLY_VB_RELEASE`` 开关守护(默认 False):
        12 fps 回归排查显示此操作可能在线程化场景下引入 VB 状态不一致。
        """
        if not getattr(self.cfg, "TRK_EARLY_VB_RELEASE", False):
            return
        if self._sensor is None:
            return
        try:
            chn_idx = self.cfg.ALGO_CHN
            img_ref = self._sensor._imgs[chn_idx]
            if img_ref is not None:
                Sensor._release_image(img_ref)
                self._sensor._imgs[chn_idx] = None
        except Exception:
            pass

    def notify_track_solved(self):
        """循迹解算有效(如 ``detection.n_valid > 0``)时由主循环调用,计入 track_fps。"""
        self._debug_overlay.on_track_frame()

    def notify_aim_processed(self):
        """CSI1 取帧成功且 find_blobs 已跑完(不要求检出)时调用,计入 aim_proc_fps。"""
        self._debug_overlay.on_aim_proc_frame()

    def notify_aim_solved(self):
        """瞄准解算有效(检出至少 1 个目标)时调用,计入 aim_hit_fps。"""
        self._debug_overlay.on_aim_hit_frame()

    # ------------------------------------------------------------------ #
    # FPS 统计(循迹 CSI0 / 瞄准 CSI1 独立)
    # ------------------------------------------------------------------ #
    def track_fps(self):
        """CSI0 循迹每秒解算有效帧数(Hz),非裸 snapshot 速率。"""
        return self._debug_overlay.track_fps()

    def aim_hit_fps(self):
        """CSI1 每秒检出有效帧数(Hz)。"""
        return self._debug_overlay.aim_hit_fps()

    def aim_proc_fps(self):
        """CSI1 每秒解算跑通帧数(Hz);用于判断节流/取帧 vs 算力瓶颈。"""
        return self._debug_overlay.aim_proc_fps()

    def aim_fps(self):
        """兼容别名:等同 :meth:`aim_hit_fps`。"""
        return self._debug_overlay.aim_fps()

    def track_period_ms(self):
        """循迹平均帧周期(ms)= 1000 / track_fps。"""
        return self._debug_overlay.track_period_ms()

    def aim_hit_period_ms(self):
        return self._debug_overlay.aim_hit_period_ms()

    def aim_proc_period_ms(self):
        return self._debug_overlay.aim_proc_period_ms()

    def aim_period_ms(self):
        return self._debug_overlay.aim_period_ms()

    def frame_count(self):
        return self._frame_count

    # ------------------------------------------------------------------ #
    # 启动期诊断:raw snapshot 计时
    # ------------------------------------------------------------------ #
    def probe_snapshot_timing(self, n_frames=60):
        """连续阻塞拉 ``n_frames`` 帧,统计每次 snapshot 的微秒耗时分布。

        用于把"snapshot 自身阻塞时间"和"主循环其他开销(exitpoint / Python /
        IDE 通讯)"分开诊断:

        探针不计入 ``frame_count`` / ``track_fps`` 统计;执行完毕主循环再开始。
        """
        if self._sensor is None or n_frames <= 0:
            return
        samples = []
        # 先取 1 帧丢弃,避免首帧冷启动偏差污染统计。
        try:
            self._sensor.snapshot(chn=self._algo_chn,
                                  timeout=self.cfg.SNAPSHOT_TIMEOUT_MS)
        except Exception as e:
            reraise_if_stop(e)
            pass
        for _ in range(n_frames):
            t0 = time.ticks_us()
            img = self._sensor.snapshot(
                chn=self._algo_chn,
                timeout=self.cfg.SNAPSHOT_TIMEOUT_MS,
            )
            dt = time.ticks_diff(time.ticks_us(), t0)
            if img is not None:
                samples.append(dt)
            img = None
        if not samples:
            print("[camera.probe] no samples collected")
            return
        samples.sort()
        n = len(samples)
        avg = sum(samples) / n
        p50 = samples[n // 2]
        p95 = samples[min(n - 1, int(n * 0.95))]
        print(
            "[camera.probe] snapshot x%d  min=%dus  p50=%dus  avg=%.0fus  "
            "p95=%dus  max=%dus  → fps_eq=%.1f"
            % (n, samples[0], p50, avg, p95, samples[-1], 1e6 / avg)
        )

    def maybe_update_fps(self, now_ms=None):
        """1 秒内累计的算法帧数换算为 FPS。每 ``OSD_REFRESH_INTERVAL_MS`` 触发一次。"""
        return self._debug_overlay.maybe_update_fps(now_ms)

    def maybe_update_binary(self, now_ms=None):
        """二值 overlay 是否到了刷新时刻(独立于 FPS 文字行)。

        ``OSD_BINARY_REFRESH_MS=0`` 时永远返回 True(每帧刷新);正数时按
        该 ms 间隔节流。返回 True 表示主循环应当调用 :meth:`render_overlay`
        把最新 ``detection`` 画上 OSD。
        """
        return self._debug_overlay.maybe_update_binary(now_ms)

    def maybe_update_path(self, now_ms=None):
        """路径标记是否到了刷新时刻(OSD_PATH_REFRESH_MS 控制)。

        0 = 每帧;正数 = 节流到指定 ms;负数 = 不独立触发(跟随 1Hz 文字行)。
        """
        return self._debug_overlay.maybe_update_path(now_ms)

    def maybe_update_aim(self, now_ms=None):
        """瞄准标记是否到了刷新时刻(OSD_AIM_REFRESH_MS 控制)。

        0 = 每帧;正数 = 节流到指定 ms;负数 = 不独立触发(跟随 1Hz 文字行)。
        """
        return self._debug_overlay.maybe_update_aim(now_ms)

    def binary_overlay_enabled(self):
        """返回主画面 ROI 红色二值 overlay 当前运行期状态。"""
        return self._debug_overlay.binary_overlay_enabled()

    def set_binary_overlay_enabled(self, enabled):
        """运行期切换主画面 ROI 红色二值 overlay。"""
        return self._debug_overlay.set_binary_overlay_enabled(enabled)

    # ------------------------------------------------------------------ #
    # 调试叠加
    # ------------------------------------------------------------------ #
    def render_overlay(self, lines=None, detection=None, path=None,
                       aim_targets=None):
        """重绘 OSD:3 段 ROI + 总 ROI 外框 + 若干行文本 + (可选) 检测 / 路径 / 瞄准可视化。
        """
        if not self.cfg.DEBUG_DISPLAY or self._osd is None:
            return

        if self._debug_overlay.draw(lines, detection, path,
                                    aim_targets=aim_targets):
            Display.show_image(self._osd, x=0, y=0, layer=Display.LAYER_OSD0)

    # ------------------------------------------------------------------ #
    # 采样(plan §12 阶段 A:"静态赛道样张 ≥ 100 张")
    # ------------------------------------------------------------------ #
    def save_algo_frame(self, img, path, quality=None):
        """把 CHN1 帧落盘为 JPEG。返回布尔成功标志。

        K230 文件 IO 在 SD 卡上较慢,单帧约 30~80 ms;调用方应控制频率。
        """
        if img is None:
            return False
        if quality is None:
            quality = self.cfg.CAPTURE_JPEG_QUALITY
        try:
            img.save(path, quality=quality)
            return True
        except Exception as e:
            print("[camera] save frame failed:", path, e)
            return False

    # ------------------------------------------------------------------ #
    # 自检(plan §11.2 强制项)
    # ------------------------------------------------------------------ #
    def self_test(self):
        """阶段 A 自检:抓 5 帧算法输入,确认尺寸与像素格式符合配置。"""
        ok = True
        for _ in range(5):
            img = self.read_algo_frame()
            if img is None:
                print("[camera.self_test] snapshot failed")
                ok = False
                break
            if img.width() != self.cfg.ALGO_WIDTH or img.height() != self.cfg.ALGO_HEIGHT:
                print(
                    "[camera.self_test] size mismatch %dx%d vs %dx%d"
                    % (
                        img.width(),
                        img.height(),
                        self.cfg.ALGO_WIDTH,
                        self.cfg.ALGO_HEIGHT,
                    )
                )
                ok = False
                break
            img = None
        return ok

软件版本


CanMV_K230_V3P0_micropython_v1.6-0-ge4441cc_nncase_v2.9.0.img

1 Answers

你好,请提供一下代码。

感谢回复,帧读取的代码贴在下面了,整个camara.py已经被AI改得不成样子了

我修改了提问