镜头看见人脸,马赛克马上登场:用 行空板K10 做一台隐私摄像头

2026-10-053

参加 DFRobot 创客活动时,我想做一个不需要解释太多、拿起来就能看懂的小作品:镜头看到人脸,屏幕立刻给人脸“穿上”一层彩色像素盔甲。它既展示了 K10 的视觉识别和屏幕绘图,也把“摄像头画面里的隐私”变成了肉眼可见的互动效果。

活动页面:DFRobot 创客活动

一、灵感:让“检测到人脸”变成看得见的效果

很多视觉项目做到最后,屏幕上只剩一个识别框和一行“检测成功”。这次我想把结果做得更直观一点:人脸一出现,彩色方块就像舞台上的像素滤镜一样盖住脸;检测稍微闪断时,马赛克也不会马上消失。路过的人不用先看代码,就能明白 K10 正在“看见人脸并保护画面”。

这也是一个适合现场演示的互动小装置:把 K10 摄像头对准自己或屏幕,观察脸部如何被彩色方块覆盖;移动位置时,遮挡区域会跟着识别框调整。看到熟悉的画面突然变成像素拼图,效果有点像电视节目里的隐私处理,也多了一点创客作品的趣味。

演示时可以这样玩:先让脸进入镜头,看看“像素面具”出现;再左右移动,观察色块跟随;最后离开画面,马赛克会多停留几秒再退场。整个过程把“摄像头采集—AI识别—屏幕绘制”串成了一个看得见的反馈回路。

最终效果

下图是程序运行时的实拍:K10 屏幕正在显示摄像头画面,识别到人脸后,脸部区域被彩色马赛克覆盖。

UNIHIKER K10 人脸识别与马赛克遮挡的实机效果

图:K10 屏幕实时预览,脸部区域已被彩色方块遮挡。

发布到 CSDN 或 DF创客社区时,请将同目录图片一并上传到平台图床,再用平台生成的图片地址替换上面的相对路径。

实现上,程序启动后先显示实时摄像头画面;检测到人脸时,根据识别框的位置和尺寸,在脸部区域铺设彩色方块;短暂漏检时继续保留上一次的遮挡区域。

这里的“隐私摄像头”是指设备屏幕上的本地预览遮挡演示。程序没有修改摄像头原始帧,也没有实现照片保存、视频录制或网络推流中的脱敏。若要把画面用于录制、上传或其他设备显示,必须在对应的数据输出路径上执行脱敏,不能只依赖屏幕上的覆盖层。

二、硬件与工程配置

  • UNIHIKER K10 开发板(ESP32-S3,带摄像头和屏幕)
  • PlatformIO
  • UNIHIKER K10 Arduino 框架及 AIRecognition 库

工程使用的 platformio.ini:

[env:unihiker]
platform = unihiker
board = unihiker_k10
framework = arduino
build_flags =
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DARDUINO_USB_MODE=1
    -DModel=None

三、程序工作流程

  1. 初始化 K10 屏幕、人脸识别模块和摄像头预览。
  2. 先创建摄像头图像对象,再创建透明画布,使画布位于预览图像上方。
  3. 查询识别库的人脸检测状态,读取人脸中心点及宽高。
  4. 将遮挡区域稍微扩大,避免只盖住脸部中央。
  5. 检测框变化时重绘马赛克;短暂漏检时保留上次的马赛克。

K10 屏幕在当前竖屏配置下为 240×320,代码据此限制遮挡区域边界。若修改屏幕方向或分辨率,需要同步调整边界值。

四、关键代码

以下代码对应 src/main.cpp 的核心实现:

#include "unihiker_k10.h"
#include "AIRecognition.h"

UNIHIKER_K10 k10;
uint8_t screenDir = 2;
AIRecognition ai;

#define MOSAIC_BLOCK 14
#define MOSAIC_HOLD_MS 5000

// 根据色块网格坐标生成固定颜色,避免每轮刷新时颜色随机变化。
static uint32_t blockColor(int gx, int gy) {
    uint32_t h = (uint32_t)(gx * 73856093) ^ (uint32_t)(gy * 19349663);
    h ^= (h >> 13);
    uint8_t r = (uint8_t)(90 + (h & 0x7F));
    uint8_t g = (uint8_t)(90 + ((h >> 8) & 0x7F));
    uint8_t b = (uint8_t)(90 + ((h >> 16) & 0x7F));
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}

// 用线宽和线段长度相近的水平实线画一个近似正方形色块。
static void fillBlock(int x, int y, int size, uint32_t color) {
    k10.canvas->canvasSetLineWidth((uint8_t)size);
    int centerY = y + size / 2;
    k10.canvas->canvasLine(x, centerY, x + size - 1, centerY, color);
}

static void drawMosaic(int x0, int y0, int w, int h, int block) {
    for (int y = y0; y < y0 + h; y += block) {
        for (int x = x0; x < x0 + w; x += block) {
            uint32_t color = blockColor((x - x0) / block, (y - y0) / block);
            fillBlock(x, y, block, color);
        }
    }
}

void setup() {
    k10.begin();
    k10.initScreen(screenDir);
    k10.setScreenBackground(0x000000);
    ai.initAi();

    k10.initBgCamerImage();
    k10.setBgCamerImage(false);
    k10.creatCanvas();
    k10.setBgCamerImage(true);

    ai.switchAiMode(AIRecognition::Face);
}

void loop() {
    static bool mosaicVisible = false;
    static int mosaicX = 0, mosaicY = 0;
    static int mosaicW = 0, mosaicH = 0;
    static uint32_t lastFaceAt = 0;

    if (ai.isDetectContent(AIRecognition::Face)) {
        int cx = ai.getFaceData(AIRecognition::CenterX);
        int cy = ai.getFaceData(AIRecognition::CenterY);
        int faceW = ai.getFaceData(AIRecognition::Length);
        int faceH = ai.getFaceData(AIRecognition::Width);

        if (cx >= 0 && cy >= 0 && faceW > 0 && faceH > 0) {
            // 额头和下巴也留出一些遮挡余量。
            int margin = max(6, faceW / 12);
            int w = faceW + margin * 2;
            int h = faceH + margin * 2;
            int x = max(0, cx - w / 2);
            int y = max(0, cy - h / 2);

            // 裁剪到 240×320 屏幕范围。
            if (x + w > 240) w = 240 - x;
            if (y + h > 320) h = 320 - y;

            lastFaceAt = millis();
            bool moved = !mosaicVisible || abs(x - mosaicX) >= 4 ||
                         abs(y - mosaicY) >= 4 || abs(w - mosaicW) >= 4 ||
                         abs(h - mosaicH) >= 4;

            if (moved && w > 0 && h > 0) {
                k10.canvas->canvasClear();
                drawMosaic(x, y, w, h, MOSAIC_BLOCK);
                k10.canvas->updateCanvas();

                mosaicX = x;
                mosaicY = y;
                mosaicW = w;
                mosaicH = h;
                mosaicVisible = true;
            }
        }
    }

    // 漏检时先保留遮挡;连续 5 秒没有有效结果后再清除。
    if (mosaicVisible && (uint32_t)(millis() - lastFaceAt) > MOSAIC_HOLD_MS) {
        k10.canvas->canvasClear();
        k10.canvas->updateCanvas();
        mosaicVisible = false;
    }

    delay(80);
}

五、几个关键点

1. 画布必须在摄像头图像之后创建

摄像头预览和画布都是屏幕上的图层对象。若先创建画布、后创建摄像头图像,后创建的图像可能盖住画布,于是人脸框数据正常,但马赛克和调试色块看不到。

因此初始化顺序采用:

k10.initBgCamerImage();
k10.setBgCamerImage(false);
k10.creatCanvas();
k10.setBgCamerImage(true);

这里先暂时隐藏摄像头预览,再创建画布,最后恢复预览。

2. 用线宽画色块,避免“1×1 矩形加粗边框”

初版曾尝试在 1×1 矩形上设置较大的边框宽度,实机显示成了彩色竖条,不能形成完整方块。改用 canvasLine(),让线段长度和线宽都接近色块尺寸,才得到稳定的块状效果。

MOSAIC_BLOCK 控制色块大小。增大数值会让画面更粗糙,也会减少绘制的色块数量;减小数值则更细腻,但绘制调用会增加。

3. 识别状态会闪断,显示层需要短暂保持

识别任务和屏幕刷新并非逐帧同步。人脸短暂漏检时,isDetectContent(AIRecognition::Face) 可能返回 false。如果每轮循环都清空画布,马赛克就会跟着检测状态反复出现、消失。

当前实现只在人脸框变化达到一定幅度时重绘,并在最后一次有效检测后保留马赛克 5 秒。这个办法改善的是遮挡显示的连续性,并没有提高识别模型本身的识别率。

保留时间可以通过 MOSAIC_HOLD_MS 调整。数值越大,漏检时越不容易闪断;人脸离开后,旧位置的马赛克也会停留更久。

4. 识别框不等于马赛克

调试时屏幕可能看到绿色检测框,但主程序的马赛克颜色由 blockColor() 生成。检测框只表示识别模块提供了人脸位置,不能说明遮挡层已经正确画出。检查时应确认脸部区域出现的是多个不透明彩色块,而不是只有轮廓线。

六、识别效果的现场调整建议

这些调整不能改变模型,但通常有助于稳定检测条件:

  • 保持人脸正对摄像头,避免大角度侧脸或被手、口罩等大面积遮挡。
  • 提供均匀、充足的光线,避免逆光和强烈明暗切换。
  • 让人脸处于画面中部,并调整距离,使人脸在画面中有足够尺寸。
  • 固定摄像头后,先观察绿框是否仍频繁消失;如果消失,调整光线、角度和距离,再考虑延长 MOSAIC_HOLD_MS。

七、隐私使用提醒

本示例只是把遮挡图形叠加到 K10 屏幕预览上。它没有修改相机帧缓冲区,也没有对录制或网络发送的数据做处理。若应用需要可靠的隐私保护,应将人脸检测和遮挡放到实际保存、传输图像的处理路径中,并针对漏检制定更严格的策略。仅延长显示层的保持时间不能保证所有画面都已脱敏。

八、总结

这个小项目的关键不只是调用人脸识别接口,还包括摄像头图像与透明画布的层级顺序、K10 画布绘图接口的实际表现,以及异步检测导致的短暂漏检处理。把这些问题分别处理后,就能在本地预览中实现较连续的人脸马赛克遮挡。

发布前可按自己的 K10 固件版本重新编译并实机验证。本文代码面向 240×320 竖屏配置;若使用不同屏幕方向,应检查识别坐标和画布坐标是否一致。

关键词: UNIHIKER K10、DFRobot、ESP32-S3、AI视觉、人脸检测、隐私保护、Arduino、创客作品

创作许可协议

本项目采用 None(不开放任何权利,保留所有权利) 进行许可。

评论(0)
- 没有更多了 -