From bda636b0efbfea276ed702d2fffc48b7558fd3d8 Mon Sep 17 00:00:00 2001 From: wds Date: Thu, 1 Jan 2026 15:41:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:sparkles:=20=E4=BA=BA=E8=84=B8?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E7=B1=BB=E4=BB=A5=E5=8F=8A=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k230/04test_face_detect.py | 65 +++++++++ k230/face_detect_module.py | 280 +++++++++++++++++++++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 k230/04test_face_detect.py create mode 100644 k230/face_detect_module.py diff --git a/k230/04test_face_detect.py b/k230/04test_face_detect.py new file mode 100644 index 0000000..593caae --- /dev/null +++ b/k230/04test_face_detect.py @@ -0,0 +1,65 @@ +# 04test_face_detect.py +# 人脸检测测试程序 +# 适用于庐山派 K230-CanMV 开发板 + +from face_detect_module import FaceDetector +import os +import sys +import gc + +sys.path.append("/sdcard") +os.exitpoint(os.EXITPOINT_ENABLE) + + +# ============ 配置 ============ +DISPLAY_MODE = "lcd" # 显示模式: "lcd" 或 "hdmi" +CONFIDENCE = 0.5 # 置信度阈值 +# ============================== + +print("=" * 40) +print("人脸检测测试") +print("显示模式: {}".format(DISPLAY_MODE)) +print("=" * 40) + +# 创建人脸检测器 +detector = FaceDetector( + display_mode=DISPLAY_MODE, + confidence_threshold=CONFIDENCE +) + +try: + # 启动检测器 + print("正在初始化...") + detector.start() + print("初始化完成,开始检测...") + + while True: + os.exitpoint() + + # 检测人脸 + img, faces = detector.detect() + + # 打印检测结果 + if faces: + print("检测到 {} 个人脸:".format(len(faces))) + for i, (x, y, w, h, score) in enumerate(faces): + print(" 人脸{}: x={}, y={}, w={}, h={}, score={:.2f}".format( + i + 1, x, y, w, h, score + )) + + # 绘制检测框 + detector.draw_boxes(color=(255, 255, 0, 255), thickness=2) + + # 显示结果 + detector.show() + + gc.collect() + +except KeyboardInterrupt: + print("\n用户停止") +except Exception as e: + print("异常: ", e) + sys.print_exception(e) +finally: + detector.stop() + print("检测器已停止") diff --git a/k230/face_detect_module.py b/k230/face_detect_module.py new file mode 100644 index 0000000..25c12b5 --- /dev/null +++ b/k230/face_detect_module.py @@ -0,0 +1,280 @@ +# face_detect_module.py +# 人脸检测模块 +# 适用于庐山派 K230-CanMV 开发板 + +from libs.PipeLine import PipeLine, ScopedTiming +from libs.AIBase import AIBase +from libs.AI2D import Ai2d +import os +import nncase_runtime as nn +import ulab.numpy as np +import aidemo +import gc + + +class FaceDetector: + """ + 人脸检测类 - 使用K230的AI能力检测人脸 + + 功能: + - 检测图像中的人脸 + - 返回人脸边界框坐标 (x, y, w, h, score) + - 支持在图像上绘制检测框 + + 参数: + display_mode: 显示模式 "lcd" 或 "hdmi" + confidence_threshold: 置信度阈值 (默认0.5) + nms_threshold: 非极大值抑制阈值 (默认0.2) + debug_mode: 调试模式开关 (默认0关闭) + + 使用示例: + detector = FaceDetector(display_mode="lcd") + detector.start() + while True: + img, faces = detector.detect() + # faces = [(x, y, w, h, score), ...] + detector.show() + detector.stop() + """ + + # 模型配置 + KMODEL_PATH = "/sdcard/examples/kmodel/face_detection_320.kmodel" + ANCHORS_PATH = "/sdcard/examples/utils/prior_data_320.bin" + MODEL_INPUT_SIZE = [320, 320] + ANCHOR_LEN = 4200 + DET_DIM = 4 + + # 显示配置 + DISPLAY_CONFIGS = { + "lcd": { + "display_size": [800, 480], + "rgb888p_size": [1920, 1080], + }, + "hdmi": { + "display_size": [1920, 1080], + "rgb888p_size": [1920, 1080], + } + } + + def __init__(self, display_mode="lcd", confidence_threshold=0.5, + nms_threshold=0.2, debug_mode=0): + """ + 初始化人脸检测器 + + 参数: + display_mode: 显示模式 "lcd" 或 "hdmi" + confidence_threshold: 置信度阈值,值越高过滤越严格 + nms_threshold: NMS阈值,防止重复检测 + debug_mode: 调试模式,1开启计时输出 + """ + if display_mode not in self.DISPLAY_CONFIGS: + raise ValueError("display_mode必须是 'lcd' 或 'hdmi'") + + self._display_mode = display_mode + self._confidence_threshold = confidence_threshold + self._nms_threshold = nms_threshold + self._debug_mode = debug_mode + + # 获取显示配置 + config = self.DISPLAY_CONFIGS[display_mode] + self._display_size = config["display_size"] + self._rgb888p_size = config["rgb888p_size"] + + # 加载锚点数据 + self._anchors = np.fromfile(self.ANCHORS_PATH, dtype=np.float) + self._anchors = self._anchors.reshape((self.ANCHOR_LEN, self.DET_DIM)) + + self._pipeline = None + self._face_det = None + self._is_running = False + self._last_faces = [] + + def start(self): + """启动人脸检测器,初始化摄像头和模型""" + if self._is_running: + return + + # 初始化Pipeline + self._pipeline = PipeLine( + rgb888p_size=self._rgb888p_size, + display_size=self._display_size, + display_mode=self._display_mode + ) + self._pipeline.create() + + # 初始化人脸检测模型 + self._face_det = _FaceDetectionApp( + kmodel_path=self.KMODEL_PATH, + model_input_size=self.MODEL_INPUT_SIZE, + anchors=self._anchors, + confidence_threshold=self._confidence_threshold, + nms_threshold=self._nms_threshold, + rgb888p_size=self._rgb888p_size, + display_size=self._display_size, + debug_mode=self._debug_mode + ) + self._face_det.config_preprocess() + + self._is_running = True + + def detect(self): + """ + 检测当前帧中的人脸 + + 返回: + tuple: (img, faces) + - img: 当前帧图像 + - faces: 人脸列表,每个元素为 (x, y, w, h, score) + 坐标已转换为显示分辨率下的实际坐标 + """ + if not self._is_running: + raise RuntimeError("检测器未启动,请先调用 start()") + + os.exitpoint() + + # 获取当前帧 + img = self._pipeline.get_frame() + + # 推理 + res = self._face_det.run(img) + + # 转换坐标为显示分辨率 + faces = [] + if res: + for det in res: + x, y, w, h = map(lambda v: int(round(v, 0)), det[:4]) + score = det[4] if len(det) > 4 else 1.0 + # 转换为显示坐标 + x = x * self._display_size[0] // self._rgb888p_size[0] + y = y * self._display_size[1] // self._rgb888p_size[1] + w = w * self._display_size[0] // self._rgb888p_size[0] + h = h * self._display_size[1] // self._rgb888p_size[1] + faces.append((x, y, w, h, score)) + + self._last_faces = faces + return img, faces + + def draw_boxes(self, color=(255, 255, 0, 255), thickness=2): + """ + 在OSD层绘制人脸检测框 + + 参数: + color: 框颜色 (R, G, B, A) + thickness: 线条粗细 + """ + if not self._is_running: + return + + self._pipeline.osd_img.clear() + for (x, y, w, h, score) in self._last_faces: + self._pipeline.osd_img.draw_rectangle( + x, y, w, h, + color=color, + thickness=thickness + ) + + def show(self): + """显示当前帧(需要先调用detect和draw_boxes)""" + if self._is_running: + self._pipeline.show_image() + gc.collect() + + def stop(self): + """停止人脸检测器,释放资源""" + if self._face_det: + self._face_det.deinit() + self._face_det = None + if self._pipeline: + self._pipeline.destroy() + self._pipeline = None + self._is_running = False + + @property + def is_running(self): + """是否正在运行""" + return self._is_running + + @property + def display_size(self): + """显示分辨率""" + return self._display_size + + @property + def last_faces(self): + """上次检测到的人脸列表""" + return self._last_faces + + +class _FaceDetectionApp(AIBase): + """内部人脸检测应用类,继承自AIBase""" + + def __init__(self, kmodel_path, model_input_size, anchors, + confidence_threshold=0.5, nms_threshold=0.2, + rgb888p_size=[224, 224], display_size=[1920, 1080], + debug_mode=0): + super().__init__(kmodel_path, model_input_size, rgb888p_size, debug_mode) + self.kmodel_path = kmodel_path + self.model_input_size = model_input_size + self.confidence_threshold = confidence_threshold + self.nms_threshold = nms_threshold + self.anchors = anchors + # 对宽度进行16字节对齐 + self.rgb888p_size = [self._align_up(rgb888p_size[0], 16), rgb888p_size[1]] + self.display_size = [self._align_up(display_size[0], 16), display_size[1]] + self.debug_mode = debug_mode + self.ai2d = Ai2d(debug_mode) + self.ai2d.set_ai2d_dtype( + nn.ai2d_format.NCHW_FMT, + nn.ai2d_format.NCHW_FMT, + np.uint8, + np.uint8 + ) + + def _align_up(self, value, alignment): + """向上对齐""" + return ((value + alignment - 1) // alignment) * alignment + + def config_preprocess(self, input_image_size=None): + """配置预处理""" + with ScopedTiming("set preprocess config", self.debug_mode > 0): + ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size + top, bottom, left, right = self.get_padding_param() + self.ai2d.pad([0, 0, 0, 0, top, bottom, left, right], 0, [104, 117, 123]) + self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) + self.ai2d.build( + [1, 3, ai2d_input_size[1], ai2d_input_size[0]], + [1, 3, self.model_input_size[1], self.model_input_size[0]] + ) + + def postprocess(self, results): + """后处理""" + with ScopedTiming("postprocess", self.debug_mode > 0): + post_ret = aidemo.face_det_post_process( + self.confidence_threshold, + self.nms_threshold, + self.model_input_size[1], + self.anchors, + self.rgb888p_size, + results + ) + if len(post_ret) == 0: + return post_ret + else: + return post_ret[0] + + def get_padding_param(self): + """计算填充参数""" + dst_w = self.model_input_size[0] + dst_h = self.model_input_size[1] + ratio_w = dst_w / self.rgb888p_size[0] + ratio_h = dst_h / self.rgb888p_size[1] + ratio = min(ratio_w, ratio_h) + new_w = int(ratio * self.rgb888p_size[0]) + new_h = int(ratio * self.rgb888p_size[1]) + dw = (dst_w - new_w) / 2 + dh = (dst_h - new_h) / 2 + top = int(round(0)) + bottom = int(round(dh * 2 + 0.1)) + left = int(round(0)) + right = int(round(dw * 2 - 0.1)) + return top, bottom, left, right