0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

p5.jsとMediaPipe(image segmentation)でスカウター風の表現

Posted at

セグメンテーション

 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);
  }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?