昨年(’21年)暮れになって,今までwebカメラとしては利用できなかった天体向けCOMSカメラにDirectshow版のドライバーが公開されたので,PythonのOpenCVモジュールでも認識できるようになりました(playerone社のカメラだけですが)。以前の記事は,動画撮影したファイル(AVIやMP4)から動態検知するものでしたが,パソコンでキャプチャーしつつ動体(流星)を検知して記録(静止画として保存,その後動画や比較明合成画像に)するプログラムに書き直しました。カメラの設定もプログラムから行います。15行目からの部分です。FPSはフレームレートで1秒間に何枚撮影するかで,だいたい30ぐらい。expは露出時間で,整数でマイナスが大きいほど短いようで調べてみましたがまだよくわかりません。-2くらいで十分高感度ですが,フレームレートが落ちるのでー4くらいで良いと思われます。gainが感度に相当しますが,最大650であることを確認しました。今回(’22年1月4日未明)のしぶんぎ座流星群を撮影してみました。geinを450にして,これで十分な感度で撮影できることが分かりました。ちょっと,失敗だったのはズームレンズの焦点距離が長めだったので,補足した流星が少なくなってしまいました(結果の写真はあまり見栄えがしません)。そこで,視野をワイドにして翌日ベランダから夜通しテストしてみました。
9時くらいから12時までは,飛行機がほとんどでした。光害のひどい空ですが,これくらいキャッチできれば火球観測には十分だと思います。一晩中晴れそうな日には以前のように動画のメモリを気にせず放置撮影してみたいと思います。
"""
Created on Feb Dec 31 2021
Camra Capturing Moving body detection program for Meteor use(detecting 3 frame)
implement user interface
"""
import os
import sys
import datetime
import cv2
import csv
import tkinter as tk
import numpy as np
from tkinter import filedialog as tkFileDialog #python3
WIDTH = 2712
HEIGHT = 1538
FPS = 30
exp = -4
gain = 290
gam = 1
dst = [0]*2000
cnt = [0]*2000
def save_file(dst,num,count):
date = datetime.datetime.now()
with open(dirname + "/" + "log_date" + str(date.day) + "_time" + str(date.hour) + "h_" +
str(date.minute)+ "m" + ".csv", "a",
newline='') as file:
w = csv.writer(file, delimiter=",")
for i in range(num):
w.writerow([i, str(count),str(date),cnt[i]])
for i in range(num):
cv2.imwrite(dirname + "/" + "meteor_" + str(date.hour) + "h_" + str(date.minute) + "m_" + str(date.second) + "s-" + str(i) + ".jpg", dst[i]) # 書き込み
def select_dir():
global dirname
#root.withdraw()
iDir = os.path.abspath(os.path.dirname(__file__))
dirname = tkFileDialog.askdirectory(initialdir=iDir,title = "select folder saving log and result images")
root.update()
def main(var):
# Capture Camera
cap = cv2.VideoCapture(1)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
cap.set(cv2.CAP_PROP_FPS, FPS)
cap.set(cv2.CAP_PROP_EXPOSURE, exp)
cap.set(cv2.CAP_PROP_GAIN, gain)
cap.set(cv2.CAP_PROP_GAMMA, gam)
# fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
if not cap.isOpened():
return
# Three frames of the beginning
img1 = img2 = img3 = get_image(cap)[1]
# Initial composition image
dst[0] = np.zeros(img1.shape)
th = var # threshold
print('th=',th)
num = 0
dn = 1
count = 1
flag = -1
while True:
img1, img2, img3 = (img2, img3, get_image(cap)[1])
# If "q" key is pushed, it is finished on the way
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# If an image cannot read, it is finished
if not ret:
break
# provide difference
diff = check_image(img1, img2, img3)
# If there is difference that counted more than a value of th, judge movement that there was
cnt[num] = cv2.countNonZero(diff)
if cnt[num] < th:
if flag > 0:
save_file(dst,num,count)
flag = -1
dn = 1
num = 0
count += 1
dst[0] = np.zeros(img1.shape)
continue
continue
elif dn < 3:
dn += 1
continue
print(f"detects move {num} ")
dst[num] = img2
cv2.imshow('detect_img',img2)
num += 1
flag = 1
print('Close it? push any key ')
cv2.waitKey(0)
cap.release()
cv2.destroyAllWindows()
# the function of detecting
def check_image(img1, img2, img3):
# convert into a gray scale image
gray1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY)
gray3 = cv2.cvtColor(img3, cv2.COLOR_RGB2GRAY)
# different of absolute
diff1 = cv2.absdiff(gray1, gray2)
diff2 = cv2.absdiff(gray2, gray3)
# giving logical product.
diff_and = cv2.bitwise_and(diff1, diff2)
# binalize black and white
_, diff_wb = cv2.threshold(diff_and, 15, 255, cv2.THRESH_BINARY)
# removing noise
diff = cv2.medianBlur(diff_wb, 3)
return diff
# geting image
def get_image(cap):
global ret
ret,img = cap.read() #ret;judgment whether or not can read it
if ret :
#pass
img = cv2.resize(img, (1270, 720))
img = cv2.flip(img,0)
cv2.imshow('image', img)
else:
return (ret,img)
return(ret,img)
""" ========= GUI ========== """
class Application(tk.Frame):
def __init__(self, master = None):
super().__init__(master)
master.title("Meteor(Fireball):Moving body detection program")
master.geometry("640x350")
self.pack()
self.create_widgets()
def create_widgets(self):
self.lb2 = tk.Label(self,text='\n 比較明画像と検知ログの保存フォルダーを選んでください。\n \nフォルダー名は英数文字のみ(日本語は不可です)。\n ファイル名には現在時刻がつきます\n',\
font=('MSゴシック', 10),width=300,anchor = 'w' )
self.lb2.pack()
self.bt2 = tk.Button(self, text = '保存フォルダー',width = 15, command = select_dir)
self.bt2.pack()
self.lb3 = tk.Label(self,text='\n \n 動体を判別する閾値(threshold)の値を入力してください。\n \nデフォルトは40です ',\
font=('MSゴシック', 10), width=300 )
self.lb3.pack()
self.var = tk.IntVar(value = 40)
#print(self.var)
self.spbox = tk.Spinbox(self, width = 6,from_ = 0, to = 300,textvariable = self.var)
self.spbox.pack()
self.bt3 = tk.Button(self, text = '実 行',width = 15,command = self.upd_scale)
self.bt3.pack()
self.lb4 = tk.Label(self,text='\n動画ファイルの'
'読み込みをやめるときは’q’キーを押してください。\n \nモニター画面は処理が終わったら何かキーを押すと消えます。',fg='blue',font=('MSゴシック', 10),)
self.lb4.pack()
self.lb5 = tk.Label(self,text='\n 実行ボタンを押す前に,保存フォルダーを選んでください。',fg = 'red',font=('MSゴシック', 10),width=300,anchor ='c')
self.lb5.pack()
self.bt4 = tk.Button(self, text = ' プロフラムを終了 ',width = 20,command = self.end)
self.bt4.place(x=450,y=10)
def upd_scale(self):
self.var=self.var.get()
main(self.var,)
def end(self):
root.destroy()
sys.exit()
root = tk.Tk()
app = Application(master = root)
app.mainloop()
ここから下は旧記事です(2020年1月公開)
惑星カメラとは,聞き慣れないかもしれませんが,最近は惑星を動画撮影からコンポジット(スタック)することを可能にした高感度天体用カメラが普及しています。 センサーはSONY だということですが,基盤を含め中国で作られている製品がシェアを占めています。通常は,望遠鏡に付けて惑星や星雲などを撮影します。センサーサイズは1/3インチ~1インチ程度で,監視カメラやドライブレコーダーなどのUSBカメラの超高感度版と言えるかと思います。レンズに,これらと同じCCTVタイプのものを装着すれば,景色ももちろん写ります。ネットで検索するとタムロン2.8mmF1.2CCTV,という1万円以下のものがあったので,購入してみました。そして,流星を撮ろうと考えたのですが,およそ1/8秒のシャッター速度でも1分間に480フレーム,15分も撮影すると数ギガバイトもメモリーを食うことになると躊躇していました。市販の流星(UFO)などの動体検知ソフトもあるのですが,これらのカメラには適合しないような仕様になっています。
ということで,pythonのモジュールで画像を扱えるOpenCVを使って,動体検知プログラムを組んでみました。撮影しながらの動体検知をすることはできませんが,キャプチャーソフトを使って数時間撮影した動画ファイルを読み込んで,動体(流星など)を検知したらフレーム数(位置)と画像を比較明合成して出力できる,というものです。動画の保存には,外付けのSSDかHDDがあったほうが良いと思います。もちろん人が動画を見て流星が写っているか監視しても良いのですが,パソコンにやらせる方がはるかに楽になります。見ての通り,pythonのコード なので利用してもらうには多少プログラミングの知識も必要ですが, ファイルの読み込み部分はGUIになっているので,numpyが同梱されているpython(Anaconda)とOpenCVモジュールのインストール(pip install opencv-python です。)ができれば動くと思います。
どの程度の画像の変化を検知するかは,前後3枚の画像上の差分をとって白黒二値に変換し,変化のピクセル数を閾値としています。GUI画面でこの閾値(変数:スレッショルド=th:0~80)を変更できます。また,比較明画像の保存先のフォルダー名は日本語ではエラーになります。英数名のフォルダーを選ぶようにしてください。プログラムの作成には,ネット上の多くの方のコードを参考に(一部コピペ)させていただきました。下のほうにソースコードを貼り付けてあります。

- 上が,起動したときに現れるパネルです。
- 「ファイル選択ボタン」で,検知したい動画ファイル(AVIまたはMP4形式)を選んでください。
- 検知したら,その順番とフレームNo.,動体で生じた差分のピクセル数(大きさ)を記録するCSVファイルと比較明画像を保存するフォルダーを指定します(「保存フォルダーボタン」)
- 動体を検知する,閾値(スレッショルド)の量を変えることができます。飛行機などはデフォルト(15以上でも)で検知しますが,暗い流星などは10位の方が良いかもしれません。明るい雲が多いとほとんどのフレームを拾ってしまうので,そういうときは50位にする方が良いです。
- 「実行ボタン」で動体検知を開始します。
- 動画のモニターフレーム(image)と比較明画像(result:検知したとき)の2画面が現れるはずです。
- 途中で止めたいときは,キーボードの”q”キーを押すと,終了します。
- すべてのフレームが表示し終わると,コンソールに「おわりますか?」と表示されます。なにかキーを押すと画面は消えます。
- 検知結果は,3.で指定したフォルダーにプログラム動作開始時の時分が付いたcsvファイルに保存されています。
- 「プログラムを終了」ボタンまたは,右上の「×」でプログラムを終わらせます。
- 例外処理を行っていないので,上記以外の動作によっては,エラーが出ることがあります。終了ボタンで,再度実行してください。
"""
Created on Sun Jan 5 11:29:32 2020
Moving body detection program for Meteor use(detecting 3 frame)
file type MP4 & AVI implement user interface
"""
import os
import sys
import datetime
import cv2
import csv
import tkinter as tk
import numpy as np
from tkinter import filedialog as tkFileDialog #python3
def select_file():
global filenameselect
#root.withdraw()
fTyp = [(' AVI MP4 file','*.avi;*.mp4')]
iDir = os.path.abspath(os.path.dirname(__file__))
print(iDir)
filenameselect = tkFileDialog.askopenfilename(filetypes=fTyp,initialdir=iDir,title = "select file")
root.update()
def select_dir():
global dirname
#root.withdraw()
iDir = os.path.abspath(os.path.dirname(__file__))
dirname = tkFileDialog.askdirectory(initialdir=iDir,title = "select folder saving log and result images")
root.update()
# Function of the comparison light composition
def Lighten(bg_img, fg_img):
result = np.zeros(bg_img.shape)
# It is a comparison and reference with Boolean value sequence
is_BG_lighter = bg_img > fg_img
result[is_BG_lighter] = bg_img[is_BG_lighter]
result[~is_BG_lighter] = fg_img[~is_BG_lighter]
return result
def main(var):
# Starting the capture of the video file
cap = cv2.VideoCapture(filenameselect)
# Three frames of the beginning
img1 = img2 = img3 = get_image(cap)[1]
# Initial composition image
dst = np.zeros(img1.shape)
th = var # threshold
print('th=',th)
date = datetime.datetime.now()
num = 1
with open(dirname + "/" + "log_" + str(date.hour) + "_" + str(date.minute) + ".csv", "a", newline='') as file:
w = csv.writer(file, delimiter=",")
while True:
# If "q" key is pushed, it is finished on the way
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# If an image cannot read, it is finished
if not ret:
break
# provide difference
diff = check_image(img1, img2, img3)
# If there is difference that counted more than a value of th, judge movement that there was
cnt = cv2.countNonZero(diff)
if cnt > th:
w.writerow([num, 'fr',int(cap.get(cv2.CAP_PROP_POS_FRAMES)),'cnt',cnt])
print(f"detects movement {num} fr= {int(cap.get(cv2.CAP_PROP_POS_FRAMES))} cnt= {cnt}")
#cv2.imshow('meteo-image', img2)
# photographs to image
bg_img = dst / 255 #clip 0~1
fg_img = img2 / 255
# to the comparison light composition
result = Lighten(bg_img, fg_img).clip(0,1)
dst = result*255
cv2.imshow('result',result)
num += 1
img1,img2,img3 = (img2,img3,get_image(cap)[1])
cv2.imwrite(dirname + "/" + "metro_" + str(num) +".jpg" , dst) #書き込み
# final work
file.close
print('Close it? push any key ')
cv2.waitKey(0)
cap.release()
cv2.destroyAllWindows()
# the function of detecting
def check_image(img1, img2, img3):
# convert into a gray scale image
gray1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY)
gray3 = cv2.cvtColor(img3, cv2.COLOR_RGB2GRAY)
# different of absolute
diff1 = cv2.absdiff(gray1, gray2)
diff2 = cv2.absdiff(gray2, gray3)
# giving logical product.
diff_and = cv2.bitwise_and(diff1, diff2)
# binalize black and white
_, diff_wb = cv2.threshold(diff_and, 15, 255, cv2.THRESH_BINARY)
# removing noise
diff = cv2.medianBlur(diff_wb, 3)
return diff
# geting image
def get_image(cap):
global ret
ret,img = cap.read() #ret;judgment whether or not can read it
if ret :
#pass
img = cv2.resize(img, (962, 720))
cv2.imshow('image', img)
else:
return (ret,img)
return(ret,img)
""" ========= GUI ========== """
class Application(tk.Frame):
def __init__(self, master = None):
super().__init__(master)
master.title("Meteor(Fireball):Moving body detection program")
master.geometry("640x420")
self.pack()
self.create_widgets()
def create_widgets(self):
self.lb1 = tk.Label(self,text='\n キャプチャーした動画から流星や(動体)を検知するプログラムです。\n \n読み込む動画のファイルタイプは,AVIまたはMP4形式です。',\
width=300,font=('MSゴシック', 11),anchor = 'nw' )
self.lb1.pack()
self.bt1 = tk.Button(self, text = 'Select file', width = 15, command = select_file)
self.bt1.pack()
self.lb2 = tk.Label(self,text='\n 比較明画像と検知ログの保存フォルダーを選んでください。\n \nフォルダー名は英数文字のみ(日本語は不可です)。\n ファイル名には現在時刻と検知したフレームの数がつきます\n',\
font=('MSゴシック', 10),width=300,anchor = 'w' )
self.lb2.pack()
self.bt2 = tk.Button(self, text = 'Save folder',width = 15, command = select_dir)
self.bt2.pack()
self.lb3 = tk.Label(self,text='\n \n 動体を判別する閾値(threshold)の値を入力してください。\n \n(0~80)デフォルトは15です ',\
font=('MSゴシック', 10), width=300 )
self.lb3.pack()
self.var = tk.IntVar(value = 15)
#print(self.var)
self.spbox = tk.Spinbox(self, width = 6,from_ = 0, to = 80,textvariable = self.var)
self.spbox.pack()
self.bt3 = tk.Button(self, text = 'Excute',width = 15,command = self.upd_scale)
self.bt3.pack()
self.lb4 = tk.Label(self,text='\n動画ファイルの読み込みをやめるときは’q’キーを押してください。\n \nモニター画面は処理が終わったら何かキーを押すと消えます。',fg='blue',font=('MSゴシック', 10),)
self.lb4.pack()
self.lb5 = tk.Label(self,text='\n 実行ボタンを押す前に,ファイルとフォルダーを選んでください。',fg = 'red',font=('MSゴシック', 10),width=300,anchor ='c')
self.lb5.pack()
self.bt4 = tk.Button(self, text = ' Finish program ',width = 20,command = self.end)
self.bt4.place(x=450,y=120)
def upd_scale(self):
self.var=self.var.get()
main(self.var)
def end(self):
root.destroy()
sys.exit()
root = tk.Tk()
app = Application(master = root)
app.mainloop()