OpenCVでボールの軌道描画

前回まで、OpenCVでボールを認識させるプログラムを書きました。

今度はボールの軌道を描画します。

環境・バージョン

Windows10
Python 3.6.1
OpenCV 3.3.1

ボール軌道に点を打つ

先に出力結果です。

赤いボールが通った道に、ピンクの点が描かれます。
(素材動画のコマ落ちで、点が一つ抜けています。)

なお、マニアックですが・・・この動画は、ボッチャの「クッションボール」という技の軌道を表したものです。

他のボールに当てて軌道を変え、ターゲットの白いボールに近づけています。

ボール軌道描画のコード

毎フレーム、ボールの座標を配列に記録し、forで配列の分だけ円(点)を描画する流れです。(もっと賢い書き方がありそう・・・)

import cv2
import numpy as np

cap = cv2.VideoCapture('xxxxx.mp4') # 任意の動画

# 動画出力設定
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
out = cv2.VideoWriter('output.avi',fourcc, 23.0, (1280,720))

orbit = []

while(1):
    _, frame = cap.read()

    #マスク画像取得
    def getMask(l, u):
        # HSVに変換
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

        lower = np.array(l)
        upper = np.array(u)
        if lower[0] >= 0:
            mask = cv2.inRange(hsv, lower, upper)
        else:
            #赤用(彩度、明度判定は簡略化)
            h = hsv[:, :, 0]
            s = hsv[:, :, 1]
            mask = np.zeros(h.shape, dtype=np.uint8)
            mask[((h < lower[0]*-1) | (h > upper[0])) & (s > lower[1])] = 255

        return cv2.bitwise_and(frame,frame, mask= mask)

    # 輪郭取得
    def getContours(img,t,r,drawOrbit):
        gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
        ret, thresh = cv2.threshold(gray, t, 255, cv2.THRESH_BINARY)
        imgEdge, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

        # 一番大きい輪郭を抽出
        contours.sort(key=cv2.contourArea, reverse=True)
        cnt = contours[0]

        # 円描画
        (x,y), radius = cv2.minEnclosingCircle(cnt)
        center = (int(x),int(y))
        radius = int(radius)

        if radius > r:
            if drawOrbit == 1:
                orbit.append(center)
                for j in orbit:
                    cv2.circle(frame,j,10,(128,128,255),-1) # 軌道描画
            return cv2.circle(frame,center,radius,(0,255,0),2) # 最小外接円描画
        else:
            return frame

    # 白・赤マスク
    res_white = getMask([0,0,100], [180,45,255])
    res_red = getMask([-10,45,30], [170,255,255])

    # 輪郭取得
    getContours(res_white, 45, 75, 0) # (画像, 明度閾値, 最小半径, 軌道描画 0=なし 1=あり)
    contours_frame = getContours(res_red, 45, 75 ,1)

    # 動画書き込み
    out.write(contours_frame)

    # 再生
    cv2.imshow('video',contours_frame)
    k = cv2.waitKey(46) & 0xFF

    #Q で終了
    if k == ord('q'):
        break

out.release()
cv2.destroyAllWindows()

軌道描画の他、動画をAVIで保存するように改修しています。

軌道描画の使い道

ボールの軌道を可視化することで、自分の投球のブレや距離を分析し、技術向上に使えます。

また将棋の「棋譜」のように記録していけば、うまい選手の戦術・戦略を学習できそうです。

初心者のために使うのであれば、戦術・技を教えるときに、可視化されていれば、理解が深まるでしょう。

OpenCVでボールの輪郭描画(複数)

前回やった白いボールの輪郭描画に引き続き、赤いボールも同時に輪郭を描画してみます。

環境・バージョン

Windows10
Python 3.6.1
OpenCV 3.3.1

輪郭描画のコード

前回と処理の流れは変わらないのですが、複数回の処理を関数にまとめるなど、コードは若干整理しています。

ただ私自身、Pythonの構文も学びながら書くレベルなので参考程度に・・・

実行してみると・・・

実際は動画で、しっかりと転がる赤いボールを追尾して認識しています。

赤いボールは途中から転がってくるので、それまで白いボールの赤文字のロゴが誤認されてしまいました。

そこで、半径が75以上のものしかボールと認識しない条件を加えています。

赤を識別する問題

他にも少し問題となったのが、赤いボールの識別です。

H(色相)の範囲指定が、前回のようにはいきません。

赤のHは分断されているからです。

赤を指定するときは、0~10 かつ 170 ~ 180 というような条件式が必要です。

上記コードでは、getMask([最小H,最小S,最小V], [最大H,最大S,最大V])という関数を作り、最小Hに負の値を指定すると、赤の条件式に切り替わります。

赤の条件式は「pythonで赤い物体を認識しよう」を参考とさせて頂きました。

同じ色のボールが複数あったときは?

赤と白、一つずつのボール認識は、比較的簡単だと思います。

これが赤6つとかになると、ボール同士がくっついて輪郭が一体化し、誤認しそうです。

くっついたボール、陰になって一部見えないボールなど、イレギュラーな認識については、haar-like機械学習と組み合わせで、地道に認識精度を上げていくのでしょう。

 

地道なことはせず、次回はボールの軌道描画を行います。

OpenCVでボールの輪郭描画

前回の色抽出に引き続き、OpenCVを使って輪郭描画を行います。

動画の中からボールを認識して、円で囲います。

環境・バージョン

Windows10
Python 3.6.1
OpenCV 3.3.1

輪郭描画の流れ

独学&初心者で正しいか分かりませんが・・・輪郭描画の流れを書きます。
コードは最後に掲載。

前回、白色の抽出をした画像を元に加工します。

白と黒の2色にしてしまいます。

このままでは、余計なノイズも検出されてしまいます。

対策として、一番大きい領域だけ検出するようにします。

そして、検出した領域の輪郭に接する円を描画して、元画像と合成。

うまく、囲ってくれました。

輪郭描画コード

import cv2
import numpy as np

cap = cv2.VideoCapture('xxxxx.mp4') # 任意の動画
while(1):
    _, frame = cap.read()
    # HSVに変換
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

    # 白のHSV範囲
    lower_white = np.array([0,0,100])
    upper_white = np.array([180,45,255])

    # 白以外にマスク
    mask_white = cv2.inRange(hsv, lower_white, upper_white)
    res_white = cv2.bitwise_and(frame,frame, mask= mask_white)

    # 輪郭抽出
    gray = cv2.cvtColor(res_white, cv2.COLOR_RGB2GRAY)
    ret, thresh = cv2.threshold(gray, 45, 255, cv2.THRESH_BINARY)
    imgEdge, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    # 一番大きい輪郭を抽出
    contours.sort(key=cv2.contourArea, reverse=True)
    cnt = contours[0]

    # 最小外接円を描く
    (x,y), radius = cv2.minEnclosingCircle(cnt)
    center = (int(x),int(y))
    radius = int(radius)
    img = cv2.circle(frame,center,radius,(0,255,0),2)

    # 再生
    cv2.imshow('video',img)
    k = cv2.waitKey(25) & 0xFF

    #Q で終了
    if k == ord('q'):
        break

cv2.destroyAllWindows()

今回も「OpenCV-Python チュートリアル文書」を参考とさせて頂きました。

ありがとうございました。

OpenCVで色抽出(白)

今回は久しぶりにOpenCVを使い、色の抽出を行います。

環境・バージョン

Windows10
Python 3.6.1
OpenCV 3.3.1

素材画像の用意

白、赤、青のボールを使った頭脳戦「ボッチャ」の動画を素材とします。
Webカメラでリアルタイムでも問題ありません。

コートが緑だと、色抽出が楽です。

白を抽出してみよう

赤、青の方が簡単そうですが、いきなり白の抽出を行います。

白は色相がないので、色抽出に不向きなような気がしますが・・・

先に結果を。

十分です!

HSVに変換して評価

色の抽出には、まず色情報を分かりやすいように、色相(H)、彩度(S)、明度(V)に分けて評価できるよう、HSV空間に変換します。

HSVはデザイナー寄りの人であれば、理解しやすい分野かと思います。

白の場合、色相は関係ないので0度~360度、すべてを対象としましょう。
※OpenCV3だと、色相を0~180で指定するみたいです。

彩度と明度は、0~255で指定。
白だと、彩度は低めで45以下。明度は影も考慮すると255~100くらい?

・・・試しながら調整したほうが早いです。

白抽出コード

import cv2
import numpy as np

cap = cv2.VideoCapture('xxxxx.mp4') # 任意の動画
while(1):
    _, frame = cap.read()
    # HSVに変換
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

    # 白のHSV範囲
    lower_white = np.array([0,0,100])
    upper_white = np.array([180,45,255])

    # 白以外にマスク
    mask_white = cv2.inRange(hsv, lower_white, upper_white)
    res_white = cv2.bitwise_and(frame,frame, mask= mask_white)

    # 再生
    cv2.imshow('res',res_white)
    k = cv2.waitKey(25) & 0xFF # 数字が小さいほど再生が速くなる

    #Q で終了
    if k == ord('q'):
        break

cv2.destroyAllWindows()

これで、動画から白を抽出できます。

HSVの値を変えれば、青でも赤でも抽出可能です。

 

意外と色抽出は簡単なので、クロマキー合成の自作とかも、気軽に楽しめそうです。

引き続き次回も同じ動画素材を使い、OpenCVで輪郭抽出します。

three.jsでボール軌道描画

Webで3Dを簡単に扱えるようになる「three.js」を使い、ボール軌道の描画に挑戦してみます。

なお私は、ボールの運動方程式をまったく知りません。

運動方程式を調べよう

ボールの運動方程式は、ネットで簡単に見つけられます。

とりあえず、検索1番目に出てきた「いろいろな運動」というPDFを開いてみました。

 

・・・よし、見なかったことにしよう。

イージングアニメーションを活用

今持っているWeb知識のみでどうにかします。

ボールの動きは、jQueryアニメーションのイージング「Easing function」を利用できるかもしれません。

バウンドは「easeOutBounce」、減速は「easeOutCubic」を使い、適当な<span>にアニメーションを割り当ててみます。

<span id="ballX"><span id="ballY" class="ball"></span><span id="ballZ" class="ball"></span></span>
var $ballX = $("#ballX"),
    $ballY = $("#ballY"),
    $ballZ = $("#ballZ");

$ballX.animate({
 'left': '500px'
},{
 'duration': 3000,
 'easing': 'easeOutCubic'
});
$ballY.animate({
 'top': '-200px'
},{
 'duration': 3000,
 'easing': 'easeOutCubic'
});
$ballZ.animate({
 'top': '100px'
},{
 'duration': 2000,
 'easing': 'easeOutBounce'
});

コートを上から見て、ボールの横位置がx、縦位置がy、高さがzとなる想定です。

あとは60ミリ秒おきに各spanの座標を配列に変換するスクリプトを実行すると・・・

[{"x":-368.33,"y":206.92,"z":107.49},{"x":-336.53,"y":194.17,"z":105.6},{"x":-306.63,"y":182.19,"z":102.08},{"x":-284.88,"y":173.47,"z":98.32},{"x":-256.94,"y":162.27,"z":91.8},{"x":-236.98,"y":154.27,"z":85.82},{"x":-211,"y":143.86,"z":76.17},{"x":-187.42,"y":134.39,"z":65.33},{"x":-170.08,"y":127.45,"z":55.89},{"x":-147.94,"y":118.56,"z":41.77},{"x":-132.19,"y":112.25,"z":30.13},{"x":-112.11,"y":104.2,"z":13.03},{"x":-93.44,"y":96.72,"z":13.82},{"x":-80.02,"y":91.33,"z":19.33},{"x":-62.98,"y":84.5,"z":25.25},{"x":-50.95,"y":79.67,"z":28.57},{"x":-35.72,"y":73.56,"z":31.5},{"x":-21.69,"y":67.92,"z":32.75},{"x":-11.69,"y":63.92,"z":32.6},{"x":0.91,"y":58.86,"z":30.91},{"x":9.72,"y":55.33,"z":28.55},{"x":20.77,"y":50.89,"z":23.89},{"x":30.83,"y":46.86,"z":17.66},{"x":37.92,"y":44,"z":11.83},{"x":46.73,"y":40.45,"z":10.07},{"x":52.83,"y":38.02,"z":12.33},{"x":60.36,"y":34.98,"z":13.91},{"x":67.11,"y":32.28,"z":13.78},{"x":71.88,"y":30.36,"z":12.57},{"x":77.52,"y":28.09,"z":9.53},{"x":81.39,"y":26.55,"z":8.49},{"x":86.08,"y":24.66,"z":9.36},{"x":90.17,"y":23.02,"z":8.58},{"x":92.94,"y":21.89,"z":7.8},{"x":96.22,"y":20.58,"z":7.8},{"x":98.38,"y":19.7,"z":7.8},{"x":100.89,"y":18.69,"z":7.8},{"x":103,"y":17.84,"z":7.8},{"x":104.34,"y":17.3,"z":7.8},{"x":105.88,"y":16.69,"z":7.8},{"x":106.52,"y":16.42,"z":7.8},{"x":107.81,"y":15.89,"z":7.8},{"x":108.59,"y":15.58,"z":7.8},{"x":109.03,"y":15.41,"z":7.8},{"x":109.47,"y":15.22,"z":7.8},{"x":109.69,"y":15.14,"z":7.8},{"x":109.86,"y":15.06,"z":7.8},{"x":109.95,"y":15.02,"z":7.8},{"x":109.98,"y":15.02,"z":7.8},{"x":110,"y":15,"z":7.8},{"x":110,"y":15,"z":7.8}]

三次元の座標配列が書き出されました。

three.jsでボール軌道を描画

座標データさえできれば、ボールの軌道を描画していくのは簡単です。

60ミリ秒おきに、半透明のボールを描画していきます。

window.addEventListener('DOMContentLoaded', init);

function init() {
  "use strict";
  var canvasID = "#court",
      width = 1280,
      height = 720,
      ballSize = 8.6, //ボール直径
      ballDents = 0.8, //ボールのへこみ
      balls = {};
  
  var orbit = [{"x":-368.33,"y":206.92,"z":107.49},{"x":-336.53,"y":194.17,"z":105.6},{"x":-306.63,"y":182.19,"z":102.08},{"x":-284.88,"y":173.47,"z":98.32},{"x":-256.94,"y":162.27,"z":91.8},{"x":-236.98,"y":154.27,"z":85.82},{"x":-211,"y":143.86,"z":76.17},{"x":-187.42,"y":134.39,"z":65.33},{"x":-170.08,"y":127.45,"z":55.89},{"x":-147.94,"y":118.56,"z":41.77},{"x":-132.19,"y":112.25,"z":30.13},{"x":-112.11,"y":104.2,"z":13.03},{"x":-93.44,"y":96.72,"z":13.82},{"x":-80.02,"y":91.33,"z":19.33},{"x":-62.98,"y":84.5,"z":25.25},{"x":-50.95,"y":79.67,"z":28.57},{"x":-35.72,"y":73.56,"z":31.5},{"x":-21.69,"y":67.92,"z":32.75},{"x":-11.69,"y":63.92,"z":32.6},{"x":0.91,"y":58.86,"z":30.91},{"x":9.72,"y":55.33,"z":28.55},{"x":20.77,"y":50.89,"z":23.89},{"x":30.83,"y":46.86,"z":17.66},{"x":37.92,"y":44,"z":11.83},{"x":46.73,"y":40.45,"z":10.07},{"x":52.83,"y":38.02,"z":12.33},{"x":60.36,"y":34.98,"z":13.91},{"x":67.11,"y":32.28,"z":13.78},{"x":71.88,"y":30.36,"z":12.57},{"x":77.52,"y":28.09,"z":9.53},{"x":81.39,"y":26.55,"z":8.49},{"x":86.08,"y":24.66,"z":9.36},{"x":90.17,"y":23.02,"z":8.58},{"x":92.94,"y":21.89,"z":7.8},{"x":96.22,"y":20.58,"z":7.8},{"x":98.38,"y":19.7,"z":7.8},{"x":100.89,"y":18.69,"z":7.8},{"x":103,"y":17.84,"z":7.8},{"x":104.34,"y":17.3,"z":7.8},{"x":105.88,"y":16.69,"z":7.8},{"x":106.52,"y":16.42,"z":7.8},{"x":107.81,"y":15.89,"z":7.8},{"x":108.59,"y":15.58,"z":7.8},{"x":109.03,"y":15.41,"z":7.8},{"x":109.47,"y":15.22,"z":7.8},{"x":109.69,"y":15.14,"z":7.8},{"x":109.86,"y":15.06,"z":7.8},{"x":109.95,"y":15.02,"z":7.8},{"x":109.98,"y":15.02,"z":7.8},{"x":110,"y":15,"z":7.8},{"x":110,"y":15,"z":7.8}];
  
  function createBall(x,y,z,color,id){
    var colorData;
    id = id || "";
    id = color + id;
    if(color === "red"){
      colorData = 0xb71c1c;  
    }
    else if(color === "blue"){
      colorData = 0x0D47A1;
    }
    else{
      colorData = 0xFFFFFF;
    }
    var geometry = new THREE.SphereGeometry(ballSize, 24, 24);
    var material = new THREE.MeshLambertMaterial({color: colorData});
    balls[id] = new THREE.Mesh(geometry, material);
    balls[id].position.set(x, y, z);
    scene.add(balls[id]);
  }
  
  function createOrbitBall(x,y,z,color){
    var colorData;
    if(color === "red"){
      colorData = 0xb71c1c;  
    }
    else if(color === "blue"){
      colorData = 0x0D47A1;
    }
    else{
      colorData = 0xFFFFFF;
    }
    var geometry = new THREE.SphereGeometry(ballSize, 12, 12);
    var material = new THREE.MeshLambertMaterial({
      color: colorData, wireframe: true, 
      transparent: true,
      opacity: 0.1}
    );
    var orbitBall = new THREE.Mesh(geometry, material);
    orbitBall.position.set(x, y, z);
    scene.add(orbitBall);
  }
  
  function createCourt(){
    var loader = new THREE.TextureLoader();
    loader.load("common/img/court.png", function(texture){
      readiness(texture);
    });
    function readiness(texture){
      var geometry = new THREE.PlaneGeometry(1258, 608);
      var material = new THREE.MeshPhongMaterial({color: 0xFFFFFF,map: texture});
      var ground = new THREE.Mesh(geometry, material);
      scene.add(ground);
      camera.lookAt(ground.position);
    }
  }

  // レンダラー作成
  var renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector(canvasID),
    antialias: true
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);

  // シーン作成
  var scene = new THREE.Scene();

  // カメラ作成
  var camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
  camera.position.set(0, -700, 500);

  // ボール作成
  var z = ballSize - ballDents;
  createBall(-440, 215, z, "jack");
  createBall(-390, 215, z, "red", 1);
  createBall(-415, 215, z, "red", 2);
  createBall(-390, 20, z, "red", 3);
  createBall(-415, 20, z, "red", 4);
  createBall(-390, -175, z, "red", 5);
  createBall(-415, -175, z, "red", 6);
  createBall(-390, 115, z, "blue", 1);
  createBall(-415, 115, z, "blue", 2);
  createBall(-390, -80, z, "blue", 3);
  createBall(-415, -80, z, "blue", 4);
  createBall(-390, -275, z, "blue", 5);
  createBall(-415, -275, z, "blue", 6);
  createCourt();
  
  // 平行光源
  var light = new THREE.DirectionalLight(0xFFFFFF);
  light.intensity = 2;
  light.position.set(-2, -1, 10);
  scene.add(light);

  // 環境光源
  var ambient = new THREE.AmbientLight(0x222222);
  ambient.intensity = 4;
  scene.add(ambient);

  // アニメーション実行
  var i = 0;
  var animOrbit = function(){
    
    if(i < orbit.length){
      balls.jack.position.set(orbit[i].x, orbit[i].y, orbit[i].z);
      createOrbitBall(orbit[i].x, orbit[i].y, orbit[i].z,"jack");
      renderer.render(scene, camera);
      i++;
    }
    else{
      clearInterval(setOrbit);
    }
  };

  var setOrbit = window.setInterval(animOrbit, 60);
}

実行してみると・・・

アニメーションしながら、自然なボールの軌道を描画することができました。

さらにカメラをマウスで操作できるように改修し、いろいろなアングルで見られるようにしました。

最終的に、現実のコートで複数カメラで撮影した動画を、OpenCVで三次元座標に変換し、ボールの軌道を可視化します。

なので、ボールの軌道を自作する必要はなかったのですが・・・試しです。

Webで3D three.jsを学ぶ

枯れた技術が大好きです。

今回は2011年に話題のピーク(Googleトレンドで調査)だった、「WebGL」を使い、3Dに挑戦しようと思います。

three.jsという ありがたいライブラリ

WebGLを簡単に利用できる「three.js」というライブラリがあります。
これを使い、3DCGを制作します。

世界で長期的に愛用されていて、情報もたくさんあるので学習コストは低いでしょう。

3Dを使う理由

Web系の仕事をしている人にとって、3Dを使う場面はあまりないと思います。
(WebVRとか出てきて、そんなことないかも)

単純にWebサイトのUIデザインを3Dにすると、操作性が落ちます。
マウスにしろタップにしろ、入力操作はxyの2Dですし、認識としても、立体より平面の方が理解しやすいからです。

さらに開発工数も上がりますし、なかなか3Dの使いどころが難しいです。

 

私の場合、それでも3Dに手を出したのは、明確な目的があったからです。

パラリンピック正式種目に「ボッチャ」という球技があるのですが、3次元で繰り広げられる頭脳戦を可視化できれば・・・(以下略)

three.jsを使うための必要知識

球や立方体を配置して、カメラと光源を調整するくらいなら、そこまで知識はいりません。

  • HTML
  • JavaScript
  • 3Dモデリング

どれも初級以上のレベルでなんとかなるような気がします。

3Dモデリングは例え未経験でも、カメラ、光源を調整しながら、理解していけるものだと思います。

three.jsの利用準備

three.jsを利用するための準備は非常に簡単です。

任意の場所にHTMLを作成します。

HTML作成

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>three.jsを学ぶ</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js"></script>
<script src="sample.js"></script>
</head>
<body>
 <canvas id="sample"></canvas>
</body>
</html>

6行目で(CDNから)three.jsを読み込むだけです。

7行目のsample.jsで書いたコードを、10行目のcanvasに描画する流れとなります。

sample.js作成

ボールを作ってみます。

window.addEventListener('DOMContentLoaded', init);

function init() {
  "use strict";
  var canvasID = "#sample",
      width = 1280,
      height = 720;
  
  // シーン作成
  var scene = new THREE.Scene();

  // ボール作成
  var geometry = new THREE.SphereGeometry(100, 24, 24);
  var material = new THREE.MeshLambertMaterial({color: 0xFFFFFF});
  var ball = new THREE.Mesh(geometry, material);
  scene.add(ball);

  // カメラ作成
  var camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
  camera.position.set(0, 0, 1000);
  
  // 光源作成
  var light = new THREE.DirectionalLight(0xFFFFFF);
  light.position.set(-1, 1, 1);
  scene.add(light);
  
  // レンダリング(描画)
  var renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector(canvasID),
    antialias: true
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);
  renderer.render(scene, camera);
}

3D空間(シーン)を作って、ボールを置いて、カメラ向けて、照明つけて、レンダリングする流れです。

レンダリング結果

闇に浮かぶ球体ができました。

基本が分かれば、冒頭のボッチャコートも2時間くらいで出来上がります。

 

次回はボールの軌道の描画です。