3点,4点からなる二直線のなす角度を求める

3点,4点からなる二直線のなす角度を求めるというのは、以下の図のように、各点を通る二直線のなす角度を求める方法を紹介したいと思います。

 

 

4点からなる二直線の角度は、2点が重なり合うように、直線を構成している2点を平行移動すれば、3点の場合と同じ様になるので、ここでは、3点からなる二直線のなす角度を求める方法で説明します。

 

普通のやり方

まず、最初に思いつくのが、2直線のそれぞれの線の傾きをアークタンジェントを使って求め、2つの角度の差を求める方法だと思います。

 

$$\theta_{1}=tan^{-1}(\frac{A_{y}-B_{y}}{A_{x}-B_{x}})$$

$$\theta_{2}=tan^{-1}(\frac{C_{y}-B_{y}}{C_{x}-B_{x}})$$

$$\theta = \theta_{2}-\theta_{1}$$

 

ただし、直線が垂直の場合、アークタンジェントの計算時の分母が0になり、0除算になってしまうため、プログラム上はアークタンジェントの関数は atan() を使うのではなく、 atan2()を使った方がいいです。

atan2()関数を使うと、戻り値が-180°~+180°となりますが、この180°の位置をまたぐ角度の算出には注意が必要です。

 

他にも、2直線のなす角度を求める方法として、内積、外積を使った方法があるので、これを紹介します。

 

内積を使った方法

2つの直線を下図のように2つのベクトルとして捉え、内積で角度を求めます。

ベクトルa と ベクトルb を

$$\overrightarrow{a}=(a_{x}, a_{y}) = (A_{x}-B_{x}, A_{y}-B_{y})$$

$$\overrightarrow{b}=(b_{x}, b_{y}) = (C_{x}-B_{x}, C_{y}-B_{y})$$

とします。

 

ベクトルa と ベクトルb の内積は

$$\overrightarrow{a}\cdot \overrightarrow{b}=a_{x}b_{x} + a_{y}b_{y} = |\overrightarrow{a}||\overrightarrow{b}|cos\theta$$

 

であるから、式を変形して、

$$cos\theta=\frac{a_{x}b_{x} + a_{y}b_{y}}{|\overrightarrow{a}||\overrightarrow{b}|}$$

$$cos\theta=\frac{a_{x}b_{x} + a_{y}b_{y}}{\sqrt{{a_{x}}^{2}+{a_{y}}^{2}}\sqrt{{b_{x}}^{2}+{b_{y}}^{2}}}$$

$$\theta=cos^{-1}\left(\frac{a_{x}b_{x} + a_{y}b_{y}}{\sqrt{{a_{x}}^{2}+{a_{y}}^{2}}\sqrt{{b_{x}}^{2}+{b_{y}}^{2}}}\right)$$

として、角度θを求める事ができます。

ただし、アークコサインで求める事の出来る角度は0°~180°なので、マイナスの角度を求める事ができず、ベクトルbがベクトルaの右側か?左側か?の判断はできません。

 

右側か?左側か?の情報が必要な場合は、外積を用います。

 

外積を使った方法

外積については、少しおさらいです。

内積の結果はノルム(大きさ)となりますが、外積の結果はベクトルです。

外積の計算は

$$\overrightarrow{a}=(a_{x},a_{y},a_{z})$$

$$\overrightarrow{b}=(b_{x},b_{y},b_{z})$$

とすると、外積の結果は

$$\overrightarrow{a}\times \overrightarrow{b}=(a_{y}b_{z}-b_{y}a_{z},a_{z}b_{x}-b_{z}a_{x},a_{x}b_{y}-b_{x}a_{y})$$

さらに、外積を行う2つのベクトルからなる平行四辺形の面積が、外積の大きさと一致します。

ここで、外積の計算は三次元ベクトルで行うのですが、外積を行う2つのベクトルのz成分を0にして、外積を計算すると、外積の結果の大きさは、外積の結果のz成分と一致します。

$$\overrightarrow{a}=(a_{x},a_{y},0)$$

$$\overrightarrow{b}=(b_{x},b_{y},0)$$

$$|\overrightarrow{a}\times \overrightarrow{b}|=a_{x}b_{y}-b_{x}a_{y}=|\overrightarrow{a}||\overrightarrow{b}|sin\theta$$

となります。

ここから式を変形して

$$sin\theta=\frac{a_{x}b_{y}-b_{x}a_{y}}{|\overrightarrow{a}||\overrightarrow{b}|}$$

$$sin\theta=\frac{a_{x}b_{y}-b_{x}a_{y}}{\sqrt{{a_{x}}^{2}+{a_{y}}^{2}}\sqrt{{b_{x}}^{2}+{b_{y}}^{2}}}$$

$$\theta=sin^{-1}\left(\frac{a_{x}b_{y}-b_{x}a_{y}}{\sqrt{{a_{x}}^{2}+{a_{y}}^{2}}\sqrt{{b_{x}}^{2}+{b_{y}}^{2}}}\right)$$

 

として、角度θを求める事ができます。

アークサインで求まる角度は-90°~90°となるので、ベクトルa と ベクトルb の位置関係を判断することができます。

θの値がプラスの場合、ベクトルb は ベクトルa に対して、反時計周りの位置にあります。

θの値がマイナスの場合、ベクトルb は ベクトルa に対して、時計周りの位置にあります。

ただし、この計算だと±90°の範囲しか計算できませんが、その範囲以上の角度を求めたい場合は、ベクトルa と ベクトルb の内積の値も使います。

 

ベクトルa と ベクトルb の内積の値が正であれば、θは±90°の範囲内

ベクトルa と ベクトルb の内積の値が負であれば、θは±90°の範囲外

 

となるので、360°の範囲で、ベクトルa と ベクトルb の位置関係を求める事ができるようになります。

 

まとめ

●アークタンジェントで角度を求めるときは、atan()関数でなくatan2()関数を使う。

●内積や外積を使って角度を求めることもできる。

●内積だと、2つの直線の位置関係がわかりませんが、外積だと位置関係が分かる。

 

参考

内積

外積

【OpenCV-Python】momentsによる重心の計算

画像処理において、重心は、欠陥部分の中心位置の算出や、レーザー光のように山なりの輝度分布を持つ画像の輝度値のピーク位置を求めるのに使っています。

 

 

 

 

 

 

 

 

 

重心の計算方法はこちらのページでも紹介しているように、計算そのものは、以下の計算式で求める事ができます。

 

$$重心=\frac{(重さ\times位置)の合計}{重さの合計}$$

 

ここで、画像処理における重さとは、画像の各画素の輝度値を差すので、

 

$$重心=\frac{(輝度値\times位置)の合計}{輝度値の合計}$$

 

となります。

 

OpenCVでは moments()関数を使って、重心を求めるのですが、OpenCVのページによると、モーメントは

$$m_{ji}=\sum_{x,y}(array(x,y)\cdot x^{j}y^{i})$$

で求められ、重心は

$$\overline{x}=\frac{m_{10}}{m_{00}},\overline{y}=\frac{m_{01}}{m_{00}}$$

となります。

と、書かれていいるのですが、式が何だか難しくて、よくわかりません。。

 

そこで、m00, m10, m01 を詳しく見ていきます。

 

m00は、

$$m_{00}=\sum_{x,y}(array(x,y)\cdot x^{0}y^{0})$$

$$=\sum_{x,y}(array(x,y)\cdot 1 \cdot 1)$$

$$=\sum_{x,y}(array(x,y))$$

 

となり、m00array(x,y)の合計ということがわかります。

array(x, y)は、位置が(x, y)の画素の輝度値を表しているので、m00 が画像の輝度値の合計となります。

 

m10は、

$$m_{10}=\sum_{x,y}(array(x,y)\cdot x^{1}y^{0})$$

$$=\sum_{x,y}(array(x,y) \cdot x \cdot 1)$$

$$=\sum_{x,y}(array(x,y) \cdot x )$$

となり、m10は画像の輝度値とx座標を掛け合わせた合計という事がわかります。

 

同様にm01は、

$$m_{01}=\sum_{x,y}(array(x,y)\cdot x^{0}y^{1})$$

$$=\sum_{x,y}(array(x,y) \cdot 1 \cdot y)$$

$$=\sum_{x,y}(array(x,y) \cdot y )$$

となり、m01は画像の輝度値とy座標を掛け合わせた合計という事がわかります。

 

ここで、重心は

$$重心のx座標=\frac{(輝度値\times x座標)の合計}{輝度値の合計}$$

$$重心のy座標=\frac{(輝度値\times y座標)の合計}{輝度値の合計}$$

となることから、重心の座標が

$$\overline{x}=\frac{m_{10}}{m_{00}},\overline{y}=\frac{m_{01}}{m_{00}}$$

で、求まる事が分かります。

 

実際のOpenCVで重心を求めるプログラムを示します。

 

画像全体の重心を求めるプログラム

import cv2

# グレースケールで画像を読み込む
img = cv2.imread("image.bmp", cv2.IMREAD_GRAYSCALE);

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# モーメントの計算
m = cv2.moments(img, False)
# 重心の計算
x,y= m['m10']/m['m00'] , m['m01']/m['m00']
print(f"Weight Center = ({x}, {y})")

# 座標を四捨五入
x, y = round(x), round(y)

# 重心位置に x印を書く
cv2.line(img_disp, (x-5,y-5), (x+5,y+5), (0, 0, 255), 2)
cv2.line(img_disp, (x+5,y-5), (x-5,y+5), (0, 0, 255), 2)

# 結果の表示
cv2.imshow("Image", img_disp)
cv2.waitKey()

実行結果

 

画像の輪郭から重心を求める方法

findContours()関数により、連続する領域の輪郭座標を求め、輪郭座標から、それぞれの重心を求めるには、以下のようにします。

import cv2

# グレースケールで画像を読み込む
img = cv2.imread("image.bmp", cv2.IMREAD_GRAYSCALE);

# 画像表示用に入力画像をカラーデータに変換する
img_disp = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

# 二値化
ret,img = cv2.threshold(img,127,255,cv2.THRESH_BINARY)

# 輪郭情報の取得
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) 

# 輪郭ごとの処理
for i, contour in enumerate(contours):
    # 重心の計算
    m = cv2.moments(contour)
    x,y= m['m10']/m['m00'] , m['m01']/m['m00']
    print(f"Weight Center = ({x}, {y})")
    # 座標を四捨五入
    x, y = round(x), round(y)
    # 重心位置に x印を書く
    cv2.line(img_disp, (x-5,y-5), (x+5,y+5), (0, 0, 255), 2)
    cv2.line(img_disp, (x+5,y-5), (x-5,y+5), (0, 0, 255), 2)

# 結果の表示
cv2.imshow("Image", img_disp)
cv2.waitKey()

実行結果

 

輪郭から求める重心の座標について

輪郭のから求める重心は、輪郭の座標だけの重心を求めているのではなく、グリーンの定理というのを使って輪郭の内側が塗りつぶされた状態の重心を求めているとのこと。

例えば、もし、下図のような図形の輪郭座標だけの重心を求めると、画像全体から求めた重心よりも輪郭から求めた重心の方が、少し、左側の位置に重心がズレそうですが、ほぼ、同じ位置の重心となります。
(輪郭から求めた重心は、少し誤差が出ます。)

 

参照ページ

https://docs.opencv.org/4.x/d8/d23/classcv_1_1Moments.html

重心の計算方法

http://opencv.jp/opencv-2.1/cpp/structural_analysis_and_shape_descriptors.html?highlight=moment

https://en.wikipedia.org/wiki/Green%27s_theorem

 

重心の計算方法

重心とは、重さの中心で、重心の位置で、力がつり合います。

 

重心の計算方法は、言葉で表すと

$$重心=\frac{(重さ\times位置)の合計}{重さの合計}$$

となります。

 

具体的な計算例を示すと、

のデータに関して、重心を計算すると、

$$重心=\frac{2\times0+5\times1+13\times2+15\times3+5\times4}{2+5+13+15+5}
=2.4$$

 

となります。

 

さらに二次元的な座標においても、各座標軸に関して、それぞれ計算を行います。

こちらも具体的に、以下のようなデータの場合、

$$重心のx座標=\frac{3\times0+10\times1+7\times2+5\times0+20\times1+7\times2+2\times0+9\times1+3\times2}{3+10+7+5+20+7+2+9+3}$$

$$= 1.1$$

$$重心のy座標=\frac{3\times0+5\times1+2\times2+10\times0+20\times1+9\times2+7\times0+7\times1+3\times2}{3+5+2+10+20+9+7+7+3}$$

$$= 0.9$$

となります。(分母の重さの合計は、計算の順番が分かりやすいように、順番を入れ替えています。)

 

画像処理において重心を計算する場合は、重さの値に画像の輝度値を用いますが、二値化した画像に対して重心の計算を行い、重さを考慮しない場合もありますが、この場合は、重心の座標は、しきい値以上となる画素の座標の平均と一致します。

【OpenCV-Python】カメラのフレームレートは設定できない?

OpenCVでカメラのフレームレート(fps: frames per second)を設定するには、文法上は

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

# フレームレートを設定
ret = cap.set(cv2.CAP_PROP_FPS, 15)

のようにすると、フレームレートを15fpsに設定した事になります。

しかしながら、実際にカメラで画像を撮影してみると、フレームレートが変わったように感じません。

OSがLinuxではフレームレートを設定出来ているような情報を見かけるのですが、Windowsでは、あまり見かけません。

 

という事で、Windows環境でフレームレートが設定できるのか?評価してみました。

 

評価環境

カメラ:Anker PowerConf C200

OS:Windows11

Python 3.11

opencv-python 4.7.0.72

 

評価方法

以下のサンプルプログラムを実行し、300枚撮影するのに掛かった時間を計測し、fpsを計算しました。

import cv2
import time

# カメラを開く
cap = cv2.VideoCapture(0)

# フレームレートを設定
ret = cap.set(cv2.CAP_PROP_FPS, 15)
# 設定されているフレームレートを取得
print(cap.get(cv2.CAP_PROP_FPS))

# 撮影開始時間
start = time.perf_counter()

# 画像を300枚撮影する
for i in range(300):
    # 画像をキャプチャする
    ret, frame = cap.read()

# 画像300枚を撮影するのにかかった時間からfpsの計算
print(f"{300/(time.perf_counter() - start)}fps")

# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このプログラムを実行すると、

ret = cap.set(cv2.CAP_PROP_FPS, 15)

の部分でfpsを15に設定すると、戻り値(ret)は Trueとなり、一見、設定できているように見えるのですが、

print(cap.get(cv2.CAP_PROP_FPS))

として、フレームレートを取得すると30fpsとなり、実際にはフレームレートは設定できていませんでした。

そこで、

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

として、DirectShowを使ってカメラを開き、set()関数でfpsを15fpsに設定し、get()関数で設定したフレームレートを取得すると、ちゃんと変更されている事が確認できます。

しかしながら、実際にread()関数でカメラのフレーム画像を取得する時間を計算してみると、29.3fpsとなり、結局のところ、フレームレートは設定できていませんでした。

CAP_DSHOW(DirectShow)以外にCAP_MSMF(Microsoft Media Foundation)など、いろいろ試してみましたが、設定できず。

 

もちろん、fpsが設定できるかどうかは、カメラ依存の部分もあろうかと思いますが、3つのカメラで評価してみたところ、フレームレートを変更することはできませんでした。。

結局のところ、fpsを落とすには、フレームを間引くしか無いのかも?

 

参考

https://docs.opencv.org/4.7.0/d8/dfe/classcv_1_1VideoCapture.html

https://docs.opencv.org/4.7.0/d4/d15/group__videoio__flags__base.html#gga023786be1ee68a9105bf2e48c700294da77ab1fe260fd182f8ec7655fab27a31d

【OpenCV-Python】Webカメラから画像をキャプチャして保存する

OpenCVでWebカメラ(USBカメラ)から画像をキャプチャして保存する方法を紹介します。

必要なもの

●Python

●OpenCV(pip install opencv-pythonでOpenCVをインストールします)

●Webカメラ(USBカメラ)

 

サンプルプログラム

OpenCVでカメラから画像を取得し、画像ファイルに保存するサンプルプログラムを以下に示します。

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

# 画像をキャプチャする
ret, frame = cap.read()

# 画像を保存する
cv2.imwrite("image.jpg", frame)

# カメラを閉じる
cap.release()

これで、Webカメラから画像を取得し、 image.jpg というファイル名で画像が保存されます。

 

次に各コードの説明をします。

 

VideoCapture()

cap = cv2.VideoCapture(0)

の部分で、カメラを開いています。

関数の引数で渡している  は、最初に見つけたカメラを開く事を意味しています。

ほとんどの場合、0 で大丈夫だと思いますが、例えば、PC内臓のカメラのあるノートPCに、別途、USBカメラを刺して使う場合には、この番号を 1 や 2 に調整してみてください。

 

read()

ret, frame = cap.read()

この部分で、現在、撮影している画像データを取得します。

画像の取得し失敗すると ret は Falseになります。

失敗する原因としては、VideoCaptureの部分で指定する番号を間違えたとか、他のプログラムでカメラを使用していた場合などがあります。

frameが撮影した画像データとなります。

 

その他のサンプルを以下に示します。

 

画像をリアルタイムに表示するサンプル

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

while True:
    # 画像をキャプチャする
    ret, frame = cap.read()

    # 画像を表示する
    cv2.imshow("Image", frame)

    # `q`キーを押すとループを終了する
    if cv2.waitKey(1) == ord('q'):
        break

# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このサンプルで q キーを押すまで画像を取得し、表示します。

 

キーを押すたびに連番でファイルに保存するサンプル

import cv2

# カメラを開く
cap = cv2.VideoCapture(0)

indexNo = 0
while True:
    # 画像をキャプチャする
    ret, frame = cap.read()

    # 画像を表示する
    cv2.imshow("Image", frame)

    # `q`キーを押すとループを終了する
    if cv2.waitKey(1) == ord('q'):
        break
    elif cv2.waitKey(1) == ord('c'):
        cv2.imwrite(f"image{indexNo:05d}.jpg", frame)
        indexNo = indexNo + 1
# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このサンプルでは、一つ前のサンプルに加えて、 c  キーを入力するたびに、連番のjpegファイルに画像を保存します。

 

read()での画像の取得タイミングについて

ここからが、個人的に興味のあった部分。

画像を取得する際に、read()関数をwhile文で繰り返し呼んでいますが、カメラのフレームレートよりも速くread()関数が呼ばれている可能性が高いです。

このとき、read()関数は、呼ばれるたびに、即、画像データを取得しているのか?それとも、1フーレムの撮影が終わるのを待ってから、画像を取得しているのか?気になった。。

 

そこで、こんなプログラムを実行してみました。

import cv2
import time

# カメラを開く
cap = cv2.VideoCapture(0)

# 撮影開始時間
start = time.perf_counter()
for i in range(300):
    # 画像をキャプチャする
    ret, frame = cap.read()

# 画像300枚を撮影するのにかかった時間
print(time.perf_counter() - start)
# カメラを閉じる
cap.release()
# すべてのウィンドウを閉じる
cv2.destroyAllWindows()

このサンプルプログラムでは、30fpsのカメラを使って、300枚の画像を撮影しています。

 

実際に、プログラムを実行すると、300枚の画像を撮影するのにかっかった時間は、10.4秒という結果になりました。

30fpsのカメラで300枚撮影しているので、計算上は300枚撮影するのに10秒かかる事になり、ほぼ、理論通りの結果となりました。

このことから、read()関数が呼ばれたら、即、画像データを取得しているのではなく、カメラが1フレーム分、撮影が完了するのを待ってから画像を取得しているものと、推測されます。

少なくとも、read()関数を繰り返し呼んでも、同じ画像を重複して取得する事は無さそうです。