自動マスキング(顔/ナンバープレート):ぼかし・モザイク・矩形入力API(Sharp×face‑api)
人物や車両が写る画像では秘匿すべき領域(顔・ナンバープレート等)が混在します。100%自動は現状不可能なため 顔自動検出+手動矩形補完 のハイブリッドが実務最適です。
本稿は (1) 検出 → (2) ユーザーUIで矩形修正 → (3) サーバ側で確定合成 → (4) 一時バッファ削除 という 再現性のある処理パイプライン を提示します。
運用上の注意
検出モデルの閾値低下は誤検出増 → 手動負荷増に繋がります。Recall 80〜90% / Precision確保 の中庸設定で“人が楽”を優先しましょう。
1. 顔検出(TinyFaceDetector)
// scripts/detect-faces.ts — 顔検出(TinyFaceDetector)
import fs from "node:fs/promises";
import sharp from "sharp";
export type Rect = { x:number; y:number; width:number; height:number; };
export async function detectFaces(buf:Buffer): Promise<Rect[]> {
try {
const faceapi = await import("@vladmandic/face-api");
const canvas = await import("canvas");
// @ts-ignore
faceapi.env.monkeyPatch({ Canvas: canvas.Canvas, Image: canvas.Image, ImageData: canvas.ImageData });
await faceapi.nets.tinyFaceDetector.loadFromDisk("./models");
const img = await canvas.loadImage(buf);
const res = await faceapi.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions());
return res.map((r:any)=>({ x:r.box.x, y:r.box.y, width:r.box.width, height:r.box.height }));
} catch { return []; }
}
2. ぼかし/モザイクAPI
// app/api/redact/route.ts — 顔自動+矩形手動のぼかし/モザイク
import { NextRequest, NextResponse } from "next/server";
import sharp from "sharp";
export const runtime="nodejs"; export const dynamic="force-dynamic";
type Rect = { x:number; y:number; width:number; height:number; };
function parseRects(q:string|null):Rect[]{
if(!q) return [];
try { return JSON.parse(q); } catch { return []; }
}
function clamp(n:number,min:number,max:number){ return Math.max(min, Math.min(max, n)); }
export async function GET(req: NextRequest){
const { searchParams } = new URL(req.url);
const src = searchParams.get("url");
const mode = (searchParams.get("mode") ?? "blur").toLowerCase(); // blur|mosaic
const rectsParam = searchParams.get("rects"); // 手動矩形 JSON: [{x,y,width,height},...]
if(!src) return NextResponse.json({error:"url is required"},{status:400});
const r = await fetch(src,{cache:"no-store"}); if(!r.ok) return NextResponse.json({error:"fetch failed"},{status:502});
const input = Buffer.from(await r.arrayBuffer());
const base = sharp(input).rotate().toColourspace("srgb"); const meta = await base.metadata();
const W = meta.width ?? 0, H = meta.height ?? 0;
// 1) 顔自動(任意)
let rects:Rect[] = [];
try {
const { detectFaces } = await import("../../../../scripts/detect-faces"); // ビルド都合でパス調整
rects = await detectFaces(input);
} catch {}
// 2) 手動矩形で上書き/追加
rects = [...rects, ...parseRects(rectsParam)];
// 3) コンポジット用に各領域を加工
const layers: { input: Buffer; left: number; top: number; }[] = [];
for (const r of rects) {
const x = clamp(Math.round(r.x), 0, W), y = clamp(Math.round(r.y), 0, H);
const w = clamp(Math.round(r.width), 1, W - x), h = clamp(Math.round(r.height), 1, H - y);
const region = await base.clone().extract({ left:x, top:y, width:w, height:h }).toBuffer();
let processed:Buffer;
if(mode==="mosaic"){
const s = Math.max(4, Math.round(Math.min(w,h)/20));
processed = await sharp(region).resize(Math.max(1, Math.round(w/s)), Math.max(1, Math.round(h/s)), { kernel:"nearest" })
.resize(w, h, { kernel:"nearest" }).toBuffer();
} else {
processed = await sharp(region).blur(20).toBuffer();
}
layers.push({ input: processed, left: x, top: y });
}
const out = await base.composite(layers).jpeg({ quality:86, mozjpeg:true, progressive:true }).toBuffer();
return new NextResponse(out, { headers: { "Content-Type":"image/jpeg", "Cache-Control":"public, max-age=60, s-maxage=86400" }});
}
3. 公開前チェック
- 検出結果に人手レビューを実施(ダブルチェック体制が望ましい)。
- 原画像/座標/生成物はジョブ完了後即時削除(S3一時バケットTTL設定)。
- 書き出し形式が用途に合致(形式使い分け)。
- 処理失敗時にマスクなし原画像を返さないフェイルセーフ。
4. まとめ
“完全自動”を追うより ハイブリッド+最小保持 の設計がコスパとリスク低減の両立に寄与します。検出→補正→合成→破棄の一方向パイプラインをテンプレ化し、ログと権限を最小化しましょう。
Version: 2025-09-06 – schemaユーティリティ移行・FAQ/keywords追加・導入/チェック/まとめ拡張。