AI PLAY玩法更新持续收录
← 回到更新流

2026-09-29 · 原文发布日

Photo Scrubber:浏览器本地给人脸打码去元数据

Simon Willison让GPT-6 Astra帮忙写了一个实验性小工具Photo Scrubber,用于分享照片前处理隐私:用Google MediaPipe的WebAssembly版和BlazeFace模型在浏览器内自动检测人脸并模糊,也可手动涂抹调整模糊强度,导出时剥离全部个人元数据。所有处理都在本地完成,照片不离开设备,适合发街拍、抗议现场等含陌生人脸的照片前快速脱敏。

原文要点

Simon Willison分享了他让GPT-6 Astra生成的实验工具Photo Scrubber:在浏览器本地处理照片,分享前给陌生人脸打码并去元数据。技术上用Google MediaPipe C++库的WebAssembly版本(@mediapipe/tasks-vision包)加载BlazeFace人脸检测模型自动识别人脸并模糊,支持手动涂抹补救和调节模糊强度,导出时剥离全部个人元数据。所有计算在浏览器内完成,照片不上传任何服务器。作者动机是拍到抗议者照片后不愿公开陌生人可识别的脸。

这次做成什么

不依赖原工具文件,自己写一个单HTML文件实现同款能力:浏览器内加载MediaPipe人脸检测,自动模糊检测到的人脸,支持手动补涂,导出无元数据的图片,并用一张合成测试图验证。

准备什么

  • 一台装了现代浏览器(Chrome或Edge)的电脑
  • 首次打开页面时需要联网以下载MediaPipe模型文件
  • 一张含人脸的测试照片(自拍或合成图,不要用他人真实敏感照片练手)
  • 会用记事本或任意编辑器保存.html文件

具体怎么做

  1. 保存并打开页面

    把下方artifact里的完整HTML代码保存为photo-scrubber.html,双击用浏览器打开。页面点击加载模型按钮后,会通过CDN加载@mediapipe/tasks-vision并下载BlazeFace模型,状态栏显示模型就绪。

  2. 上传照片并自动检测

    点选择图片载入测试照片,图片画进canvas后点检测人脸。MediaPipe返回每个人脸的边界框,页面在框位置叠加高斯模糊块,并在图片下方列出检测到几张脸。

  3. 手动补涂与调强度

    自动检测漏掉的侧脸或遮挡脸,按住鼠标在canvas上涂抹,松开后涂抹区域被模糊;拖动模糊强度滑块(控制像素块大小,越大越糊)后重新点应用模糊更新效果。

  4. 导出并验证脱敏

    点导出图片,页面用canvas.toDataURL重新编码为PNG下载。因为重新编码不携带原文件的EXIF,GPS、设备型号、拍摄时间等元数据被剥离。用系统属性或exif查看工具确认导出图无任何EXIF字段。

可直接复制的材料

photo-scrubber.html完整代码

本站独立编写的教学实现,非Simon Willison原工具代码;CDN版本号0.10.14为示例,使用前可在npmjs.com核对@mediapipe/tasks-vision最新版本。

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>本地人脸打码工具(本站示例)</title>
<style>body{font-family:sans-serif;max-width:900px;margin:20px auto}canvas{border:1px solid #ccc;max-width:100%}button{margin:6px 4px;padding:8px 14px}</style>
</head>
<body>
<h2>本地人脸打码与元数据清除</h2>
<p>所有处理在本浏览器内完成,图片不上传。</p>
<input type="file" id="file" accept="image/*">
<button id="loadModel">加载模型</button>
<button id="detect" disabled>检测人脸</button>
<label>模糊强度 <input type="range" id="strength" min="4" max="24" value="10"></label>
<button id="apply" disabled>应用模糊</button>
<button id="export" disabled>导出图片</button>
<p id="status">请先加载模型(首次需联网)。</p>
<canvas id="cv"></canvas>
<script type="module">
import { FaceDetector, FilesetResolver } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14";
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const st = document.getElementById('status');
let detector = null, faces = [], strokes = [], img = null, drawing = false, cur = null;
document.getElementById('loadModel').onclick = async () => {
  st.textContent = '模型下载中…';
  const files = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm");
  detector = await FaceDetector.createFromOptions(files, {
    baseOptions: { modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite" },
    runningMode: "IMAGE" });
  st.textContent = '模型就绪,请选择图片。';
  document.getElementById('detect').disabled = false;
};
document.getElementById('file').onchange = e => {
  const f = e.target.files[0]; if (!f) return;
  const url = URL.createObjectURL(f);
  img = new Image();
  img.onload = () => { cv.width = img.width; cv.height = img.height; redraw(); st.textContent = '图片已载入,点检测人脸。'; };
  img.src = url;
};
function redraw(){ if(!img) return; ctx.drawImage(img,0,0); faces.forEach(b=>blurRect(b)); strokes.forEach(b=>blurRect(b)); }
function blurRect(b){
  const s = parseInt(document.getElementById('strength').value);
  const d = ctx.getImageData(b.x,b.y,b.w,b.h);
  const off = document.createElement('canvas'); off.width=b.w; off.height=b.h;
  const octx = off.getContext('2d'); octx.putImageData(d,0,0);
  octx.globalAlpha=0.5; octx.drawImage(off,0,0,b.w/s,b.h/s);
  octx.globalAlpha=1; octx.imageSmoothingEnabled=true;
  octx.drawImage(off,0,0,b.w/s,b.h/s,0,0,b.w,b.h);
  ctx.putImageData(octx.getImageData(0,0,b.w,b.h), b.x, b.y);
}
document.getElementById('detect').onclick = () => {
  if (!detector || !img) return;
  const r = detector.detect(cv);
  faces = r.detections.map(d => ({ x:d.boundingBox.originX, y:d.boundingBox.originY, w:d.boundingBox.width, h:d.boundingBox.height }));
  redraw();
  st.textContent = '检测到 ' + faces.length + ' 张人脸。';
  document.getElementById('apply').disabled = false;
  document.getElementById('export').disabled = false;
};
document.getElementById('apply').onclick = redraw;
cv.onmousedown = e => { drawing = true; cur = null; };
cv.onmousemove = e => {
  if (!drawing) return;
  const r = cv.getBoundingClientRect();
  const x = (e.clientX - r.left) * (cv.width / r.width);
  const y = (e.clientY - r.top) * (cv.height / r.height);
  if (cur) { cur.w = x - cur.x; cur.h = y - cur.y; } else { cur = { x, y, w:0, h:0 }; }
};
cv.onmouseup = () => { if (cur && Math.abs(cur.w) > 5) strokes.push(cur); cur = null; drawing = false; redraw(); st.textContent = '已添加手动涂抹区域。'; };
document.getElementById('export').onclick = () => {
  const a = document.createElement('a');
  a.download = 'scrubbed.png';
  a.href = cv.toDataURL('image/png');
  a.click();
  st.textContent = '已导出PNG,原EXIF元数据不随canvas重编码保留。';
};
</script>
</body>
</html>

输入与输出示例

本站虚构演示,未实际运行;BlazeFace对侧脸和小脸的召回有限,漏检属预期行为。

输入

载入一张1280x960的测试照片,画面里有2张正脸和1张侧脸,原文件EXIF含GPS纬度31.23、设备型号Pixel测试机、拍摄时间2026-09-01 10:00:00。

对应输出

点检测人脸后状态栏显示检测到 2 张人脸,两张正脸被模糊块覆盖;侧脸漏检,用鼠标在其位置拖出一个涂抹框,松开后该区域变模糊;导出scrubbed.png后用EXIF查看器检查,GPS、设备型号、拍摄时间字段全部不存在,文件只含PNG基础信息。

做完检查这些

  • 检测人脸后正脸区域被不可恢复的模糊覆盖,导出图中无法辨认五官
  • 手动涂抹的区域同样被模糊,且调节强度滑块后模糊块大小变化
  • 导出PNG在任何EXIF查看器中不含GPS、设备、时间字段
  • 整个流程在打开页面后不产生对自家服务器的图片上传(可在浏览器开发者工具Network面板确认只有模型下载请求)

容易卡住的地方

  • 首次加载模型需要联网访问CDN和Google模型存储,完全离线环境不可用
  • BlazeFace是轻量模型,对侧脸、遮挡脸、小脸会漏检,必须配合手动涂抹复查;本示例代码的涂抹框需从左上向右下拖动,反向拖会因负宽高报错
  • 模糊属可逆性存疑的弱打码,高价值敏感场景建议用实心黑块替代模糊
  • canvas导出PNG体积可能比原图大很多;改JPEG导出可减小体积但仍有损
  • 原工具是实验性项目,本站代码同为教学示例,不要当作合规审计级工具

原文与作者资料 ↗

本站借助AI整理来源事实,独立编写中文操作指南与示例。示例不代表实际运行结果;来源效果尚未由本站独立复现。付费模板与模型、服务费用分别计算。

原文标题
Photo Scrubber — local face blur & metadata removal
原作者
Simon Willison
原始发布日期
2026-09-29
来源
https://simonwillison.net/2026/Sep/29/photo-scrubber/