Macでマウスを使って矩形領域を指定して画面キャプチャする(2021.7.7)

Summary

Macで指定したデバイスから画面キャプチャするのはできるようになったので、さらに、キャプチャ画面上でマウスを使って矩形領域を指定して、その領域だけをキャプチャするようにしたい。
前回と同様にデバイスが複数ある場合には最初に選択する。

cv2.setMouseCallback()でマウスイベントを取得して描画

マウスイベントの取得

call back関数を定義し、その関数でevent, x, y, flags, paramを引数として受け取るようにする。

cv2.namedWindow('Type [q] to quit capturing')
cv2.setMouseCallback('Type [q] to quit capturing', draw_circle)

cv2.namedWindow()で設定した名前の文字列と同一文字列で対象のwindowを特定できるようにしてcall back関数を指定する。
この対応が誤っていると、

 (-27:Null pointer) NULL window handler in function 'cvSetMouseCallback'.

などとおこられます。

カメラキャプチャ画像へのインポーズ描画

whileループ中では毎回新しいフレーム画像が取得されるので、必要な図形をマイフレーム再描画する必要がある。
draw_circleの中では、クリックイベントが発生するごとにポジションをpt0に保存する。
一度マウスがクリックされたなら、画面内のどこか(最新のpt0の場所)に赤丸を書き続ける。

def draw_circle(event, x, y, flags, param):
    global circle_clicked, pt0
    print(x, y, event, flags, param, cv2.EVENT_LBUTTONDOWN, circle_clicked)
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Draw!!! (%d, %d), %s" % (x, y, circle_clicked))
        # pt0 shall be updeted for every Left click
        pt0 = (x, y)
        if circle_clicked == False:
            circle_clicked = True
           
while True:
    # Capture frame-by-frame
    ret, frame = cap.read()

    if circle_clicked == True:
            cv2.circle(frame, center=pt0, radius=5, color=(0,0,255), thickness=-1)

マウスでクリックしたところに赤い丸を表示するプログラムはこんな感じ。

プログラムサンプル
import cv2

def draw_circle(event, x, y, flags, param):
    global circle_clicked, pt0
    print(x, y, event, flags, param, cv2.EVENT_LBUTTONDOWN, circle_clicked)
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Draw!!! (%d, %d), %s" % (x, y, circle_clicked))
        # pt0 shall be updeted for every Left click
        pt0 = (x, y)
        if circle_clicked == False:
            circle_clicked = True

# init position values
pt0 = (0, 0)
circle_clicked = False

cap = cv2.VideoCapture(0)

cv2.namedWindow('Type [q] to quit capturing')
cv2.setMouseCallback('Type [q] to quit capturing', draw_circle)

while True:
    # Capture frame-by-frame
    ret, frame = cap.read()

    if circle_clicked == True:
            cv2.circle(frame, center=pt0, radius=5, color=(0,0,255), thickness=-1)

    # show the frame
    cv2.imshow('Type [q] to quit capturing', frame)

    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):
        break

# terminate resources
cap.release()
cv2.destroyAllWindows()

マウスクリックで矩形領域を指定する

マウスで左上と右下の座標を取得し長方形をインポーズ描画する

カメラ画像の取得したい矩形領域の左上と右下の2点でマウス左クリックすると、領域を選択できる。その状態でsキーを押すと領域を切り出して画像として保存する。
選択された領域と切り出された画像が一致していないのは、キーを押す操作をしているあいだに手持ちの被写体がずれているからです。

camera image

cropped image

1回目のクリックが完了すると、現在選択されている区画を緑線で表示する。
2度目にクリックすると赤枠になり選択領域が確定する。(最初の画像)

camera image

Macでマウスで指定した矩形領域をキャプチャするPythonスクリプト

完成したスクリプトは次のようになります。 起動すると、ビデオデバイスを調査し2つ以上のデバイスが見つかったら、ユーザにデバイス番号を入力させます。デバイスが1つしかなければ自動で選定し、1つも見つからなければエラー終了します。
領域の選択が完了した状態でsキーを押すと、赤枠で囲まれた領域を切り出して./photo_crop.jpgに保存し、赤枠で領域を確定していない城代でsキーが押された場合は、全体をキャプチャーして./photo.jpgに保存します。qで終了。

ソースコードはこちら(video_impose_test.py
avfcam_list.swift)をスクリプトと同じ場所に置いてください。)


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# =======================================================
#  opencvを使ってカメラから画像をキャプチャするスクリプト
#  マウスで矩形領域を指定してその範囲をキャプチャする
#
#  video_impose_test.py
#  coded by Noboru Harada (noboru@ieee.org)
#
#  Changes:
#  2021/07/07: First version
# =======================================================

import sys
import os
import subprocess
import json
from collections import OrderedDict
import numpy as np
import cv2


def draw_circle(event, x, y, flags, param):
    global circle_clicked, pt0
    print(x, y, event, flags, param, cv2.EVENT_LBUTTONDOWN, circle_clicked)
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Draw!!! (%d, %d), %s" % (x, y, circle_clicked))
        # pt0 shall be updeted for every Left click
        pt0 = (x, y)
        if circle_clicked == False:
            circle_clicked = True

def draw_rectangle(event, x, y, flags, param):
    global pt1, pt2, topLeft_clicked, botRight_clicked
    print(x, y, event, flags, param, cv2.EVENT_LBUTTONDOWN, topLeft_clicked, botRight_clicked)
    if event == cv2.EVENT_LBUTTONDOWN:
        if topLeft_clicked == True and botRight_clicked == True:
            # release botRight_clicked
            botRight_clicked = False
            # renew topLeft_clicked
            topLeft_clicked = True
            pt1 = (x, y)
            pt2 = (x, y)
        elif topLeft_clicked == False and botRight_clicked == False:
            pt1 = (x, y)
            topLeft_clicked = True
        elif topLeft_clicked == True and botRight_clicked == False:
            pt2 = (x, y)
            botRight_clicked = True
        print("Draw!!! (%d, %d), %s, %s" % (x, y, topLeft_clicked, botRight_clicked))
    elif botRight_clicked == False:
        pt2 = (x, y)

# init position values
pt0 = (0, 0)
pt1 = (0, 0)
pt2 = (0, 0)
circle_clicked = False
topLeft_clicked = False
botRight_clicked = False

# Get path for the swift script (supporse to be in the same location with this python script)
script_path = os.path.dirname(os.path.abspath(__file__))
script_path = "swift " + script_path + "/avfcam_list.swift"
print(script_path)

# Check camera devices with a swift script
camera_devices = subprocess.check_output(script_path, shell=True)
json_dict = json.loads(camera_devices, object_pairs_hook=OrderedDict)

camera_devices = camera_devices.decode("utf-8")
print(camera_devices)

if json_dict.get('SPCameraDataType') == None:
    print("No camera device found")
    sys.exit()
     
num_devices = len(json_dict['SPCameraDataType'])
if num_devices == 0:
    print("No camera device found")
    sys.exit()

val = 0
if num_devices > 1:
    print("Select a camera devie:")
    for s in range(num_devices):
        name = json_dict['SPCameraDataType'][s].get("_name")
        print(" %d: %s" % (s, name))
    try:
        val = int(input())
    except ValueError:
        print("Wrong device id")
        val = 0

if val > num_devices-1 or val < 0:
        print("Wrong device id")
        val = 0

device_id = val
device_name = json_dict['SPCameraDataType'][device_id].get("_name")
print(" Use Camera device %d: %s" % (val, device_name))
print("Type 'q' to quit capturing")

cap = cv2.VideoCapture(device_id)
cv2.namedWindow('Type [q] to quit capturing')

#cv2.setMouseCallback('Type [q] to quit capturing', draw_circle)
cv2.setMouseCallback('Type [q] to quit capturing', draw_rectangle)

# get window size
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

while(True):
    # Capture frame-by-frame
    ret, frame = cap.read()

#    if circle_clicked == True:
#        cv2.circle(frame, center=pt0, radius=5, color=(0,0,255), thickness=-1)

    if topLeft_clicked == True and botRight_clicked == False:
        cv2.rectangle(frame, pt1, pt2, (0, 255, 0), 1)
        cv2.circle(frame, center=pt1, radius=5, color=(0,0,255), thickness=-1)
    elif topLeft_clicked == True and botRight_clicked == True:
        cv2.rectangle(frame, pt1, pt2, (0, 0, 255), 2)

    # show the frame
    cv2.imshow('Type [q] to quit capturing', frame)

    key = cv2.waitKey(1) & 0xFF
    
    if key == ord('q'):
        break
    if key == ord('s'):
        ret, frame = cap.read()
        if topLeft_clicked == True and botRight_clicked == True:
            x1, y1 = pt1
            x2, y2 = pt2
            if x1 > x2 or y1 > y2:
                x1, y1 = pt2
                x2, y2 = pt1
            crop_frame = frame[y1 : y2, x1 : x2]
            filename = "./photo_crop.jpg"
            cv2.imwrite(filename,crop_frame)
        else:
            filename = "./photo.jpg"
            cv2.imwrite(filename,frame)

# terminate resources
cap.release()
cv2.destroyAllWindows()

参考

OpenCVでのマウスイベント取得とカメラ画像へのインポーズ描画

カメラの解像度設定

Pythonから外部コマンド(subprocess)を実行する

Back to Index