Cloud Vision API を使用して写真に映る顔をマスキングする
この記事はCureApp Advent Calendar 2024 12日の記事です。
はじめに
社会人サークルで遊んでいると、 SNS による広報活動の一端を担うことになりました。
活動内容を伝えるには遊んでいる風景を載せることが効果的なのですが、個人情報の観点から顔写真をそのままアップロードすることはできません。
最近では活動頻度も増えて画像を手動編集するのも面倒なので、スクリプト化することにしました。
以下の通り、顔を絵文字でマスキングできます。
Before/After | 画像 |
---|---|
Before | |
After |
※参考
画像はマイクロソフトデザイナーを使用し、集合写真として生成しました。
注意点
Cloud Vision API を利用するために Google Cloud で課金設定するため、クレジットカードが必要です。
MacBook 以外の場合、マスキング用の絵文字として Apple Color Emoji を使用するため、フォント設定が必要です。
環境
- Node.js
- 20.14.0
- Yarn
- 4.1.1
- エディタ
- Cursor
リポジトリのセットアップ
作業用のリポジトリを作成します。
mkdir masking-face-script
cd masking-face-script
yarn init
package.json
ファイルを編集します。
{
"name": "masking-face-script",
"version": "1.0.0",
"license": "UNLICENSED",
"private": true,
"packageManager": "yarn@4.1.1"
}
TypeScript をインストールします。
yarn add typescript -D
TypeScriptの設定ファイル(tsconfig.json)を生成します。
yarn tsc --init
tsconfig.json
を編集します。
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
スクリプト実行ファイル作成
スクリプトファイルを実行可能か確認します。
初めに、TypeScript ファイルをそのまま実行したいので、 tsx をインストールします。
yarn add tsx -D
スクリプト実行用ファイルを作成します。
mkdir src
touch src/index.ts
index.ts に以下を追記します。
console.log("Hello, world!");
yarn dev
を実行し、以下の通り出力したら確認完了です。
Hello, world!
Cloud Vision API の認証情報を取得
Google Cloud でプロジェクトを作成します。
プロジェクト名は、任意の値を入力してください。
課金設定します。
(画像は省略)
Cloud Vision API を有効にします。
「認証情報の作成」ボタンを押下します。
「API とサービス」に遷移します。
アプリケーションデータを選択し、次へボタンを押下します。
サービスアカウント名に適当な値を入力し、完了ボタンを押下します。
作成完了すると、サービスアカウントの項目に追加されたことが確認できます。
サービスアカウントを編集する画面に移動します。
「キーを追加」ボタンを押下し、秘密鍵を JSON で作成します。
作成実行すると、認証情報を自動ダウンロードされます。
「IAM と管理」画面に移動し、アクセスを許可を押下します。
以下の通り入力し、保存ボタンを押下します。
入力項目 | 入力内容 |
---|---|
プリンシパルの追加 > 新しいプリンシパル | 先ほど作成したサービスアカウントを選択する (例: cloud-vision-api@masking-face-script.iam.gserviceaccount.com) |
ロールを割り当てる > ロール | 「Cloud Vision AI サービスエージェント」を選択 |
先ほどダウンロードした JSON をリポジトリのルートフォルダに移動し、google-service-account-key.json にリネームします。
秘匿すべき認証情報が Git の履歴に残らないように、.gitignore に以下を追記します。
google-service-account-key.json
これで Cloud Vision API の機能を使用できる認証情報の取得完了です。
顔の位置を検出
Cloud Vision AI 用のクライアントライブラリをインストールします。
yarn add @google-cloud/vision
cloud-vision-api.ts ファイルを作成し、以下を追記します。
import { ImageAnnotatorClient } from "@google-cloud/vision";
import { google } from "@google-cloud/vision/build/protos/protos";
const keyFilename = "./google-service-account-key.json";
const visionApiClient = new ImageAnnotatorClient({ keyFilename });
/**
* 画像から顔を検出し、顔の座標情報を返す関数
* @param imagePath - 解析する画像ファイルのパス
* @returns 検出された顔の頂点座標の配列(各顔につき4つの頂点座標を持つ)
*/
async function detectFacesFromImage(
imagePath: string
): Promise<google.cloud.vision.v1.IVertex[][]> {
try {
/*
* Cloud Vision AI で顔検出 API を実行する。
* 顔検出結果のデフォルト設定が上限10個なため、上限100個に設定する。
* https://cloud.google.com/vision/docs/detecting-faces?hl=ja#vision_face_detection_gcs-nodejs
*/
const [result] = await visionApiClient.faceDetection({
image: {
source: { filename: imagePath },
},
features: [
{
maxResults: 100,
type: protos.google.cloud.vision.v1.Feature.Type.FACE_DETECTION,
},
],
});
const faceAnnotations = result.faceAnnotations;
// 顔が検出されなかった場合はエラーを投げる。
if (!faceAnnotations || faceAnnotations.length === 0) {
throw new Error("No faces detected");
}
/*
* 検出された各顔のboundingPoly(境界ボックス)の頂点座標を抽出する。
* IVertexは{x: number, y: number}の形式で座標を表しており、
* [IVertex, IVertex, IVertex, IVertex]、4つの頂点で顔の位置を示す。
*/
const faces = faceAnnotations.reduce<google.cloud.vision.v1.IVertex[][]>(
(faceVertices, faceAnnotation) => {
// 頂点座標が不完全な場合(座標値が欠落している場合)はスキップする。
if (
!faceAnnotation.boundingPoly?.vertices ||
faceAnnotation.boundingPoly.vertices.some(
(vertice) => !vertice.x || !vertice.y
)
)
return faceVertices;
// 有効な頂点座標を持つ顔の情報を配列に追加する。
return [...faceVertices, faceAnnotation.boundingPoly.vertices];
},
[]
);
return faces;
} catch (error) {
console.error("Error:", error);
throw new Error("Failed to detect faces from image");
}
}
export { detectFacesFromImage };
顔をマスキング
顔のマスキングには絵文字を使用します。
Node.js 用の HTML Canvas 描画 API のブラウザレス実装である skia-canvas をインストールします。
yarn add skia-canvas
mask-face-by-emoji.ts ファイルを作成し、以下を追記します。
import { Canvas, loadImage } from "skia-canvas";
import fs from "fs";
import { google } from "@google-cloud/vision/build/protos/protos";
// 顔マスキング用の絵文字
const emojis = ["😊", "😎", "😍", "🤔", "😄"];
async function maskFacesByEmojis({
inputImagePath,
faces,
}: {
inputImagePath: string;
faces: google.cloud.vision.v1.IVertex[][];
}) {
try {
if (!faces || faces.length === 0) {
console.log("No faces detected");
return;
}
const image = await loadImage(inputImagePath);
// マスキング後も元画像と同じサイズを維持するため、同じ幅・高さでCanvasを作成する。
const canvas = new Canvas(image.width, image.height);
const ctx = canvas.getContext("2d");
// 元の画像を描画する。
ctx.drawImage(image, 0, 0, image.width, image.height);
// 顔の位置に対して絵文字でマスキングする。
faces.forEach((face, index) => {
const { x, y, faceWidth, faceHeight } = extractFaceLocation(face);
const emoji = emojis[index % emojis.length];
ctx.font = `${faceWidth}px "Apple Color Emoji"`;
ctx.fillStyle = "#000000";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(emoji, x + faceWidth / 2, y + faceHeight / 2);
});
const buffer = await canvas.toBuffer("jpeg");
fs.writeFileSync(`${inputImagePath.split(".")[0]}-masked.jpg`, buffer);
} catch (error) {
console.error("Error processing image:", error);
}
}
function extractFaceLocation(face: google.cloud.vision.v1.IVertex[]): {
x: number;
y: number;
faceWidth: number;
faceHeight: number;
} {
/*
* Vision APIから返される顔の座標から、描画に必要な情報を抽出します。
* - X, Y座標の最小値が顔の左上の起点
* - 幅と高さは、それぞれX, Y座標の最大値と最小値の差分から計算
*/
const x = Math.min(...face.map((v) => v.x || 0));
const y = Math.min(...face.map((v) => v.y || 0));
const faceWidth = Math.max(...face.map((v) => v.x || 0)) - x;
const faceHeight = Math.max(...face.map((v) => v.y || 0)) - y;
return {
x,
y,
faceWidth,
faceHeight,
};
}
export { maskFacesByEmojis };
index.ts ファイルに以下を上書きします。
import { detectFacesFromImage } from "./cloud-vision-api";
import { maskFacesByEmojis } from "./mask-face-by-emoji";
const main = async (inputImagePath: string) => {
const faces = await detectFacesFromImage(inputImagePath);
maskFacesByEmojis({ inputImagePath, faces });
};
const validateArgs = () => {
const inputImagePath = process.argv[2];
if (!inputImagePath) {
console.error("Error: 画像ファイルのパスを指定してください");
process.exit(1);
}
return inputImagePath;
};
main(validateArgs());
動作確認
画像ファイル sample.jpg をリポジトリのルートフォルダに移動し、以下のコマンドを実行します。
yarn dev sample.jpg
まとめ
実装コードは以下の通りです。
Google Cloud のプロジェクトについては、不要ならこのタイミングで削除しておくと良いでしょう。
残しておいても万が一があるので。
トラブルシュート
Cannot find module ...
のエラーが発生
TypeScript で Yarn が v4.1.1 の場合、 TypeScript を導入してもライブラリの型定義が見つからないエラーが発生することがあります。
Yarn の問題なため、Editor SDKs | Yarn を参考に設定します。
エディタが Cursor または VSCode の場合、以下のコマンドを実行します。
yarn dlx @yarnpkg/sdks vscode
.yarnrc.yml
ファイルを作成し、以下を追記します。
nodeLinker: node-modules
yarn install を実行後、 node_modules フォルダが作成されると成功です。
yarn install
Discussion