镜头看见人脸,马赛克马上登场:用 行空板K10 做一台隐私摄像头
参加 DFRobot 创客活动时,我想做一个不需要解释太多、拿起来就能看懂的小作品:镜头看到人脸,屏幕立刻给人脸“穿上”一层彩色像素盔甲。它既展示了 K10 的视觉识别和屏幕绘图,也把“摄像头画面里的隐私”变成了肉眼可见的互动效果。
活动页面:DFRobot 创客活动
一、灵感:让“检测到人脸”变成看得见的效果
很多视觉项目做到最后,屏幕上只剩一个识别框和一行“检测成功”。这次我想把结果做得更直观一点:人脸一出现,彩色方块就像舞台上的像素滤镜一样盖住脸;检测稍微闪断时,马赛克也不会马上消失。路过的人不用先看代码,就能明白 K10 正在“看见人脸并保护画面”。
这也是一个适合现场演示的互动小装置:把 K10 摄像头对准自己或屏幕,观察脸部如何被彩色方块覆盖;移动位置时,遮挡区域会跟着识别框调整。看到熟悉的画面突然变成像素拼图,效果有点像电视节目里的隐私处理,也多了一点创客作品的趣味。
演示时可以这样玩:先让脸进入镜头,看看“像素面具”出现;再左右移动,观察色块跟随;最后离开画面,马赛克会多停留几秒再退场。整个过程把“摄像头采集—AI识别—屏幕绘制”串成了一个看得见的反馈回路。
最终效果
下图是程序运行时的实拍: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
三、程序工作流程
- 初始化 K10 屏幕、人脸识别模块和摄像头预览。
- 先创建摄像头图像对象,再创建透明画布,使画布位于预览图像上方。
- 查询识别库的人脸检测状态,读取人脸中心点及宽高。
- 将遮挡区域稍微扩大,避免只盖住脸部中央。
- 检测框变化时重绘马赛克;短暂漏检时保留上次的马赛克。
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、创客作品



