【Python】OpenCVで特徴点の追跡【メダカの軌跡】

はじめに
タイトルのサムネイル写真は、メダカの動きをOpenCVでコーナー検出し、特徴点の追跡を行なって軌跡を描いたものです。この記事ではそのやり方を解説していきます。
Python上のOpenCVで calcOpticalFlowPyrLK
関数によって特徴点の追跡が可能です。つまりは、動画フレーム内で特徴点が移動した場所を追跡できるようになります。特徴点とは、
コーナー検出
などによって得られる画像内の物体の特徴的な座標になります。
準備
こちらのメダカビオトープの動画素材をOpenCVで加工していきます。

ここではPython3.xとOpenCVを使用しました。
$ pip list | grep opencv
opencv-contrib-python 4.6.0.66
opencv-python 4.5.5.62
OpenCVがまだインストールされてない場合は、pipコマンドでインストールしましょう。numpyも使いますので合わせてインストールします。
$ pip install opencv-python
$ pip install opencv-contrib-python
$ pip install numpy
HSV色空間でメダカのみを抽出する
で行なったような方法で、メダカのみをHSV色空間のフィルタリングで抽出してみます。
カラーピッカーでメダカのカラダのRGB色を取得して、HSV色空間へ変換、特定のHSV色のみを抽出した例です:
import cv2
import numpy as np
import itertools
import colorsys
def rgb2hsv(rgb):
= rgb
r,g,b = colorsys.rgb_to_hsv(r, g, b)
h,s,v return int(h*180), int(s*255), int(v)
def hsv_maks(img, rgb_list=None):
for i, hsv in enumerate([rgb2hsv(rgb) for rgb in rgb_list]):
= hsv
h,s,v = np.array([h-3, s-4, v-4])
lower = np.array([h+3, s+4, v+4])
upper = cv2.inRange(img,lower,upper)
src
if i == 0:
= src
mask_hsv else:
= cv2.addWeighted(src1=src,alpha=1,src2=mask_hsv,beta=1,gamma=0)
mask_hsv
return mask_hsv
def main(inpath, outpath, rgb_list):
# -------------------------------------------------
# setup
# -------------------------------------------------
= cv2.VideoCapture(inpath)
vidcap
= int(vidcap.get(cv2.CAP_PROP_FRAME_WIDTH))
width = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
height = 30 #vidcap.get(cv2.CAP_PROP_FPS)
fps
= cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
fmt = cv2.VideoWriter(outpath, fmt, fps, (width, height)) # ライター作成
writer
= 0
counter
# -------------------------------------------------
# loop
# -------------------------------------------------
while True:
= vidcap.read()
grabbed_frame, frame if frame is None:
break
= cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
img_hsv = hsv_maks(img_hsv, rgb_list=rgb_list)
mask_hsv = cv2.bitwise_and(frame, frame, mask=mask_hsv)
img
'img', img)
cv2.imshow(= cv2.resize(img, (width, height))
img
writer.write(img)
= cv2.waitKey(5)
key if key == 27: # Esc
break
+= 1
counter
vidcap.release()
writer.release()
if __name__ == '__main__':
= '../assets/biotope_short.mov' # 1280 x 720
inpath = '../build/medaka_hsv.mp4'
outpath
= [
rgb_list 156, 134, 70], # 金色メダカ
[144,146,80], # 金色メダカのお腹
[217, 186, 137], # 金色メダカ
[122, 111, 48], # 金色メダカ
[150,120,56], # 金色メダカ
[133,106,42], # 金色メダカ
[137,122,52], # 金色メダカ
[144,145,80], # 金色メダカ
[185,149,77], # 金色メダカ
[130,117,69], # 金色メダカ
[
89, 73, 47], # 黒メダカ
[115, 105, 62], # 黒メダカ
[79, 61, 29], # 黒メダカ
[100, 97, 47], # 黒メダカ
[120, 101, 46], # 黒メダカ
[108, 110, 85], # 黒メダカのお腹
[69,67,31], # 黒メダカ
[87,77,16], # 黒メダカ
[103,104,26], # 黒メダカ
[80,75,43], # 黒メダカ
[81,79,43], # 黒メダカ
[98,87,54], # 黒メダカ
[147,156,156], # 黒メダカのお腹
[133,135,139], # 黒メダカのお腹
[140,133,135], # 黒メダカのお腹
[53,46,12], # 黒メダカ
[78,69,18], # 黒メダカ
[77,85,42], # 黒メダカ
[90,82,40], # 黒メダカ
[
]
main(inpath, outpath, rgb_list)
実行結果が次になります。
少しわかりづらいですが、メダカのカラダの色を捉えることができてます。
hsv_maks()
関数内のlower
と
upper
でHSV色空間の範囲を持たせてます。範囲をゆるめればメダカのカラダの色がもう少しはっきりしますが、今度は葉っぱなど他の物体の色と被ってしまうので狭めに設定しました。
その代わり、メダカのカラダの色をカラーピッカーで取得してリスト化する作業が大変になります。

特徴点の検出(コーナー検出)
Shi-Tomasiコーナー検出のgoodFeaturesToTrack()
関数を使ってコーナー検出した例です:
import cv2
import numpy as np
import itertools
import colorsys
def rgb2hsv(rgb):
= rgb
r,g,b = colorsys.rgb_to_hsv(r, g, b)
h,s,v return int(h*180), int(s*255), int(v)
def hsv_maks(img, rgb_list=None):
for i, hsv in enumerate([rgb2hsv(rgb) for rgb in rgb_list]):
= hsv
h,s,v = np.array([h-3, s-4, v-4])
lower = np.array([h+3, s+4, v+4])
upper = cv2.inRange(img,lower,upper)
src
if i == 0:
= src
mask_hsv else:
= cv2.addWeighted(src1=src,alpha=1,src2=mask_hsv,beta=1,gamma=0)
mask_hsv
return mask_hsv
def fill(size, color):
= size
w, h = np.zeros((h, w, 3), dtype="uint8")
canvas 0,0), (w,h), color, -1)
cv2.rectangle(canvas, (return canvas
def is_nearby(pt1, pt2, maxDistance, minDistance):
= (pt1[0] - pt2[0])**2 + (pt1[1] - pt2[1])**2
r if r < maxDistance**2 and r > minDistance**2:
return True
return False
def clamp(n, smallest, largest):
return max(smallest, min(n, largest))
def main(inpath, outpath, rgb_list):
# -------------------------------------------------
# setup
# -------------------------------------------------
= cv2.VideoCapture(inpath)
vidcap
= int(vidcap.get(cv2.CAP_PROP_FRAME_WIDTH))
width = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
height = 30 #vidcap.get(cv2.CAP_PROP_FPS)
fps
= cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
fmt = cv2.VideoWriter(outpath, fmt, fps, (width, height)) # ライター作成
writer
= 2
radius
= 0
counter
# -------------------------------------------------
# loop
# -------------------------------------------------
while True:
= vidcap.read()
grabbed_frame, frame if frame is None:
break
# HSVマスク作成
= cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
img_hsv = hsv_maks(img_hsv, rgb_list=rgb_list)
mask_hsv = cv2.bitwise_and(frame, frame, mask=mask_hsv)
img_bgr
# グレースケール変換
= cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
gray = cv2.dilate(gray,None,iterations = 1)
gray
# Shi-Tomasiコーナー検出
= cv2.goodFeaturesToTrack(gray, maxCorners=180, qualityLevel=0.001, minDistance=12)
corners = np.int0(corners)
corners
# キャンバスへ描画
= np.zeros_like(frame)
canvas
for i in corners:
= i.ravel()
pt 255,255,255), -1)
cv2.circle(canvas, pt, radius, (
for pair in itertools.combinations(corners, 2): # 組み合わせ
= pair
d1, d2 = d1.ravel()
pt1 = d2.ravel()
pt2
if is_nearby(pt1, pt2, 30, 0):
255,255,255))
cv2.line(canvas, pt1, pt2, (
# 書き出し
'Canvas', canvas)
cv2.imshow(= cv2.resize(canvas, (width, height))
canvas
writer.write(canvas)
= cv2.waitKey(5)
key if key == 27: # Esc
break
+= 1
counter
vidcap.release()
writer.release()
if __name__ == '__main__':
= '../assets/biotope_short.mov' # 1280 x 720
inpath = '../build/medaka_detected_corner.mp4'
outpath
= [
rgb_list 156, 134, 70], # 金色メダカ
[144,146,80], # 金色メダカのお腹
[217, 186, 137], # 金色メダカ
[122, 111, 48], # 金色メダカ
[150,120,56], # 金色メダカ
[133,106,42], # 金色メダカ
[137,122,52], # 金色メダカ
[144,145,80], # 金色メダカ
[185,149,77], # 金色メダカ
[130,117,69], # 金色メダカ
[
89, 73, 47], # 黒メダカ
[115, 105, 62], # 黒メダカ
[79, 61, 29], # 黒メダカ
[100, 97, 47], # 黒メダカ
[120, 101, 46], # 黒メダカ
[108, 110, 85], # 黒メダカのお腹
[69,67,31], # 黒メダカ
[87,77,16], # 黒メダカ
[103,104,26], # 黒メダカ
[80,75,43], # 黒メダカ
[81,79,43], # 黒メダカ
[98,87,54], # 黒メダカ
[147,156,156], # 黒メダカのお腹
[133,135,139], # 黒メダカのお腹
[140,133,135], # 黒メダカのお腹
[53,46,12], # 黒メダカ
[78,69,18], # 黒メダカ
[77,85,42], # 黒メダカ
[90,82,40], # 黒メダカ
[
]
main(inpath, outpath, rgb_list)
実行結果が次になります。
Processingのジェネラティブアートっぽく、点と点を線で結んでみました。一定の距離以下の点と点を結ぶ方法は比較的簡単でして、やり方は の記事で解説してます。
特徴点を追跡する(モーション解析)
コーナー検出によって見つけられた特徴点を、動画フレームの前後で保持(追跡)する便利な機能がOpenCVに備えられてます。それが
calcOpticalFlowPyrLK()
関数です。
calcOpticalFlowPyrLK()
は Lucas-Kanade法(英語論文)
を使ったオプティカルフローと呼ばれるものです。入力には、以前と現在の二つのグレースケール画像と、以前の特徴点の情報が必要になります。結果として移動後の特徴点の配列を得られます。
次はメダカの特徴点(軌跡)を追跡した例です:
import cv2
import numpy as np
import colorsys
def rgb2hsv(rgb):
= rgb
r,g,b = colorsys.rgb_to_hsv(r, g, b)
h,s,v return int(h*180), int(s*255), int(v)
def hsv_maks(img, rgb_list=None):
for i, hsv in enumerate([rgb2hsv(rgb) for rgb in rgb_list]):
= hsv
h,s,v = np.array([h-3, s-4, v-4])
lower = np.array([h+3, s+4, v+4])
upper = cv2.inRange(img,lower,upper)
src
if i == 0:
= src
mask_hsv else:
= cv2.addWeighted(src1=src,alpha=1,src2=mask_hsv,beta=1,gamma=0)
mask_hsv
return mask_hsv
def clamp(n, smallest, largest):
return max(smallest, min(n, largest))
def main(inpath, outpath, rgb_list):
# -------------------------------------------------
# setup
# -------------------------------------------------
= cv2.VideoCapture(inpath)
vidcap
= int(vidcap.get(cv2.CAP_PROP_FRAME_WIDTH))
width = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
height = 30 #vidcap.get(cv2.CAP_PROP_FPS)
fps
= cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
fmt = cv2.VideoWriter(outpath, fmt, fps, (width, height)) # ライター作成
writer
= 2
radius
= 0
counter = 0.0
alpha = 1.0
beta
# Parameters for lucas kanade optical flow
= dict( winSize = (15,15),
lk_params = 2,
maxLevel = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
criteria
= vidcap.read()
grabbed_frame, frame = []
old_masks
# -------------------------------------------------
# loop
# -------------------------------------------------
while True:
= vidcap.read()
grabbed_frame, frame if frame is None:
break
= np.zeros_like(frame)
mask
= cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
img_hsv = hsv_maks(img_hsv, rgb_list=rgb_list)
mask_hsv = cv2.bitwise_and(frame, frame, mask=mask_hsv)
img_bgr = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
gray = cv2.goodFeaturesToTrack(gray, maxCorners=180, qualityLevel=0.001, minDistance=12)
corners
if counter > 0:
= cv2.calcOpticalFlowPyrLK(old_gray, gray, old_pts, None, **lk_params)
new_pts, st, err = new_pts[st==1]
good_new = old_pts[st==1]
good_old
for new,old in zip(good_new,good_old):
= np.int0(new.ravel())
pt0 = np.int0(old.ravel())
pt1 255,128,0), 2)
cv2.line(mask, pt0, pt1, (255,0,255), -1)
cv2.circle(mask, pt1, radius, (
0, mask)
old_masks.insert(if len(old_masks) > 15:
= old_masks[:15]
old_masks = 1
a for m in old_masks:
= cv2.addWeighted(src1=m,alpha=a, src2=mask,beta=1,gamma=0)
mask -= 0.05
a
= clamp(alpha, 0, 1)
alpha = clamp(beta, 0, 1)
beta
'img', mask)
cv2.imshow(= cv2.resize(mask, (width, height))
mask
writer.write(mask)
= cv2.waitKey(5)
key if key == 27: # Esc
break
+= 1
counter = corners.copy()
old_pts = gray
old_gray
vidcap.release()
writer.release()
if __name__ == '__main__':
= '../assets/biotope_short.mov' # 1280 x 720
inpath = '../build/medaka_flow_short.mp4'
outpath
= [
rgb_list 156, 134, 70], # 金色メダカ
[144,146,80], # 金色メダカのお腹
[217, 186, 137], # 金色メダカ
[122, 111, 48], # 金色メダカ
[150,120,56], # 金色メダカ
[133,106,42], # 金色メダカ
[137,122,52], # 金色メダカ
[144,145,80], # 金色メダカ
[185,149,77], # 金色メダカ
[130,117,69], # 金色メダカ
[
89, 73, 47], # 黒メダカ
[115, 105, 62], # 黒メダカ
[79, 61, 29], # 黒メダカ
[100, 97, 47], # 黒メダカ
[120, 101, 46], # 黒メダカ
[108, 110, 85], # 黒メダカのお腹
[69,67,31], # 黒メダカ
[87,77,16], # 黒メダカ
[103,104,26], # 黒メダカ
[80,75,43], # 黒メダカ
[81,79,43], # 黒メダカ
[98,87,54], # 黒メダカ
[147,156,156], # 黒メダカのお腹
[133,135,139], # 黒メダカのお腹
[140,133,135], # 黒メダカのお腹
[53,46,12], # 黒メダカ
[78,69,18], # 黒メダカ
[77,85,42], # 黒メダカ
[90,82,40], # 黒メダカ
[
]
main(inpath, outpath, rgb_list)
実行結果が次になります。
▼ ぜひこちらの映像もご覧ください。
https://www.youtube.com/shorts/zQ89Vlm7oKE
calcOpticalFlowPyrLK
=..., err=..., winSize=..., maxLevel=..., criteria=..., flags: int = ..., minEigThreshold=...) calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts, status
パラメータ | 意味 |
---|---|
prevImg | 1番目の入力画像 |
nextImg | 2番目の入力画像 |
prevPts | 1番目の入力画像の特徴点 |
winSize | ピラミッドレベルにおける探索窓のサイズ |
maxLevel | 画像ピラミッドの最大レベル数 |
criteria | 反復探索アルゴリズムの停止基準 |
derivLambda | 画像の空間微分の相対的な重み |
(大きな動きの場合はピラミッドのスケールをアップする)
関連記事
- 【Python】OpenCVで図形の描画からアニメーションまで【線・四角・丸・塗りつぶし】
- 【Python】OpenCVでコーナーの検出【Harris/Shi-Tomasi】
- 【Python】OpenCVで画像操作いろいろ(グレースケール・モノ・輪郭抽出・切り抜く・透過)
- 【Python】VidStabで手ぶれ補正【動画編集への道#2】
- 【Python】OpenCVで画像をアフィン変換【移動・拡大・回転・剪断】