動機
- 手元を真上から撮ってOBSに取り込みたい(1回数分程度の録画)
- カメラの物理的な位置合わせ、切り抜きの微調整が面倒
作ったもの
- カメラ内の四点をクリックすると切り抜き、変形、回転を一発で変換して別ウインドウに出すもの
- クリック順で回転の仕方を変えられる
- 変換後に左上、右上、左下、右下になる位置を順にクリックする
- クリック順で回転の仕方を変えられる
コード
- Windows 11、Python 3.13.2、opencv-python 4.11.0.86
import cv2
import numpy as np
DST_CAM_WIDTH = 800
DST_CAM_HEIGHT = 800
DST_SIZE = (DST_CAM_WIDTH, DST_CAM_HEIGHT)
print("左上、右上、左下、右下の順に左クリック(Z字の順)")
print("右クリックでリセット、Qで終了")
SRC_PTS = np.float32([
[0,0], # 左上
[DST_CAM_HEIGHT,0], # 右上
[0,DST_CAM_WIDTH], # 左下
[DST_CAM_HEIGHT,DST_CAM_WIDTH] ]) # 右下
TGT_PTS = None
CLICKED_PTS = []
def print_camera_prop(cap):
fps = cap.get(cv2.CAP_PROP_FPS)
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
print(f"camera: {width}x{height} ({fps}fps)")
def pts_clicked_clear():
CLICKED_PTS.clear()
print("クリックした点をクリア")
def mouseCB(event,x,y,flags,param):
global TGT_PTS
if event == cv2.EVENT_RBUTTONDOWN:
pts_clicked_clear()
if event != cv2.EVENT_LBUTTONDOWN:
return
if len(CLICKED_PTS) >=4:
return
item = (x,y)
CLICKED_PTS.append(item)
print(f'Mouse L x:{x}, y:{y}')
if len(CLICKED_PTS) >=4:
TGT_PTS = np.float32(CLICKED_PTS)
print(f'変換:{TGT_PTS}')
else:
TGT_PTS = None
def main():
cap = cv2.VideoCapture(0, cv2.CAP_MSMF) # cv2.CAP_MSMF CAP_DSHOW CAP_WINRT
cap.set(cv2.CAP_PROP_FPS, 60)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # (4:3)1280x960 (16:9)1280x720 1920x1080
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 960)
# cap.set(cv2.CAP_PROP_SETTINGS, 1) # CAP_DSHOW
print_camera_prop(cap)
ret = True
while(ret):
ret, frame = cap.read()
frame_ui = frame.copy()
msg_ui1 = f"Exit=Q Reset=Click(R) CropPoint=Click(L)"
msg_ui2 = f"Points: {len(CLICKED_PTS)} {[f'({x},{y}) ' for x,y in CLICKED_PTS]}"
cv2.putText(frame_ui, msg_ui1, (20, 20), cv2.FONT_HERSHEY_PLAIN, 1.0, (0,255,0))
cv2.putText(frame_ui, msg_ui2, (20, 40), cv2.FONT_HERSHEY_PLAIN, 1.0, (0,255,0))
for pt in CLICKED_PTS:
cv2.drawMarker(frame_ui, pt, (0, 255, 0), cv2.MARKER_CROSS)
cv2.imshow("Camera", frame_ui)
cv2.setMouseCallback('Camera', mouseCB)
if TGT_PTS is not None:
M = cv2.getPerspectiveTransform(TGT_PTS, SRC_PTS)
dst = cv2.warpPerspective(frame, M, DST_SIZE)
else:
dst = cv2.resize(frame, DST_SIZE, fx=0,fy=0,interpolation=cv2.INTER_LINEAR)
cv2.imshow("WARPED", dst)
if cv2.waitKey(1) & 0xff == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
動作の様子
コマンドプロンプトの出力
左上、右上、左下、右下の順に左クリック(Z字の順)
右クリックでリセット、Qで終了
camera: 1280.0x960.0 (60.0fps)
Mouse L x:321, y:849
Mouse L x:304, y:100
Mouse L x:1056, y:826
Mouse L x:1039, y:100
変換:[[ 321. 849.]
[ 304. 100.]
[1056. 826.]
[1039. 100.]]
入力・切り抜きポイント指定
- 90度回転させたいので『左下、左上、右下、右上』の順にクリックする(正対した時に左上、右上、左下、右下になる位置)
- 見えにくいが緑の十字がクリックした場所
- 重いブームマイクスタンドで真俯瞰気味の場所にWebカメラを設置
出力(切り出し・変形・回転)
OBS取り込みイメージ(Windowの名前でキャプチャ)
誤算だったこと
- モニタ上や卓上三脚から角度をつけてカメラをセットすると大幅に画質が落ちる
- 撮像範囲に対し切り出した範囲が狭い
- 小さく切り出したものを大きく変形するのでどうしても汚くなる
- 被写界深度の関係で端がボケる
- 撮像範囲に対し切り出した範囲が狭い
- 真俯瞰撮影できる位置にカメラを設置するスタンドが結局必要
- 大きく写す&被写体の面とセンサーの面を平行にする=真上