射影変換とスポーツアナリティクス

に公開

はじめまして。データアナリティクスラボ株式会社、データソリューション事業部の伊藤です。

ラボチームも支援しているスポーツ案件から射影変換技術の紹介です。

本記事はDAL Tech Blog Advent Calendar 2025として投稿しました。全ての記事は以下からご確認いただけます。

https://adventar.org/calendars/12288

はじめに

生活の中で利用する様々な製品は、長方形や正方形など綺麗な図形をしています。
人の目でそれらを見た時、長方形など綺麗な図形として認識していることがほとんどですが、対象物の遠近感も含めてそのまま2次元上に描画すると綺麗な図形の形はしていません。
つまり、写真で見てみると、「長方形だと思っていたノートは、写真上では台形として描画されている」状態になっています。

具体的には、以下の画像をご覧ください。

※ChatGPTによって生成

当たり前のように、長方形の手帳と認識しているかと思いますが、概ね同じサイズの長方形を上から重ねると以下のようになります。

このように、人間の視覚が捉えている情報は、無意識下で多くの補正がされています。射影変換を活用して、人間が頭の中でイメージしている理論的な形へと変換することができれば、より扱いやすい形で画像を活用することができます。

上記の画像を例に、射影変換を活用すると、

  1. ノートの表紙を真上から見た画像に変換する
  2. 長方形の画像を射影変換して、ノートの表紙に合成する

というような処理をすることができます。

本記事では、この『射影変換』について、数学的な理論とその実装方法(Python)、そして、スポーツアナリティクスにおける実際の活用例を紹介いたします。

射影変換の理論

射影変換とは、変換前の座標(x,y)を、新しい座標(X,Y)に変換する処理のことです。
この変換を特定の行列を用いておこないます。さらに、この特定の行列は変換前と後で4点定まっていると、一意に定まり画像全体に適用することができます。この特定の行列のことを射影変換行列(ホモグラフィ行列)と呼びます。

数式を用いて表現すると以下のようになります。

まず射影変換行列Hです。射影変換行列は以下のように表現します。

H = \begin{bmatrix} h_{11} & h_{12} & h_{13} \\ h_{21} & h_{22} & h_{23} \\ h_{31} & h_{32} & h_{33} \end{bmatrix}

射影変換行列は以上のように定義されます。
この射影変換行列と、元々の座標点(x,y)を含む同次座標(x,y,1)とで行列積を取ることで、以下のような式を得ることができます。

\begin{bmatrix} x' \\ y'\\ w' \end{bmatrix} = \begin{bmatrix} h_{11} & h_{12} & h_{13} \\ h_{21} & h_{22} & h_{23} \\ h_{31} & h_{32} & h_{33} \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}

上記の(x',y',w')は射影変換後の同次座標を表しております。この同次座標を用いて、射影変換後の画像上の座標を得ることができます。
具体的には以下のように計算することで求まります。

X = \frac{x'}{w'}, \quad Y = \frac{y'}{w'}

このようにして求まった(X,Y)が射影変換後の座標となります。
射影変換行列は元々決まっているということはなく、実際に変換する際は変換前後で4点の座標を事前に確認しておく必要があります。
4点の変換前座標と変換後座標を利用することで、射影変換行列を求めることができます。求めた射影変換行列を画像全体に適用することで、変換した後の新たな画像を得ることができるという仕組みになっています。

射影変換の実装

射影変換は、OpenCVとnumpyを使用して実装することができます。サンプルコードは以下の通りです。

import cv2
import numpy as np

# 元画像を読み込み
img = cv2.imread("input.jpg")

# 元画像上の4点(台形の四隅など)
src = np.float32([
    [120, 200],  # 左上
    [520, 180],  # 右上
    [560, 400],  # 右下
    [100, 420]   # 左下
])

# 補正後の画像サイズ
width, height = 400, 300

# 変換後の4点(きれいな長方形の四隅)
dst = np.float32([
    [0,     0],      # 左上
    [width, 0],      # 右上
    [width, height], # 右下
    [0,     height]  # 左下
])

# 射影変換行列を求める
H = cv2.getPerspectiveTransform(src, dst)

# 射影変換を適用
warped = cv2.warpPerspective(img, H, (width, height))

# 保存
cv2.imwrite("output_warped.jpg", warped)

実装上の注意点として、指定する4点の座標が変換前後で対応していることが重要になります。
射影変換をする際には「左上→右上→右下→左下」の順番で指定することが多く、一般的です。画像処理においては、左上を原点として扱いますので、原点から時計回りに座標を指定していると考えれば頭に入ってきやすいと思います。

また変換には、画像上の座標が必要となります。上記の実装コードだと変換前の座標が

[120, 200],  # 左上
[520, 180],  # 右上
[560, 400],  # 右下
[100, 420]   # 左下

となっています。
座標の求め方で実践的かつ柔軟性のある方法(力技)を紹介いたします。
先ほどの手帳の画像に適用します。

from PIL import Image
import matplotlib.pyplot as plt
import numpy as np

# 画像の読み込み
im = Image.open("chatgpt_generate_image.png")
fig, ax = plt.subplots(figsize=(10,10))

ax.set_xlim(-100, 1100)
ax.set_ylim(1100, -100)
plt.xticks(np.arange(-100, 1100, step=100))
plt.yticks(np.arange(-100, 1100, step=100))

ax.grid()
plt.imshow(im)
plt.show()

実行すると以下のようになります。

※画像上にグリッド線を付与

このようにして、変換に用いる座標を力技で特定することができます。
あまり実用的ではないように見受けられるかもしれませんが、固定カメラからの動画(画像)を射影変換する場合、一度変換する座標を特定してしまえばあとは活用できますので、全く使えない方法ともいえないやり方です。ただし、この方法は目視による座標点の特定なので、微調整することが前提となる点にご注意ください。

また、画像は通常『左上』が画像の原点として扱われます。そのためy軸方向の座標のみ直感に反する捉え方をする必要がある点に注意してください。

スポーツアナリティクスへの活用

射影変換の実践的な活用として、スポーツアナリティクスへの活用を紹介いたします。まずは、2016年クラブワールドカップ決勝のメインスタンドからの画像をご覧ください。

筆者撮影

この画像を射影変換して、真上からみた画像に変換いたします。

まず以下のコードでグリッド線を引いて、変換するための座標を見定めていきます。

from PIL import Image
import matplotlib.pyplot as plt
import numpy as np

# 画像の読み込み
im = Image.open("2016_cwc.JPG")
fig, ax = plt.subplots(figsize=(10,10))

ax.set_xlim(-50, 1000)
ax.set_ylim(600, -50)
plt.xticks(np.arange(-50, 1000, step=25), rotation=45)
plt.yticks(np.arange(-50, 600, step=25))

ax.grid(True, alpha=0.75, linestyle='--', color='gray')
plt.imshow(im)
plt.show()

上記の画像から、以下の座標をピッチの四隅とします。

[205, 220],  # 左上
[735, 220],  # 右上
[935, 340],  # 右下
[-40, 322]   # 左下

この座標は、何度も微調整しながら調整する必要があります。画素が荒かったり、はっきりとポイントになる座標がわかりにくい時は、少しずつずらしながら確かめてピッタリするところを探っていきます。

上記の座標を用いて実際にコードを動かすと以下のようになります。

import cv2
import numpy as np

# 元画像を読み込み
img = cv2.imread("IMG_0848.JPG")

# 元画像上の4点(台形の四隅など)
src = np.float32([
    [205, 220],  # 左上
    [735, 220],  # 右上
    [935, 340],  # 右下
    [-40, 322]   # 左下
])

# 補正後の画像サイズ
width, height = 1200, 900

# 変換後の4点(きれいな長方形の四隅)
dst = np.float32([
    [0,     0],      # 左上
    [width, 0],      # 右上
    [width, height], # 右下
    [0,     height]  # 左下
])

# 射影変換行列を求める
H = cv2.getPerspectiveTransform(src, dst)

# 射影変換を適用
warped = cv2.warpPerspective(img, H, (width, height))

# 保存
cv2.imwrite("output_warped.jpg", warped)


射影変換により真上からの視点に変換したサッカーコート

上記の画像のように、そもそも画像に映っていない箇所を表示する画像の中に入れようとすると、黒塗りの形で表現されます。

ラボスポーツチームでもこのような画像(動画)の変換処理を加えることで、撮影した映像を真上から捉えた状態に変換し、スポーツアナリティクスに活用しています。

おわりに

射影変換の理論とPython実装、そしてスポーツアナリティクスへの活用について紹介いたしました。
より良い方法・よりスマートな方法はまだまだあると思っていますが、ラボスポーツチームではこのやり方で実際にスポーツの現場を支援しています。
この取り組みは非常に建設的な関係性でもあるので、今回紹介した方法以外にも、技術ドリブンで様々な支援方法を提案して支援を継続してまいります。その中の活用方法をもっと投稿できるよう日々精進してまいります。

付録

掲載調整中

DAL Tech Blog

Discussion