gazou-compressor.jp

自動マスキング(顔/ナンバープレート):ぼかし・モザイク・矩形入力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. 公開前チェック

4. まとめ

“完全自動”を追うより ハイブリッド+最小保持 の設計がコスパとリスク低減の両立に寄与します。検出→補正→合成→破棄の一方向パイプラインをテンプレ化し、ログと権限を最小化しましょう。

Version: 2025-09-06 – schemaユーティリティ移行・FAQ/keywords追加・導入/チェック/まとめ拡張。

公開:2025-09-06 / 監修:gazou-compressor.jp 編集部

gazou-compressor.jp 編集部

画像圧縮・変換・背景除去などの実践テクニックと、Webで“速く・軽く・崩さない”ためのノウハウを発信しています。

関連記事

トピック/更新日の近いコンテンツ