当初は,高感度の惑星撮影用のCMOSカメラを流星撮影に応用して,動画撮影したファイルからPythonのOpenCVモジュールを使って動体検知するプログラムを作りました。さらに2021年暮れになって,今までwebカメラとしては利用できなかったCOMSカメラにDirectshow版のドライバーが公開されたので(playerone社のカメラだけですが)リアルタイムで,パソコンでキャプチャーしつつ動体(流星)を検知して記録(静止画として保存,その後動画や比較明合成画像に)するプログラムに書き直しました。

 ところが,昨年(2022年)になって,AtomCAM2という,いわゆる防犯カメラ(あるいは見守りカメラ)で流星をとらえる,という方法が流星研究者などの間で話題となり,私も1台購入して使うようになりました。なんと,1台3~4000円のこのAtomCAM2(AC2)のナイトビジョンを使うと,都会の空でも明るい流星をとらえることができ,しかも防水機能とWi-Fi環境で屋外に備え付けで四六時中データを保存できます。
 すでに1年半になりますが,雨ざらしでも順調に稼働しています。この流星キャプチャーソフトは,アマチュア天文家でエンジニアでもある長谷川均さんが作成したものです。私が,最初に作った動体検知のアルゴリズムは,前後の画像を白黒2値化してその量の面積差から動体を検知するという単純なもので,少し雲などが流れてきても検知してしまうという欠点(夜空では致命的)があります。長谷川さんのatomcam.pyは,1秒間のframeを合成して,線状の構造を検知するので流星のみをうまく検知できるすぐれものです。

 このプログラムとアルゴリズムを借りて,高感度のCMOSカメラで流星を検知しようと,いろいろ試したのですが,Pythonのカメラ制御や画像処理モジュールのOpenCVでは限界があり,CMOSカメラ制御をあきらめ,連続的に撮影した動画ファイルから流星をディテクト(検知)する方法で,今年(2023年)のふたご座流星群に臨みました。しかし,天気が悪く,薄雲を通してのテスト撮影になりました。そこで感じたのはCMOSよりAtomCAMのほうが感度が高く(多分赤外線感度が高い?)CMOSでの流星撮影より八ヶ岳の暗い空でも動画撮影には防犯カメラの方が優れていることでした。この記事を読んで問い合わせを頂いた方が,H.Viewというブランドのカラー高感度防犯カメラを使って流星撮影をされていると聞き,すごくきれいな(800万画素のカラー)の火球写真を見せていいただいたので,私もと暮れに上の写真のようなH.Viewを購入しました(値段はAtomCAM2の三個分ほど)。こちらは,ネットワークを通じて画像を取り込みますので,長谷川さんのatomcam.pyが使えるわけです(センサーサイズに合わせてプログラムを修正)。

今のところ,CMOSカメラなどで,SharpCapを使って外づけハードディスクやSSDに長時間録画した動画から流星を検知するプログラムと,AtomCAM2以外でも高感度の防犯カメラでNetwork対応のものからリアルタイムで検知するGUI付きのプログラムができています。どちらもPythonの知識なしでも使える実行形式のファイルを用意しました。予知なくバージョンアップしますが,とくに問題なく試していただけるかと思います。前者のために,短い動画をひとまとめにつなげるスクリプトと実行ファイルもありますので,興味があればお問い合わせください。→お問い合わせへ

CMOSカメラなど動画撮影ファイル検知ソフト:こちら→ダウンロードページへ。下のソースコード(ソースコードもときどき書き換えます)。

NetCamera-streamingMD.exeのGUI画面

高感度ネットワーク防犯カメラ用リアルタイム検知ソフト:最低限RTSPの知識が必要ですが,Pythonの知識なしでも利用できます。こちら→ダウンロードページへ。ソースコードは,この一個下にあります。

AtomCAM2用リアルタイム検知ソフト:タイムスタンプ(時刻表示)部分のマスクを個々に設定するのは面倒なのでAtomCAM2用に設定したものです。→ダウンロードページへ。ソースコードはほぼ同じです。

#-------------------------------------------------------------------------------
# Purpose:  meteoro detect from COMS VideoFile
#
# Author:      souta
# Created:     09/12/2023
#-------------------------------------------------------------------------------

from pathlib import Path
import sys
import os
import numpy as np
import cv2
from tkinter import filedialog as tkFileDialog #python3


# 行毎に標準出力のバッファをflushする。
sys.stdout.reconfigure(line_buffering=True)

fTyp = [(' AVI MP4 MTS file','*.avi;*.mp4;*MTS')]
iDir = os.path.abspath(os.path.dirname(__file__))       # 読み込むファイル名
filename = tkFileDialog.askopenfilename(filetypes=fTyp,initialdir=iDir,title = "select file")
print(filename)
iDir = os.path.abspath(os.path.dirname(__file__))      # 出力ディレクトリ
output_dir = tkFileDialog.askdirectory(initialdir=iDir,title = "select folder saving result images")
exp = 2                    # 検出するフレーム単位(秒)

def composite(list_images):
    """画像リストの合成(単純スタッキング)
    list_images: 画像データのリスト
    Returns:
      合成された画像
    """
    equal_fraction = 1.0 / (len(list_images))

    output = np.zeros_like(list_images[0])

    for img in list_images:
        output = output + img * equal_fraction

    output = output.astype(np.uint8)

    return output

def brightest(img_list):
    """比較明合成処理
    Returns:
      比較明合成された画像
    """
    output = img_list[0]

    for img in img_list[1:]:
        output = cv2.max(img, output)

    return output

def diff(img_list, mask):
    """画像リストから差分画像のリストを作成する。
      img_list: 画像データのリスト
      mask: マスク画像(2値画像)
    Returns:
      差分画像のリスト
    """
    diff_list = []
    for img1, img2 in zip(img_list[:-2], img_list[1:]):
        img1 = cv2.bitwise_or(img1, mask)
        img2 = cv2.bitwise_or(img2, mask)
        diff_list.append(cv2.subtract(img1, img2))

    return diff_list

def detect(img, min_length):        # 流星を線状パターンとして検知
    blur_size = (5, 5)
    blur = cv2.GaussianBlur(img, blur_size, 0)
    canny = cv2.Canny(blur, 100, 200, 3)

    # The Hough-transform algo:
    return cv2.HoughLinesP(canny, 1, np.pi/180, 25, minLineLength=min_length, maxLineGap=5)

def main():
    global capture
    global mask
    global min_length
    global HEIGHT
    global WIDTH
    global FPS
 # video CMOS_File
    capture = cv2.VideoCapture(filename)
    HEIGHT = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
    WIDTH =  int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
    FPS = int(capture.get(cv2.CAP_PROP_FPS))

    print(HEIGHT, WIDTH, FPS)
        # 時刻表示部分のマスクを作成
    zero = np.zeros((HEIGHT, WIDTH, 3), np.uint8)
            # mask timestam 255, 255, 255), -1)
    mask = cv2.rectangle(
                  zero,(0, 0), (700, 80), (255, 255, 255), -1)
                  # upper left (0, 0), (300, 30)
                  # (1390, 1010), (1920, 1080),   AtomcCAM

    min_length = 50

    meteor(exp, output_dir, filename)

def save_movie(img_list, pathname):
    size = (WIDTH, HEIGHT)
    fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
    video = cv2.VideoWriter(pathname, fourcc, FPS, size)
    for img in img_list:
        video.write(img)

    video.release()

def meteor(exposure, output, filename):
    output_dir = Path(output)
    output_dir.mkdir(exist_ok=True)
    tbl = str.maketrans('/', '_',':')
    filename = filename.translate(tbl)
    filename = filename.strip('.MP4.mp4.AVI.avi')
    num_frames = int(FPS * exposure)
    composite_img = None
    count = 0
    while (True):
        ret, frame = capture.read()
        if not ret:
            break
        cv2.imshow("frame",cv2.resize(frame,(640,400)))
        img_list = []
        if ret:
            for n in range(num_frames):
                try:
                    ret,frame = capture.read()
                    if not ret:
                        continue

                except Exception as e:
                    print(e, file=sys.stderr)
                    continue

                img_list.append(frame)

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

        number = len(img_list)
        count += 1
        print(count)

        if number > 2:

            try:
                diff_img = brightest(diff(img_list, mask))
                if detect(diff_img, min_length) is not None:

                    obs_time = "{}-{}".format(filename, str(count).zfill(2))
                    print('{} A possible meteor was detected.'.format(obs_time))
                    path_name = str(Path(output_dir, obs_time + ".jpg"))
                    composite_img = brightest(img_list)
                    #cv2.imshow(" CMOS", cv2.resize(composite_img,(640,400)))
                    cv2.imwrite(path_name, composite_img)

                        # 検出した動画を保存する。
                    movie_file = str(
                            Path(output_dir, "movie-" + obs_time + ".mp4"))
                    save_movie(img_list, movie_file)

            except Exception as e:
                    # print(traceback.format_exc(), file=sys.stderr)
                print(e, file=sys.stderr)
    capture.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    main()

 

こちらGUI(ユーザーインターフェース)付きで,ネットワークカメラからのストリーミングバージョンです。カメラの設定でrtspアドレスを入力して使います。タイムスタンプが左上(長さ700,幅80px)に表示される場合のマスクが前提になっています。

#-------------------------------------------------------------------------------
# Name:
# Purpose:   Meteor-Detecter Network-camera streaming Vesion
# Refer from: https://github.com/kin-hasegawa/meteor-detect
# Author:      souta
#
# Created:     20/12/2023
# Copyright:   (c) souta 2023
# Licence:     <your licence>
#-------------------------------------------------------------------------------

from pathlib import Path
import sys
import os
from datetime import datetime, timedelta, timezone
import time
import numpy as np
import cv2
import tkinter as tk
from tkinter import filedialog as tkFileDialog #python3
# マルチスレッド関係
import threading
import queue
import traceback

# 行毎に標準出力のバッファをflushする。
sys.stdout.reconfigure(line_buffering=True)


def composite(list_images):

    equal_fraction = 1.0 / (len(list_images))

    output = np.zeros_like(list_images[0])

    for img in list_images:
        output = output + img * equal_fraction

    output = output.astype(np.uint8)

    return output

def average(list_images):
    img_list = []

    for img in list_images:
        img_list.append(img)

    return np.average(img_list, axis=0).astype(np.uint8)


def brightest(img_list):
    output = img_list[0]

    for img in img_list[1:]:
        output = cv2.max(img, output)

    return output


def diff(img_list, mask):
    diff_list = []
    for img1, img2 in zip(img_list[:-2], img_list[1:]):
        if mask is not None:            img1 = cv2.bitwise_or(img1, mask)
        img2 = cv2.bitwise_or(img2, mask)
        diff_list.append(cv2.subtract(img1, img2))

    return diff_list


def detect(img, min_length):
    blur_size = (5, 5)
    blur = cv2.GaussianBlur(img, blur_size, 0)
    canny = cv2.Canny(blur, 100, 200, 3)

    # The Hough-transform algo:
    return cv2.HoughLinesP(canny, 1, np.pi/180, 25, minLineLength=min_length, maxLineGap=5)


class VideoCam:
    def __init__(self, video_url, dirname, minLineLength, to):
        self._running = False
        # video device url or movie file path
        self.capture = None
        self.source = "Live"
        self.url = video_url

        self.connect()
        # opencv-python 4.6.0.66 のバグで大きな値(9000)が返ることがあるので対策。
        self.FPS = min(int(self.capture.get(cv2.CAP_PROP_FPS)), 60)
        self.HEIGHT = int(self.capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
        self.WIDTH = int(self.capture.get(cv2.CAP_PROP_FRAME_WIDTH))
        print("FPS=",self.FPS)
        print(self.HEIGHT,":",self.WIDTH)

        # 出力先ディレクトリ
        if dirname:
            output_dir = Path(dirname)
            output_dir.mkdir(exist_ok=True)
        else:
            output_dir = Path('./')
        self.output_dir = output_dir

        # MP4ファイル再生の場合を区別する。
        self.mp4 = Path(video_url).suffix == '.mp4'

        # 終了時刻を設定する。
        now = datetime.now()
        try:
            t = datetime.strptime(to, "%H%M")
        except:
            print("終了時刻を正しく入力してください。")
            time.sleep(3)
            app.end()
        self.end_time = datetime(
               now.year, now.month, now.day, t.hour, t.minute)
        if now > self.end_time:
            self.end_time = self.end_time + timedelta(hours=24)

        print("# scheduled end_time = ", self.end_time)
        self.now = now

        # 時刻表示部分のマスクを作成
        zero = np.zeros((self.HEIGHT, self.WIDTH, 3), np.uint8)
        # mask timestamp 左上タイプ
        self.mask = cv2.rectangle(
                    zero, (0, 0), (700, 80), (255, 255, 255), -1)
                    # AtomCAM2のときは  (1390, 1010), (1920, 1080)
        self.min_length = minLineLength
        self.image_queue = queue.Queue(maxsize=200)

    def __del__(self):
        now = datetime.now()
        obs_time = "{:04}/{:02}/{:02} {:02}:{:02}:{:02}".format(
            now.year, now.month, now.day, now.hour, now.minute, now.second
        )
        print("# {} stop".format(obs_time))

        if self.capture:
            self.capture.release()
        cv2.destroyAllWindows()

    def connect(self):
        if self.capture:
            self.capture.release()

        url = self.url
        self.capture = cv2.VideoCapture(url)
        if not self.capture.isOpened():
            print("RTSPアドレスが見当たりません")
            app.end()

    def stop(self):
        # thread を止める
        self._running = False

    def queue_streaming(self):
        print("# streaming version started.")
        frame_count = int(self.capture.get(cv2.CAP_PROP_FRAME_COUNT))
        self._running = True
        while(True):
            try:
                ret, frame = self.capture.read()

                if ret:
                    # self.image_queue.put_nowait(frame)
                    now = datetime.now()
                    self.image_queue.put((now, frame))
                    if self.mp4:
                        current_pos = int(self.capture.get(
                            cv2.CAP_PROP_POS_FRAMES))
                        if current_pos >= frame_count:
                            break
                else:
                    self.connect()
                    time.sleep(5)
                    continue

                if self._running is False:
                    break
            except Exception as e:
                print(type(e), file=sys.stderr)
                print(e, file=sys.stderr)
                continue

    def dequeue_streaming(self, exposure=2, no_window=False):
        num_frames = int(self.FPS * exposure)

        while True:
            img_list = []
            for n in range(num_frames):
                (t, frame) = self.image_queue.get()
                # print(frame.shape)
                key = chr(cv2.waitKey(1) & 0xFF)
                if key == 'q':
                    self._running = False
                    return

                if self.mp4 and self.image_queue.empty():
                    self._running = False
                    return

                # exposure time を超えたら終了
                if len(img_list) == 0:
                    t0 = t
                    img_list.append(frame)
                else:
                    dt = t - t0
                    if dt.seconds < exposure:
                        img_list.append(frame)
                    else:
                        break

            if len(img_list) > 2:
                self.composite_img = brightest(img_list)
                if not no_window:
                    cv2.imshow('{}'.format(self.source), cv2.resize(self.composite_img,(640,400)))
                self.detect_meteor(img_list)

            # 終了時刻を過ぎたなら終了。
            now = datetime.now()
            if not self.mp4 and now > self.end_time:
                print("# end of observation at ", now)
                self._running = False
                return

    def detect_meteor(self, img_list):

        now = datetime.now()
        obs_time = "{:04}/{:02}/{:02} {:02}:{:02}:{:02}".format(
            now.year, now.month, now.day, now.hour, now.minute, now.second)

        if len(img_list) > 2:
            # 差分間で比較明合成を取るために最低3フレームが必要。
            # 画像のコンポジット(単純スタック)
            diff_img = brightest(diff(img_list, self.mask))
            try:
                # if True:
                if now.hour != self.now.hour:
                    # 毎時空の様子を記録する。
                    filename = "sky-{:04}{:02}{:02}{:02}{:02}{:02}".format(
                        now.year, now.month, now.day, now.hour, now.minute, now.second)
                    path_name = str(Path(self.output_dir, filename + ".jpg"))
                    mean_img = average(img_list)
                    # cv2.imwrite(path_name, self.composite_img)
                    cv2.imwrite(path_name, mean_img)
                    self.now = now

                detected = detect(diff_img, self.min_length)
                if detected is not None:
                    '''
                    for meteor_candidate in detected:
                        print('{} {} A possible meteor was detected.'.format(obs_time, meteor_candidate))
                    '''
                    print('{} A possible meteor was detected.'.format(obs_time))
                    filename = "{:04}{:02}{:02}{:02}{:02}{:02}".format(
                        now.year, now.month, now.day, now.hour, now.minute, now.second)
                    path_name = str(Path(self.output_dir, filename + ".jpg"))
                    cv2.imwrite(path_name, self.composite_img)

                    # 検出した動画を保存する。
                    movie_file = str(
                        Path(self.output_dir, "movie-" + filename + ".mp4"))
                    self.save_movie(img_list, movie_file)
            except Exception as e:
                print(traceback.format_exc())
                # print(e, file=sys.stderr)

    def save_movie(self, img_list, pathname):

        size = (self.WIDTH, self.HEIGHT)
        fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')

        video = cv2.VideoWriter(pathname, fourcc, self.FPS, size)
        for img in img_list:
            video.write(img)

        video.release()


def streaming_thread(url, dirname, minLength, to ): #RTSPストリーミング、及び動画ファイルからの流星の検出(スレッド版)
    print(url)

    VDC = VideoCam(url, dirname, minLength, to)

    now = datetime.now()
    obs_time = "{:04}/{:02}/{:02} {:02}:{:02}:{:02}".format(
        now.year, now.month, now.day, now.hour, now.minute, now.second
    )
    print("# {} start".format(obs_time))

    # スレッド版の流星検出
    t_in = threading.Thread(target=VDC.queue_streaming)
    t_in.start()

    try:
        exposure = 2
        VDC.dequeue_streaming(exposure)   #  露出時間は2秒 
    except KeyboardInterrupt:
        VDC.stop()

    t_in.join()
    return


"""        =========  GUI  ==========       """
class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)
        master.title("流星検知プログラム  Network対応カメラ用")
        master.geometry("680x360")
        self.pack()
        self.create_widgets()
        self.dirname = None
        self.iDir = None
    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 = self.select_dir)
        self.bt2.pack()
        self.lb3 = tk.Label(self,text='\n  \n  カメラのrtspアドレス(rtsp://...)を入力してください。\n  '  , font=('MSゴシック', 10), width=100 )
        self.lb3.pack()
        self.url = tk.StringVar(value = "rtsp://admin:hview@192.168./live/main" )
        #print(self.var)
        self.text1 = tk.Entry(self, width = 50, textvariable = self.url)
        self.text1.pack()
        self.minLength = tk.IntVar(value=60)
        self.lb6 = tk.Label(self, text='\n 検知する最小の長さ(minLength)を入力してください。',
                            font=('MSゴシック', 10), width=80)
        self.lb6.pack()
        self.spbox1 = tk.Spinbox(self, width=10, from_=0, to=100, textvariable= self.minLength)
        self.spbox1.pack()
        self.to = tk.StringVar(value="0600")
        self.lb7 = tk.Label(self, text='\n  終了時刻を入力してください(hhmm:形式) ',font=('MSゴシック', 10),width=40)
        self.lb7.pack()
        self.textbox2 = tk.Entry(self, width=10, textvariable=self.to)
        self.textbox2.pack()
        self.bt3 = tk.Button(self, text = '実 行',width = 15, command = self.upd_scale)
        self.bt3.place(x=480,y=240)
        self.bt4 = tk.Button(self, text = ' プロフラムを終了 ',width = 20, command = self.end)
        self.bt4.place(x=450,y=20)
        self.lb4 = tk.Label(self, text = ' 途中で終了するにはモニター画面\n をアクティブにして[q]キーを押す ', font=('MSゴシック', 10), width =  30)
        self.lb4.place(x=400,y=60)
    def upd_scale(self):
        self.url=self.url.get()
        self.minLength=self.minLength.get()
        self.to=self.to.get()
        streaming_thread(self.url, self.dirname, self.minLength, self.to)
    def select_dir(self):
        self.iDir = os.path.abspath(os.path.dirname(__file__))
        self.dirname = tkFileDialog.askdirectory(initialdir=self.iDir, title = "select folder saving result images")
        root.update()
    def end(self):
        root.destroy()
        sys.exit()

root = tk.Tk()
app = Application(master = root)
app.mainloop()

2022年6月29日には,大きな火球をAtomCAM2でとらえることができました。


ここから下は旧記事です(2020年1月公開)

惑星カメラとは,聞き慣れないかもしれませんが,最近は惑星を動画撮影からコンポジット(スタック)することを可能にした高感度天体用カメラが普及しています。 センサーはSONY だということですが,基盤を含め中国で作られている製品がシェアを占めています。通常は,望遠鏡に付けて惑星や星雲などを撮影します。センサーサイズは1/3インチ~1インチ程度で,監視カメラやドライブレコーダーなどのUSBカメラの超高感度版と言えるかと思います。レンズに,これらと同じCCTVタイプのものを装着すれば,景色ももちろん写ります。ネットで検索するとタムロン2.8mmF1.2CCTV,という1万円以下のものがあったので,購入してみました。そして,流星を撮ろうと考えたのですが,およそ1/8秒のシャッター速度でも1分間に480フレーム,15分も撮影すると数ギガバイトもメモリーを食うことになると躊躇していました。市販の流星(UFO)などの動体検知ソフトもあるのですが,これらのカメラには適合しないような仕様になっています。

2020年1月9日未明に出現した火球
2020年8月12日午前0時~3時45分までに動画撮影で写ったペルセウス座流星群の流星19個を比較明合成

ということで,pythonのモジュールで画像を扱えるOpenCVを使って,動体検知プログラムを組んでみました。撮影しながらの動体検知をすることはできませんが,キャプチャーソフトを使って数時間撮影した動画ファイルを読み込んで,動体(流星など)を検知したらフレーム数(位置)と画像を比較明合成して出力できる,というものです。動画の保存には,外付けのSSDかHDDがあったほうが良いと思います。もちろん人が動画を見て流星が写っているか監視しても良いのですが,パソコンにやらせる方がはるかに楽になります。見ての通り,pythonのコード なので利用してもらうには多少プログラミングの知識も必要ですが, ファイルの読み込み部分はGUIになっているので,numpyが同梱されているpython(Anaconda)とOpenCVモジュールのインストール(pip install opencv-python です。)ができれば動くと思います。

どの程度の画像の変化を検知するかは,前後3枚の画像上の差分をとって白黒二値に変換し,変化のピクセル数を閾値としています。GUI画面でこの閾値(変数:スレッショルド=th:0~80)を変更できます。また,比較明画像の保存先のフォルダー名は日本語ではエラーになります。英数名のフォルダーを選ぶようにしてください。プログラムの作成には,ネット上の多くの方のコードを参考にしました。こちらは古いので使用しないでください。参考までに置いておきます。

"""
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()