動機
私は自分や友人の顔が分かる画像をSNSに載せる際、必ず顔が分からないように投稿しています。
隠す方法は様々で、SNSに備えついている機能で隠したり、Photoshop等の画像編集アプリで加工したり...。
色々ありますが、どれも地味に手間...というか、考える時間が発生するんですよね。
脳死で画像UP ⇒ 自動で顔にモザイク ⇒ 加工後画像DL
このようなシンプルでかつ必要最低限、お金がかからなくて広告もないアプリがあったらいいな~ということで、自分で作ってみることにしました。
と言ってもアプリなんて作ったことないし...顔認識とかどうやるか分かんないし...どうしよ。
って状況でしたが、ふと「あれ?AI使えば作れちゃうんじゃね?」と思いつきました。
今回は自動モザイクアプリをAIを用いて作成した工程、結果を簡単にご紹介します。
※結果的に半自動になりました
目標
- 画像UP後、顔を認識して自動的にモザイクを掛ける
- 加工後画像を保存できる
全体構成
役割 | 主な技術 | |
---|---|---|
Backend (Flask) | 画像を受け取り、顔を自動検出してモザイク処理し、結果画像を返す | Flask, OpenCV, MediaPipe, NumPy |
Frontend (React) | ユーザーが画像をアップロードし、モザイク強度を調整したり手動で追加モザイクを描くUI | React, HTML5 Canvas |
通信 | React → Flask に画像をPOST送信、処理済み画像を受け取る | fetch API + Flask API(CORS対応) |
GPTにこういうものが作りたいんだよね~と言ったら提案してくれました。
※さりげなくFrontend行に追加モザイクを手動で描くとか書いてある件には後ほど...
メインフォルダ構成
mosaic-project/
├─ backend/
│ ├─ app.py
│ └─ requirements.txt
│
└─ client/
└─ src/
└─ App.js
└─ MosaicCanvas.jsx
編集した物のみ記載しています。
1. バックエンド(Flask)作成
必要パッケージの記載
今回のアプリを動かすために必要なPythonパッケージ(ライブラリ)一覧です。
flask
flask-cors
opencv-python
mediapipe
numpy
app.pyの記載
React(フロント)から送られた画像ファイルを受け取り、顔を検出してモザイクをかけ、加工済み画像を返す役割を担っています。
from flask import Flask, request, send_file, jsonify
from flask_cors import CORS
import cv2
import numpy as np
import mediapipe as mp
import tempfile
import os
app = Flask(__name__)
CORS(app)
mp_face_detection = mp.solutions.face_detection
def apply_mosaic(img, detections, strength=15):
h, w, _ = img.shape
for detection in detections:
bbox = detection.location_data.relative_bounding_box
x1 = int(bbox.xmin * w)
y1 = int(bbox.ymin * h)
x2 = int((bbox.xmin + bbox.width) * w)
y2 = int((bbox.ymin + bbox.height) * h)
# 画面外に出ないよう調整
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(w, x2), min(h, y2)
face = img[y1:y2, x1:x2]
if face.size > 0:
face = cv2.resize(face, (strength, strength))
face = cv2.resize(face, (x2 - x1, y2 - y1), interpolation=cv2.INTER_NEAREST)
img[y1:y2, x1:x2] = face
return img
@app.route("/api/mosaic", methods=["POST"])
def mosaic():
try:
if "image" not in request.files:
return jsonify({"error": "No image uploaded"}), 400
file = request.files["image"]
strength = int(request.form.get("strength", 15)) # モザイク強さ
img = cv2.imdecode(np.frombuffer(file.read(), np.uint8), cv2.IMREAD_COLOR)
all_detections = []
rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Short range モデル (近距離複数人)
with mp_face_detection.FaceDetection(model_selection=0, min_detection_confidence=0.45) as fd_short:
res_short = fd_short.process(rgb_img)
if res_short.detections:
all_detections.extend(res_short.detections)
# Full range モデル (遠距離向け)
with mp_face_detection.FaceDetection(model_selection=1, min_detection_confidence=0.45) as fd_full:
res_full = fd_full.process(rgb_img)
if res_full.detections:
all_detections.extend(res_full.detections)
# モザイク処理
if all_detections:
img = apply_mosaic(img, all_detections, strength=strength)
# 一時ファイルに保存して返却
tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg")
cv2.imwrite(tmp_file.name, img)
tmp_file.close()
return send_file(tmp_file.name, mimetype="image/jpeg")
except Exception as e:
return jsonify({"error": str(e)}), 500
finally:
# 処理後に一時ファイルを削除
if "tmp_file" in locals():
try:
os.unlink(tmp_file.name)
except Exception:
pass
if __name__ == "__main__":
app.run(debug=True)
Flaskサーバー起動
必要パッケージとapp.pyの記載ができたので、実際に起動します。
- cd backend
- pip install -r requirements.txt ← こちらで記載した必要パッケージのインストール
- python app.py ← Flask起動
起動後、この表示が出ていればFlaskサーバーの起動が出来ています。
2. フロントエンド(React)作成
事前準備
- Node.jsのインストール
- npx create-react-app client ← Reactプロジェクトを自動生成
- npm install ← 不足してるようであればパッケージインストール
App.jsの記載
ユーザーがUPした画像を取得し、POSTでFlaskサーバーへの画像送信、Flaskから返ってきた加工済み画像の表示とダウンロードをさせる役割を担っています。
import React, { useState } from "react";
export default function App() {
const [preview, setPreview] = useState(null);
const [resultUrl, setResultUrl] = useState(null);
const [uploading, setUploading] = useState(false);
const [strength, setStrength] = useState(15);
const handleFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
setPreview(URL.createObjectURL(file));
setResultUrl(null);
// 自動モザイク適用(バックエンド呼び出し)
const fd = new FormData();
fd.append("image", file);
fd.append("strength", strength);
setUploading(true);
try {
const res = await fetch("http://localhost:5000/api/mosaic", {
method: "POST",
body: fd,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || res.statusText);
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setResultUrl(url);
} catch (err) {
alert("失敗: " + err.message);
} finally {
setUploading(false);
}
};
const handleStrengthChange = (e) => setStrength(Number(e.target.value));
const handleDownload = () => {
if (!resultUrl) return;
const a = document.createElement("a");
a.href = resultUrl;
a.download = "mosaic.jpg";
a.click();
};
return (
<div style={{ maxWidth: 560, margin: "auto", padding: 16, textAlign: "center" }}>
<h1>顔モザイクアプリ</h1>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
style={{ display: "block", margin: "12px auto" }}
disabled={uploading}
/>
<label>
モザイク強さ: {strength}
<input
type="range"
min="1"
max="60"
value={strength}
onChange={handleStrengthChange}
style={{ width: "100%" }}
/>
</label>
{uploading && <p>処理中です...</p>}
{resultUrl && (
<div style={{ marginTop: 18 }}>
<p>加工後</p>
<img src={resultUrl} alt="モザイク結果" style={{ maxWidth: "100%" }} />
<button
style={{ marginTop: 8, padding: "6px 12px" }}
onClick={handleDownload}
>
ダウンロード
</button>
</div>
)}
</div>
);
}
Reactサーバー(Node.js)起動
- cd client
- npm start ← React起動
3. 表示・動作確認
表示確認
先程のReact起動と同時に、http://localhost:3000 でページが自動で開きました。
ページデザインに関してはGPTに作ってもらったままなので鬼ダサいです。
表示は問題なさそうですね。
動作確認
次は実際に画像をUPして、顔を認識して自動でモザイクがかかり、DLできるか確認します。
加工する画像はこちらです。
実際の動き↓
自動で顔にモザイクを掛けられていることが分かります。
これで完成!と言いたいところですが...
別画像を試してみる
奥の3人の顔は認識できモザイクが適用されていますが、手前の2人は顔認識ができていません。
どうやら今回顔認識の役割を担っているMediapipeライブラリは、真横顔や顔付近に手などの障害物がある場合、顔の特徴が不明瞭として顔認識ができないようです
どうする
始めに脳死で画像UPして自動的に~とか言ってたのに誠に遺憾ですが...。
手動でドラッグして選択した箇所に、モザイクをかけれる機能を追加することにしました。
4. (再)フロントエンド作成
App.js変更点
変更内容 | 目的 | 変更箇所 |
---|---|---|
① MosaicCanvas コンポーネントを導入 |
手動モザイクを描けるように&最後に追加したモザイクを消す |
import MosaicCanvas from "./MosaicCanvas"; と <MosaicCanvas ... />
|
② useRef の追加 |
Canvas を外から操作できるように(例:ダウンロード) | const canvasRef = useRef(null); |
③ handleDownload 関数で Canvas の内容を保存 |
手動で描いたモザイクを含めてダウンロード |
const handleDownload = () => {...} の部分 |
④ 「ひとつ前に戻す」機能を追加 | 最後に描いたモザイクを取り消せるようにする |
MosaicCanvas.js に undoLast() 実装済み → App.js に「ひとつ前に戻す」ボタンを追加 |
MosaicCanvasコンポーネントの追加
加工後画像上でマウスドラッグを行い、モザイクを描画できるようになるためにMosaicCanvasというコンポーネントを新規で追加します。
コンポーネント内で行ってることは:
- imageUrlで渡された画像をキャンバスに描画
- マウス操作で指定した範囲にモザイクを適用
- 最後に手動追加したモザイクを戻す
- ref経由で親コンポーネント(App.js)からアクセス可能に
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
const MosaicCanvas = forwardRef(({ imageUrl, strength = 15 }, ref) => {
const canvasRef = useRef(null);
const [dragging, setDragging] = useState(false);
const [start, setStart] = useState(null);
const [current, setCurrent] = useState(null);
const [img, setImg] = useState(null);
const [mosaicStrength, setMosaicStrength] = useState(strength);
const appliedAreas = useRef([]); // 過去のモザイク情報
// 親コンポーネントから操作可能に
useImperativeHandle(ref, () => ({
undoLast: () => {
if (appliedAreas.current.length > 0) {
appliedAreas.current.pop();
redrawCanvas();
}
},
getCanvas: () => canvasRef.current // ← ここで canvas を取得
}));
// 画像ロード
useEffect(() => {
if (!imageUrl) return;
const image = new Image();
image.src = imageUrl;
image.onload = () => {
setImg(image);
const canvas = canvasRef.current;
canvas.width = image.width;
canvas.height = image.height;
redrawCanvas(image);
};
}, [imageUrl]);
// モザイク強さ変更時
useEffect(() => {
setMosaicStrength(strength);
redrawCanvas();
}, [strength]);
const getMousePos = (e) => {
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY
};
};
const applyMosaic = (x, y, w, h, strength) => {
if (!img) return;
const ctx = canvasRef.current.getContext("2d");
const imageData = ctx.getImageData(x, y, w, h);
const tempCanvas = document.createElement("canvas");
tempCanvas.width = w;
tempCanvas.height = h;
tempCanvas.getContext("2d").putImageData(imageData, 0, 0);
const small = document.createElement("canvas");
small.width = Math.max(1, Math.floor(w / strength));
small.height = Math.max(1, Math.floor(h / strength));
small.getContext("2d").drawImage(tempCanvas, 0, 0, small.width, small.height);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(small, x, y, w, h);
};
const redrawCanvas = (imageOverride) => {
if (!img && !imageOverride) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imageOverride || img, 0, 0);
appliedAreas.current.forEach(a => applyMosaic(a.x, a.y, a.w, a.h, a.strength));
};
const handleMouseDown = (e) => {
setDragging(true);
setStart(getMousePos(e));
};
const handleMouseMove = (e) => {
if (!dragging || !start) return;
const pos = getMousePos(e);
setCurrent(pos);
redrawCanvas();
const x = Math.min(start.x, pos.x);
const y = Math.min(start.y, pos.y);
const w = Math.abs(start.x - pos.x);
const h = Math.abs(start.y - pos.y);
if (w > 0 && h > 0) {
const ctx = canvasRef.current.getContext("2d");
ctx.fillStyle = "rgba(0,0,0,0.3)";
ctx.fillRect(x, y, w, h);
}
};
const handleMouseUp = (e) => {
if (!dragging || !start) return;
setDragging(false);
const end = getMousePos(e);
const x = Math.min(start.x, end.x);
const y = Math.min(start.y, end.y);
const w = Math.abs(start.x - end.x);
const h = Math.abs(start.y - end.y);
if (w < 5 || h < 5) {
setStart(null);
setCurrent(null);
redrawCanvas();
return;
}
appliedAreas.current.push({ x, y, w, h, strength: mosaicStrength });
applyMosaic(x, y, w, h, mosaicStrength);
setStart(null);
setCurrent(null);
redrawCanvas();
};
return (
<canvas
ref={canvasRef}
style={{ maxWidth: "100%", border: "1px solid #ccc", cursor: "crosshair" }}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
/>
);
});
export default MosaicCanvas;
変更後App.js
上記MosaicCanvasに合わせて変更を加えたApp.jsです。
import React, { useState, useRef } from "react";
import MosaicCanvas from "./MosaicCanvas";
export default function App() {
const [preview, setPreview] = useState(null);
const [resultUrl, setResultUrl] = useState(null);
const [uploading, setUploading] = useState(false);
const [strength, setStrength] = useState(15);
const canvasRef = useRef(null);
const handleFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
setPreview(URL.createObjectURL(file));
setResultUrl(null);
// 自動モザイク適用(バックエンド呼び出し)
const fd = new FormData();
fd.append("image", file);
fd.append("strength", strength);
setUploading(true);
try {
const res = await fetch("http://localhost:5000/api/mosaic", {
method: "POST",
body: fd,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || res.statusText);
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setResultUrl(url);
} catch (err) {
alert("失敗: " + err.message);
} finally {
setUploading(false);
}
};
const handleStrengthChange = (e) => setStrength(Number(e.target.value));
const handleDownload = () => {
const canvas = canvasRef.current?.getCanvas();
if (!canvas) return;
const url = canvas.toDataURL("image/jpeg");
const a = document.createElement("a");
a.href = url;
a.download = "mosaic.jpg";
a.click();
};
return (
<div style={{ maxWidth: 560, margin: "auto", padding: 16, textAlign: "center" }}>
<h1>顔モザイクアプリ</h1>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
style={{ display: "block", margin: "12px auto" }}
disabled={uploading}
/>
<label>
モザイク強さ: {strength}
<input
type="range"
min="1"
max="60"
value={strength}
onChange={handleStrengthChange}
style={{ width: "100%" }}
/>
</label>
{uploading && <p>処理中です...</p>}
{resultUrl && (
<div style={{ marginTop: 18 }}>
<p>加工後</p>
<MosaicCanvas ref={canvasRef} imageUrl={resultUrl} strength={strength} />
<button onClick={() => canvasRef.current?.undoLast()} style={{ marginLeft: 8 }} > ひとつ前に戻す </button>
<button
style={{ marginTop: 8, padding: "6px 12px" }}
onClick={handleDownload}
>
加工後をダウンロード
</button>
</div>
)}
</div>
);
}
5. 再び動作確認
既存機能は問題なく動いてます。
では、画像上でドラックしてモザイクがかけられ、戻せるか確認します。
DLしても手動追加分が適応されてます。
まとめ
今回はGPTの出力をもとに、半自動モザイクアプリを作成してみました。
とてつもなくシンプルな動作ですが、ほぼGPTに聞くだけでアプリが作れてしまいました。
私は日常的にPythonを使用していないので、Flask部分を作成してる最中は「❓」ばかり浮かんでいました
そんな未経験の人間でもアプリが作成できる時代に感動しつつも、AIが書いたコードを100%信用するのはリスクの匂いがしました。
業務などで今回のようなバイブコーディングをする場合は、最終的にAIが書いたコードを人間が細かくレビューする必要があると感じました。
使い方に気をつけてAIを活用していこうと改めて思うことができた制作でした!
※本記事内の画像は全てphotoACの物を使用しています。