野鳥の自動撮影→撮影した写真と名前でファインチューニングチャレンジ

# はじめに
最近、野鳥にはまってます。

「ピーピー」
「ヒヨヒヨ」
「ツーツーピー」

鳥の鳴き声なんてどれも同じに聞こえそうなものですが、
野鳥撮影にはまってからは、不思議と聞き分けられるようになりました。
![download.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581710/288f0f28-9090-a639-e60e-8581f4f9d37d.jpeg)!

中でも「キビタキ」という鳥がすごい。
声が綺麗なうえに、他の鳥のなき声をマネするなんて特技もある。
ツクツクボウシをマネするキビタキに出会ったときは感動しました。
鳥の鳴き声の世界は、本当に奥が深いんですよね。

……と、画像処理を銘打っておきながら
鳥の音声の話ばかりをする理由はいたってシンプル。

「葉っぱ多すぎて小さい鳥なんか見つけられる気がしない。」

一匹見つけるのも一苦労なので、
今回はYolov8を使いながら野鳥の自動撮影を目指します。
本記事では、その失敗と過程を記録用に残します。

# 環境
ここでいう環境は開発環境ではなく撮影環境。
山奥の家の室内にカメラを設置して、窓に向けて撮影します。
位置の関係上、日差しの影響がものすごい

# 概要と目標
何かを学ぶとき、漠然と勉強するよりも
何か明確な目的があったほうが、モチベーションが続くもの。
野鳥自動撮影チャレンジを通じてたくさんの野鳥画像を蓄積して、
ViTや画像関連の様々なタスクにつなげられたらなぁと期待しています。

とにもかくにも今回は野鳥の自動撮影。
野鳥自動撮影チャレンジと題しまして、
以下の３段階に分けて細々とやっていこうと思います。

●１段階：野鳥の自動撮影
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581710/b33beea1-49d0-187f-d659-bd6b06b11bb4.png)　　
```
　　１回目：opencvの動体検知編
　　　　→失敗
　　２回目：Yolo v8編
　　　　→失敗
　　３回目：プログラムと機材整理編
　　　　→どうなる・・・？！
```
●２段階：撮影写真のラベリング
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581710/cefe5aed-aeae-f2a8-0197-bc472b4f7458.png)
```
　　→まだ到達してない
```

●３段階：野鳥の名前を学習
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581710/d492ab77-4c59-63ea-8d71-cff6c2e1c021.png)
```
　　→まだ到達してない
```

# ３回目：プログラムと機材整理編
いきなり最後のチャレンジから説明。
1回目2回目の失敗は以降の節に記載しています.

教訓を要約すれば、
「カメラだめすぎ」
「いままでのソース問題起きた時何も分析できなさすぎ」
「低スペックPCでリアルタイム処理とか無茶すぎ」
「でもYoloはさいこう！」

という結論に達したので、
Yoloの部分はそのままにログやら画像保存やら整理します。

## 機材
カメラ：高いＷＥＢカメラ　８０００円
　　　　　焦点距離 5-50mm　10倍ズーム
　　　　　解像度: 1920x1080
　　　　　
ＰＣ　：やすいミニＰＣ　　　２００００円
　　　　　メモリ:6GB
　　　　　CPU   :J4125 4コア 4スレッド


## 実装
概要としては
時間を管理しながら無限ループをぐるぐる回して、
下の処理を繰り返すだけ。

１．朝４時～１９時の間は録画
２．夜２０時に録画したデータに対して野鳥検出

以下、もしかしたら今後何かに転用できるかもしれなくもないから
主要なクラスごとに分けて記載。

### ロガー
ロガーです。ネットに転がってたものを拝借

```logger.py
class Logger():
	def __init__(self, filename='', format='', level=10, stdout=False):
		self.filename = self.__create_log_path() if filename == '' else filename
		self.format = '[%(asctime)s]\t[%(levelname)s]\t%(message)s' if format == '' else format

		# ロガーを生成
		self.logger = logging.getLogger(os.path.basename(__file__))
		# エラーレベルを設定
		self.logger.setLevel(level)
		# ハンドラを登録
		self.set_handler(self.filename, self.format, stdout)

	def set_handler(self, filename, format, stdout=False):
		fmt = logging.Formatter(format)  # フォーマッタの作成
		fh = logging.FileHandler(filename)  # ファイルハンドラの生成
		fh.setFormatter(fmt)  # ファイルハンドラにフォーマッタを登録
		self.logger.addHandler(fh)  # ロガーにファイルハンドラを登録

		# 標準出力の出力指示がされていたら、標準出力用のハンドラとフォーマッタを登録
		if stdout:
			sd = self.logger.StreamHandler(sys.stdout)
			sd.setFormatter(fmt)
			self.logger.addHandler(sd)

	def setLevel(self, level):
		self.logger.setLevel(level)
	def info(self, message):
		self.logger.info(message)
	def error(self, message):
		self.logger.error(message)
	def __create_log_path(self):
		folder = os.path.join(self.__get_parent(__file__), 'logs')
		name_without_ext = self.__get_name_without_ext(__file__)
		return os.path.join(folder, name_without_ext + '.log')
	def warning(self, message):
		self.logger.warning(message)
	def __get_parent(self, path):
		return '/'.join(os.path.dirname(path).replace('\\', '/').split('/')[0:-1])
	def __get_name_without_ext(self, filename):
		return os.path.splitext(os.path.basename(filename))[0]


```

## 時間の管理
pythonのscheduleライブラリを使って、
時間帯ごとの処理を分けようと思っていたのだけれど、
複数のタスクを実行させたり、二重ループ重ねると動かない現象が起きたので
地道にやります。

```test.py
	cst_HMS_Since = HMS(4,0,0)
	cst_HMS_Until = HMS(4,59,50)
	cst_HMS_AfterProc = HMS(20,0,0)
	cst_HMS_FinalProc = HMS(21,0,0)
	cst_HMS_Until_MAX = HMS(19,30,0) #MAX 録画はこの時間までしか行わない。
```

cst_HMS_Sinceは、録画を開始する時間。（朝４時）
cst_HMS_Untilは、録画を終了する時間。（朝４時５９分）
cst_HMS_AfterProcは、録画完了後に後処理を開始する時間
cst_HMS_FinalProcは、次の日の処理のために初期化する時間

この設定だと朝４時～朝４時５９分の間まで録画をする設定。

当プログラムでは１時間おきに録画データ出力するので、
１時間おきにここのパラメータが１時間ずつ増えて更新される。


```Timekeeper.py
class Common():
	@classmethod
	def getTodayTime(self,h,m,s):
		year = date.today().year
		month = date.today().month
		hour = h
		minute = m
		second = s
		set_until_time = datetime(year, month, date.today().day, hour, minute, second)
		return set_until_time

	@classmethod
	def getTommorrowTime(self,h,m,s):
		year = date.today().year
		month = date.today().month
		hour = h
		minute = m
		second = s
		set_until_time = datetime(year, month, date.today().day+1, hour, minute, second)
		return set_until_time

class TimeKeeper():
	class HMS():
		def date(self,date):
			return
		def __init__(self,_h,_m,_s):
			self.h = _h
			self.m = _m
			self.s = _s

	TIME_SCHEDULE = []
	cst_HMS_Since = HMS(4,0,0)
	cst_HMS_Until = HMS(4,59,50)
	cst_HMS_AfterProc = HMS(20,0,0)
	cst_HMS_FinalProc = HMS(21,0,0)
	cst_HMS_Until_MAX = HMS(19,30,0) #MAX 録画はこの時間までしか行わない。

	def __init__(self):
		self.__initialize()

	def __initialize(self):
		self.TIME_SCHEDULE = []

		# 日中録画
		self.SinceRecTime = self.__setTime(self.cst_HMS_Since)
		self.UntilRecTime = self.__setTime(self.cst_HMS_Until)
		self.RecMaxTime = self.__setTime(self.cst_HMS_Until_MAX)

		# 指定時間に画像処理
		self.AfterProcTime = self.__setTime(self.cst_HMS_AfterProc)

		# 日付またぎの処理
		self.FinalProcTime = self.__setTime(self.cst_HMS_FinalProc)

	def __setTime(self,HMS):
		return Common.getTodayTime(h=HMS.h, m=HMS.m, s=HMS.s)

	def __Next_h(self,date):
		return self.__setTime(self.HMS(date.hour  + 1, date.minute, date.second))

	def IsRecording(self):
		return self.SinceRecTime in self.TIME_SCHEDULE

	def add(self):
		self.TIME_SCHEDULE.append(self.SinceRecTime)

	def update_hour(self):
		self.SinceRecTime = self.SinceRecTime + timedelta(hours=1)
		self.UntilRecTime = self.UntilRecTime + timedelta(hours=1)

	def update_minute(self):
		self.SinceRecTime = self.SinceRecTime + timedelta(minutes=1)
		self.UntilRecTime = self.UntilRecTime + timedelta(minutes=1)

	def update_day(self):
		self.__initialize()
		self.SinceRecTime = self.SinceRecTime + timedelta(days=1)
		self.UntilRecTime = self.UntilRecTime + timedelta(days=1)
		self.AfterProcTime = self.AfterProcTime + timedelta(days=1)
		self.FinalProcTime = self.FinalProcTime + timedelta(days=1)

```

## 画像処理部分

Yolo以外の処理を追加する場合はProcessingの抽象クラスから実装。

```processing.py
class Processing(metaclass=ABCMeta):
	@abstractmethod
	def processing(self):
		pass
	@abstractmethod
	def SummryResult(self):
		pass

	def Detect(self,Fol):
		now = datetime.now()
		logger = Logger(filename="{0:%Y_%m_%d}".format(now) + '.txt')

		# 今日のフォルダの画像を取得
		files = glob.glob(Fol.BigPath + "/RECORD*/*.png")
		for file in files:
			frame = cv2.imread(file)
			retImg = self.processing(frame)
			# 実行結果を格納
			for type in retImg:
				logger.info("検知結果：" + type + " FileName:" + file)
				cv2.imwrite(os.path.join(Fol.LitPath, os.path.basename(file) + type + ".png" ), frame)
		logger.info("最終結果：" + self.SummryResult())

import cv2
from ultralytics import YOLO
import supervision as sv
import numpy as np

class Yolo8(Processing):
	result = {}

	def __init__(self,Camera):
		cam_width = Camera.width
		cam_height = Camera.height

		cam_resolution = np.array([cam_width, cam_height]).astype(int)
		ZONE_POLYGON = np.array([
			[0, 0],
			[1, 0],
			[1, 1],
			[0, 1]
		])

		self.model = YOLO("yolov8m.pt")
		box_annotator = sv.BoxAnnotator(
			thickness=2,
			text_thickness=2,
			text_scale=1
		)
		zone_polygon = (ZONE_POLYGON * np.array(cam_resolution)).astype(int)
		zone = sv.PolygonZone(polygon=zone_polygon, frame_resolution_wh=tuple(cam_resolution))
		zone_annotator = sv.PolygonZoneAnnotator(
			zone=zone,
			color=sv.Color.red(),
			thickness=2,
			text_thickness=4,
			text_scale=2
		)

	def SummryResult(self):
		result = ''
		for key in self.result:
			result += key + '件数=' + str(self.result[key])
		return result

	def processing(self,frame):
		retImg = []
		result = self.model(frame, agnostic_nms=True)[0]
		detections = sv.Detections.from_yolov8(result)

		for bbox, _, conf, class_id, tracker_id in detections:
			det_class = self.model.names[class_id]
			if det_class == 'bird':
				if conf > 0.7:
					retImg.append('bird-'+str(conf).replace(".",""))
			if det_class == 'person':
				if conf > 0.7:
					retImg.append('person-'+str(conf).replace(".",""))

			if det_class in self.result.keys():
				self.result[det_class] += 1
			else:
				self.result[det_class] = 1

		return retImg
```

## 全体
全体は以下の通り
１時間ごとに録画処理をわけるという思想で作られてるが、
もしもこの仕様をなくしたい場合は
１時間先のスケジュールを設定する「Tkeeper.update_hour」を消して、
「cst_HMS_Since」、「cst_HMS_Until」を更新させる（未来の自分へのメモ）


```main.py
import schedule
import sys
import os
import time
from datetime import datetime,date,timedelta
from time import sleep
from abc import ABCMeta, abstractmethod
import cv2
import ffmpeg
import numpy as np
import logging
import time

class Logger():
	def __init__(self, filename='', format='', level=10, stdout=False):
		self.filename = self.__create_log_path() if filename == '' else filename
		self.format = '[%(asctime)s]\t[%(levelname)s]\t%(message)s' if format == '' else format

		# ロガーを生成
		self.logger = logging.getLogger(os.path.basename(__file__))
		# エラーレベルを設定
		self.logger.setLevel(level)
		# ハンドラを登録
		self.set_handler(self.filename, self.format, stdout)

	def set_handler(self, filename, format, stdout=False):
		fmt = logging.Formatter(format)  # フォーマッタの作成
		fh = logging.FileHandler(filename)  # ファイルハンドラの生成
		fh.setFormatter(fmt)  # ファイルハンドラにフォーマッタを登録
		self.logger.addHandler(fh)  # ロガーにファイルハンドラを登録

		# 標準出力の出力指示がされていたら、標準出力用のハンドラとフォーマッタを登録
		if stdout:
			sd = self.logger.StreamHandler(sys.stdout)
			sd.setFormatter(fmt)
			self.logger.addHandler(sd)

	def setLevel(self, level):
		self.logger.setLevel(level)
	def info(self, message):
		self.logger.info(message)
	def error(self, message):
		self.logger.error(message)
	def __create_log_path(self):
		folder = os.path.join(self.__get_parent(__file__), 'logs')
		name_without_ext = self.__get_name_without_ext(__file__)
		return os.path.join(folder, name_without_ext + '.log')
	def warning(self, message):
		self.logger.warning(message)
	def __get_parent(self, path):
		return '/'.join(os.path.dirname(path).replace('\\', '/').split('/')[0:-1])
	def __get_name_without_ext(self, filename):
		return os.path.splitext(os.path.basename(filename))[0]


class Common():
	@classmethod
	def getTodayTime(self,h,m,s):
		year = date.today().year
		month = date.today().month
		hour = h
		minute = m
		second = s
		set_until_time = datetime(year, month, date.today().day, hour, minute, second)
		return set_until_time

	@classmethod
	def getTommorrowTime(self,h,m,s):
		year = date.today().year
		month = date.today().month
		hour = h
		minute = m
		second = s
		set_until_time = datetime(year, month, date.today().day+1, hour, minute, second)
		return set_until_time

class TimeKeeper():
	class HMS():
		def date(self,date):
			return
		def __init__(self,_h,_m,_s):
			self.h = _h
			self.m = _m
			self.s = _s

	TIME_SCHEDULE = []
	cst_HMS_Since = HMS(4,0,0)
	cst_HMS_Until = HMS(4,59,50)
	cst_HMS_AfterProc = HMS(20,0,0)
	cst_HMS_FinalProc = HMS(21,0,0)
	cst_HMS_Until_MAX = HMS(19,30,0) #MAX 録画はこの時間までしか行わない。

	def __init__(self):
		self.__initialize()

	def __initialize(self):
		self.TIME_SCHEDULE = []

		# 日中録画
		self.SinceRecTime = self.__setTime(self.cst_HMS_Since)
		self.UntilRecTime = self.__setTime(self.cst_HMS_Until)
		self.RecMaxTime = self.__setTime(self.cst_HMS_Until_MAX)

		# 指定時間に画像処理
		self.AfterProcTime = self.__setTime(self.cst_HMS_AfterProc)

		# 日付またぎの処理
		self.FinalProcTime = self.__setTime(self.cst_HMS_FinalProc)

	def __setTime(self,HMS):
		return Common.getTodayTime(h=HMS.h, m=HMS.m, s=HMS.s)

	def __Next_h(self,date):
		return self.__setTime(self.HMS(date.hour  + 1, date.minute, date.second))

	def IsRecording(self):
		return self.SinceRecTime in self.TIME_SCHEDULE

	def add(self):
		self.TIME_SCHEDULE.append(self.SinceRecTime)

	def update_hour(self):
		self.SinceRecTime = self.SinceRecTime + timedelta(hours=1)
		self.UntilRecTime = self.UntilRecTime + timedelta(hours=1)

	def update_minute(self):
		self.SinceRecTime = self.SinceRecTime + timedelta(minutes=1)
		self.UntilRecTime = self.UntilRecTime + timedelta(minutes=1)

	def update_day(self):
		self.__initialize()
		self.SinceRecTime = self.SinceRecTime + timedelta(days=1)
		self.UntilRecTime = self.UntilRecTime + timedelta(days=1)
		self.AfterProcTime = self.AfterProcTime + timedelta(days=1)
		self.FinalProcTime = self.FinalProcTime + timedelta(days=1)

class Camera():
	def __init__(self,width,height,fps):
		self.width = width
		self.height  = height
		self.fps = fps

class FolderPath():
	def __init__(self,parentTime,childTime,desc):
		CST_FOLDER_DATA = './'
		# パス関連
		self.BigFolderNm = "{0:%Y_%m_%d}".format(parentTime)
		self.BigPath = os.path.join(CST_FOLDER_DATA, self.BigFolderNm)
		fmt = "{0:" + desc +"_%H_%M}"
		self.LitFolderNm = fmt.format(childTime)
		self.LitPath = os.path.join(self.BigPath, self.LitFolderNm)

		# フォルダ作成
		if not os.path.exists(self.BigPath):
			os.makedirs(self.BigPath)
		if not os.path.exists(self.LitPath):
			os.makedirs(self.LitPath)

import glob
class Processing(metaclass=ABCMeta):
	@abstractmethod
	def processing(self):
		pass
	@abstractmethod
	def SummryResult(self):
		pass

	def Detect(self,Fol):
		now = datetime.now()
		logger = Logger(filename="{0:%Y_%m_%d}".format(now) + '.txt')

		# 今日のフォルダの画像を取得
		files = glob.glob(Fol.BigPath + "/RECORD*/*.png")
		for file in files:
			frame = cv2.imread(file)
			retImg = self.processing(frame)
			# 実行結果を格納
			for type in retImg:
				logger.info("検知結果：" + type + " FileName:" + file)
				cv2.imwrite(os.path.join(Fol.LitPath, os.path.basename(file) + type + ".png" ), frame)
		logger.info("最終結果：" + self.SummryResult())

import cv2
from ultralytics import YOLO
import supervision as sv
import numpy as np

class Yolo8(Processing):
	result = {}

	def __init__(self,Camera):
		cam_width = Camera.width
		cam_height = Camera.height

		cam_resolution = np.array([cam_width, cam_height]).astype(int)
		ZONE_POLYGON = np.array([
			[0, 0],
			[1, 0],
			[1, 1],
			[0, 1]
		])

		self.model = YOLO("yolov8m.pt")
		box_annotator = sv.BoxAnnotator(
			thickness=2,
			text_thickness=2,
			text_scale=1
		)
		zone_polygon = (ZONE_POLYGON * np.array(cam_resolution)).astype(int)
		zone = sv.PolygonZone(polygon=zone_polygon, frame_resolution_wh=tuple(cam_resolution))
		zone_annotator = sv.PolygonZoneAnnotator(
			zone=zone,
			color=sv.Color.red(),
			thickness=2,
			text_thickness=4,
			text_scale=2
		)

	def SummryResult(self):
		result = ''
		for key in self.result:
			result += key + '件数=' + str(self.result[key])
		return result

	def processing(self,frame):
		retImg = []
		result = self.model(frame, agnostic_nms=True)[0]
		detections = sv.Detections.from_yolov8(result)

		for bbox, _, conf, class_id, tracker_id in detections:
			det_class = self.model.names[class_id]
			if det_class == 'bird':
				if conf > 0.7:
					retImg.append('bird-'+str(conf).replace(".",""))
			if det_class == 'person':
				if conf > 0.7:
					retImg.append('person-'+str(conf).replace(".",""))

			if det_class in self.result.keys():
				self.result[det_class] += 1
			else:
				self.result[det_class] = 1

		return retImg

def startRec(Tkeeper):
	now = datetime.now()
	Fol = FolderPath(parentTime = Tkeeper.SinceRecTime,childTime=Tkeeper.SinceRecTime,desc = "RECORD_")

	# パス関連
	LitPath = Fol.LitPath

	#キャプチャ開始
	video = cv2.VideoCapture(0)
	width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
	height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
	fps = video.get(cv2.CAP_PROP_FPS)
	Cam = Camera (width=width,height = height,fps=fps)

	while True:
		now = datetime.now()
		OutputNm = "{0:%Y_%m_%d___%H_%M_%S}".format(now) + '.png'
		check, frame = video.read()
		#保存
		cv2.imwrite(os.path.join(LitPath, OutputNm), frame)

		if now > Tkeeper.UntilRecTime:
			video.release()
			#次の1時間を録画
			Tkeeper.update_hour()
			break
		key = cv2.waitKey(1)
		if key == ord('q'):
			break

def AfterProcessing(Tkeeper):
	# カメラ情報取得
	video = cv2.VideoCapture(0)
	width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
	height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
	fps = video.get(cv2.CAP_PROP_FPS)
	Cam = Camera (width=width,height = height,fps=fps)
	video.release()
	# フォルダパス情報取得
	Fol = FolderPath(parentTime = Tkeeper.SinceRecTime,childTime=Tkeeper.SinceRecTime,desc = "DETECT_")
	# 検知
	proc = Yolo8(Cam)
	proc.Detect(Fol)
	pass

def main():
	TKeep = TimeKeeper()

	flg_Final = True
	flg_Proc = True

	now = datetime.now()
	logger = Logger(filename=	"{0:%Y_%m_%d}".format(now) + '.txt')
	logger.setLevel(logging.INFO)

	while True:
		now = datetime.now()
		# 1時間ごとにファイル作成
		if now < TKeep.RecMaxTime:
			if now > TKeep.SinceRecTime and now < TKeep.UntilRecTime:
				logger.info("録画処理開始")
				TKeep.add()  # 録画時間追加
				startRec(TKeep)
				logger.info("録画処理終了")

		#今日録画したデータを解析
		if 	flg_Proc  and now > TKeep.AfterProcTime:
			logger.info("解析開始")
			AfterProcessing(TKeep)
			logger.info("解析終了")
			flg_Proc = False

		# 日付またぎの処理
		if 	flg_Final and now > TKeep.FinalProcTime:
			logger.info("日またぎ処理開始 " +
						" FinalProcTime : "+ "{0:%Y_%m_%d___%H_%M_%S}".format(TKeep.FinalProcTime) +
						" SinceRecTime :  "+ "{0:%Y_%m_%d___%H_%M_%S}".format(TKeep.SinceRecTime) +
						" UntilRecTime : "+ "{0:%Y_%m_%d___%H_%M_%S}".format(TKeep.UntilRecTime))
			#明日の日付で初期化
			TKeep.update_day()
			flg_Final = False

			logger.info("日またぎ処理終了 " +
						" FinalProcTime : "+ "{0:%Y_%m_%d___%H_%M_%S}".format(TKeep.FinalProcTime) +
						" SinceRecTime :  "+ "{0:%Y_%m_%d___%H_%M_%S}".format(TKeep.SinceRecTime) +
						" UntilRecTime : "+ "{0:%Y_%m_%d___%H_%M_%S}".format(TKeep.UntilRecTime))
			now = datetime.now()
			logger = Logger(filename="{0:%Y_%m_%d}".format(now) + '.txt')
			logger.setLevel(logging.INFO)

if __name__ == "__main__":
	main()
```

## 結果
結果は・・・・まだためしていない！
今週末試そうわくわく


# １回目：opencvの動体検知編
先に書きますが、この章でやったことは失敗に終わります

## 機材
カメラ：やすいＷＥＢカメラ　１０００円
　　　　　画角  : 60度
　　　　　解像度: 640x480
　　　　　
ＰＣ　：やすいミニＰＣ　　　２００００円
　　　　　メモリ:6GB
　　　　　CPU   :J4125 4コア 4スレッド

## 実装
webカメラの画像を取得して、
前回フレームの画像と現在のフレームの差分が
どれくらい離れてるかをみて検知を行います。
opencvのよくある作例のやつです。

```ng1.py
import cv2
import time
from datetime import datetime

# 動体検知
static_back = None

cst_update_background=20

video = cv2.VideoCapture(0)
time_begin =  time.time()

while True:
	check, frame = video.read()
	gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
	gray = cv2.GaussianBlur(gray, (21, 21), 0)

	if static_back is None:
		static_back = gray
		continue

	if  (time.time() - time_begin) > cst_update_background:
		static_back = gray
		time_begin = time.time()
		continue

	diff_frame = cv2.absdiff(static_back, gray)
	thresh_frame = cv2.threshold(diff_frame, 30, 255, cv2.THRESH_BINARY)[1]
	thresh_frame = cv2.dilate(thresh_frame, None, iterations = 2)

	cnts,_ = cv2.findContours(thresh_frame.copy(),
					cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

	for contour in cnts:
		if cv2.contourArea(contour) < 10000:
			continue
		now = datetime.now()
		cv2.imwrite('./' +"{0:%Y%m%d%H%M%S}".format(now) +'.png',frame)

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

video.release()
cv2.destroyAllWindows()
```


# 結果
結果は前述のとおり失敗です。
理由は「誤撮影が多すぎる」から。

閾値をいじれば少しは収まりますが、
風が吹けばパシャリ、日商条件がかわればパシャリとなかなか難しい。

上記ソースでは、
「static_back」という動体検知の比較対象になる画像を
定期的に更新するようにしましたが、それでもやはり難しい。

とはいえ、日商条件がころころ変わらない環境、
常に風で揺れる物体がない環境であれば使えるソースだと思うので
記録として記載。


# ２回目：Yolo v8x編
先に書きますが、この章でやったことは失敗に終わります。
Yoloがわるいんじゃない。わたしがわるい。

## 機材
カメラ：やすいＷＥＢカメラ　１０００円
　　　　　画角  : 60度
　　　　　解像度: 640x480
　　　　　
ＰＣ　：やすいミニＰＣ　　　２００００円
　　　　　メモリ:6GB
　　　　　CPU   :J4125 4コア 4スレッド

先にこの節の結論を書くと、
「人用のWEBカメラで2メートル先の鳥を撮影したうえに、
しかも解像度640x480の画像で検知するとか無理だよね。」
という結論になります。

## 実装
流れとしては単純。Yoloで野鳥検知したら画像保存するだけです。

```ng2.py
import warnings
warnings.simplefilter('ignore')

import cv2
import time
from datetime import datetime

# https://www.geeksforgeeks.org/webcam-motion-detector-python/
# https://github.com/praneth123/Bird-Detection
# YoloV8
# pip install supervision==0.3.0
# pip install ultralytics

from ultralytics import YOLO
import supervision as sv
import numpy as np

video = cv2.VideoCapture(0)

cam_width = video.get(cv2.CAP_PROP_FRAME_WIDTH)
cam_height = video.get(cv2.CAP_PROP_FRAME_HEIGHT)

cam_resolution = np.array([cam_width,cam_height]).astype(int)
ZONE_POLYGON = np.array([
    [0, 0],
    [1, 0],
    [1, 1],
    [0, 1]
])

model = YOLO("yolov8m.pt")
box_annotator = sv.BoxAnnotator(
	thickness=2,
	text_thickness=2,
	text_scale=1
)
zone_polygon = (ZONE_POLYGON * np.array(cam_resolution)).astype(int)
zone = sv.PolygonZone(polygon=zone_polygon, frame_resolution_wh=tuple(cam_resolution))
zone_annotator = sv.PolygonZoneAnnotator(
	zone=zone,
	color=sv.Color.red(),
	thickness=2,
	text_thickness=4,
	text_scale=2
)

cst_update_background=20

while True:
	check, frame = video.read()

	result = model(frame, agnostic_nms=True)[0]
	detections = sv.Detections.from_yolov8(result)

	det_counter = {}
	person_boxes = []
	for bbox, _, conf, class_id, tracker_id in detections:
		det_class = model.model.names[class_id]
		print(det_class,conf)
		if det_class == 'bird':
			if conf > 0.7 :
				now = datetime.now()
				cv2.imwrite('./' + "bird_{0:%Y%m%d%H%M%S}".format(now) + '.png', frame)
		if det_class == 'person':
			if conf > 0.7 :
				now = datetime.now()
				cv2.imwrite('./' + "person_{0:%Y%m%d%H%M%S}".format(now) + '.png', frame)

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

video.release()
cv2.destroyAllWindows()
```

# 結果
Yoloはすごい。Yoloは悪くありません。
ネットで拾った画像を検知させると見事に「"bird"」と検知してくれました。

しかし結果は前述のとおり失敗です。
理由は３つあります。

１．カメラがだめすぎる。
640x480の解像度で、画角は60度と標準レンズなので画が遠い。
野鳥をとれたとしても、20ピクセル以下くらいしかない。

２．ソースがダメ。
確実に成功するだろうという謎の自身の元、
ドストレートなソースをかきましたが、
このソースだと、野鳥検知しないと何一つ画像を保存しないので、
問題が起きた時になにも改善ができない（致命的）。


３．２万円の低スペックPCでリアルタイム処理とか無理すぎ