セグメンテーション
CV(コンピュータビジョン)におけるセグメンテーションとは、画像をピクセル単位で意味のある領域(セグメント)に分割し、画像内のオブジェクトや領域を詳細に理解する技術です。具体的には、各ピクセルが「車」「歩行者」「空」などのクラスに属するかを分類します。
MediaPipeは顔のランドマーク、全身のスケルトン、手指など計測できて便利です。
そしてセマンティック セグメンテーションもできちゃいます。
スカウター風の表現
p5.jsで動かします。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- PLEASE NO CHANGES BELOW THIS LINE (UNTIL I SAY SO) -->
<script language="javascript" type="text/javascript" src="libraries/p5.min.js"></script>
<script language="javascript" type="text/javascript" src="scauter.js"></script>
<!-- OK, YOU CAN MAKE CHANGES BELOW THIS LINE AGAIN -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/selfie_segmentation.js"></script>
<style>
body {
padding: 0;
margin: 0;
}
</style>
</head>
scauter.js
let video;
let selfieSegmentation;
// MediaPipeから受け取るためのオフスクリーンcanvas
let segCanvas, segCtx;
// p5.js側で表示する用の画像
let segImg;
let power = 530000;
function setup() {
// Retina対策(ピクセル数と配列のズレ防止)
pixelDensity(1);
// 左: 640x480(カメラ) + 右: 640x480(マスク) = 1280x480
createCanvas(1280, 480);
// カメラ
video = createCapture(VIDEO, () => {
initSelfieSegmentation();
});
video.size(640, 480);
video.hide();
// segmentationMask を受けるためのDOMキャンバス
segCanvas = document.createElement('canvas');
segCanvas.width = 640;
segCanvas.height = 480;
segCtx = segCanvas.getContext('2d');
// p5側で表示する画像バッファ
segImg = createImage(640, 480);
}
function initSelfieSegmentation() {
selfieSegmentation = new SelfieSegmentation({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/${file}`;
}
});
selfieSegmentation.setOptions({
modelSelection: 1
});
selfieSegmentation.onResults(onSegResults);
async function loop() {
if (video.elt.readyState >= 2) {
await selfieSegmentation.send({ image: video.elt });
}
requestAnimationFrame(loop);
}
loop();
}
function onSegResults(results) {
// ① segmentationMask をオフスクリーンcanvasに描く
segCtx.clearRect(0, 0, segCanvas.width, segCanvas.height);
segCtx.drawImage(results.segmentationMask, 0, 0,
segCanvas.width, segCanvas.height);
// ② ピクセル取得
let imgData = segCtx.getImageData(0, 0, segCanvas.width, segCanvas.height);
let d = imgData.data; // RGBA配列
// ③ p5.Image に書き込む(緑の人物マスク)
segImg.loadPixels();
for (let i = 0; i < d.length; i += 4) {
let r = d[i]; // マスクの濃度(人物ほど明るい)
let a = d[i + 3]; // アルファ
// 人物とみなす閾値(調整OK)
let isPerson = (r > 150 && a > 150);
segImg.pixels[i + 0] = isPerson ? 0 : 0; // R
segImg.pixels[i + 1] = isPerson ? 255 : 0; // G
segImg.pixels[i + 2] = isPerson ? 0 : 0; // B
segImg.pixels[i + 3] = 255; // A(常に不透明)
}
segImg.updatePixels();
}
function draw() {
background(0);
// 左:カメラ映像
image(video, 0, 0, 640, 480);
fill(255);
noStroke();
//text("Camera + Outline", 10, 20);
// 左:カメラの上に輪郭線(黄色)を重ね描き
drawPersonOutlineOnCamera();
// 右:人物マスク(緑)
image(segImg, 640, 0, 640, 480);
fill(255);
noStroke();
text("Person area (green)", 650, 20);
// 全体を緑フィルタ
fill(0,255,0,80);
rect(0,0,640,480);
//戦闘力
fill(200,255,0);
power++;
drawNumber(power, 10, 10);
}
// ==== 輪郭線描画 ====
function drawPersonOutlineOnCamera() {
if (!segImg) return;
segImg.loadPixels();
let w = segImg.width;
let h = segImg.height;
let thickness = 5; // 黄色線の太さ
noStroke();
fill(255, 255, 0); // 黄色
for (let y = 1; y < h - 1; y++) {
for (let x = 1; x < w - 1; x++) {
let idx = 4 * (y * w + x);
let g = segImg.pixels[idx + 1]; // Gチャンネル(人物なら255)
let isPerson = g > 128;
if (!isPerson) continue;
// 4近傍のどれかが背景ならエッジ
let idxL = 4 * (y * w + (x - 1));
let idxR = 4 * (y * w + (x + 1));
let idxU = 4 * ((y - 1) * w + x);
let idxD = 4 * ((y + 1) * w + x);
let gL = segImg.pixels[idxL + 1];
let gR = segImg.pixels[idxR + 1];
let gU = segImg.pixels[idxU + 1];
let gD = segImg.pixels[idxD + 1];
let edge =
gL < 128 || gR < 128 ||
gU < 128 || gD < 128;
if (edge) {
// 小さな四角を描いて太さ表現
rect(x - thickness / 2, y - thickness / 2, thickness, thickness);
}
}
}
}
const FONT = [
[1,1,1,0,0,1,0,0,0,1,0,0,1,1,0,0,1,0,1,0,1,1,1,0,1,0,0,0,1,1,1,0,1,1,1,0,1,1,1,0],
[1,0,1,0,1,1,0,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,1,0,1,0,1,0],
[1,0,1,0,0,1,0,0,0,0,1,0,0,1,0,0,1,1,1,0,1,1,1,0,1,1,1,0,0,1,0,0,1,1,1,0,1,1,1,0],
[1,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,1,0,0,1,0,1,0,0,0,1,0],
[1,1,1,0,0,1,0,0,1,1,1,0,1,1,0,0,0,0,1,0,1,1,1,0,1,1,1,0,0,1,0,0,1,1,1,0,0,0,1,0]
];
// 数字1桁を描画
function drawDigit(d, x0, y0) {
// d は 0〜9 の整数
// x0, y0 は描画開始位置(左上)
// dotSize は1ドットのピクセルサイズ
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 3; col++) {
let v = FONT[row][d * 4 + col];
if (v === 1) {
let x = x0 + col * 8;
let y = y0 + row * 8;
rect(x, y, 8, 8);
}
}
}
}
// 数字列を横に並べて描画
function drawNumber(value, x, y) {
let s = str(value);
for (let i = 0; i < s.length; i++) {
let ch = s[i];
let d = int(ch);
let xDigit = x + i * 4 * 8;
drawDigit(d, xDigit, y);
}
}