ラズパイで動体検知カメラを作った

## はじめに
python も ラズパイも全くの独学で、３年程経過しました。いろいろ作っているので紹介できればと思います。コーディングなどは他人に見せるには恥ずかしいレベルと思われますが、誰かの参考になればと。

## 目次
1. まずはWindows下で
1. ラズパイの準備
1. カメラの起動
1. LCD表示
1. 使用ハードウエアまとめ
1. おまけ

## まずはWindows下で
動体検知カメラと云うことで、Motionをラズパイにインストールしたが、動作が良く解らない。
解らないのでコマンドにてサービスを停止した。停止したはずなのに9KB程度のjpegを吐き出し続けているのを発見した。これは自分には対処不能と判断しアンインストール。
心機一転　全て自分で作ろう！
なるべく小さくまとめたかったのでヘッドレスラズパイ＋ミニLCDという構成。
しかし、いきなりヘッドレスラズパイでのデバッグ作業は面倒だと思うので、まずは Windows の下でロジック確認。
ソースはこれ。
```ruby:camchk.py
# -*- coding: utf-8 -*-
# パラメタ調整は差分の要素数にて行う。
param1 = 2000    # 画像記録開始要素数-1
param2 = 60000   # 上限要素数（カメラ振れなど全体的な動きを除外）

import cv2
import numpy as np
import time
import keyboard

# カメラの指定
cap = cv2.VideoCapture(0)

# 解像度の指定
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

def cam2img(): # カメラ撮影
    #print("capture") # キャプチャの実施
    ret, frame = cap.read()
    if ret:     # frameに画像が入ったらretはTrue。入らなければFalse。
        camimg = frame
    return camimg

def imgchk(): # 画像の差分チェック

    # グレイスケールにする
    img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
    img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

    # 平均化してノイズの影響を除く
    averaged_img1 = cv2.blur(img1_gray,(8,8)) # 8x8ピクセルの計64ピクセルを輝度で平均化する
    averaged_img2 = cv2.blur(img2_gray,(8,8))

    # 差分画像を計算
    diff = cv2.absdiff(averaged_img2, averaged_img1)

    # しきい値を設定して大きな違いを検出する
    thresh = 15 #カメラの解像度により調整（15～50程度。高解像度ほど大きな値が良い？）
    diff[diff < thresh] = 0
    diff[diff >= thresh] = 255

    # 変化した部分の輪郭を見つける(グレイスケールの画像が対象)
    contours, hierarchy = cv2.findContours(diff, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 変化ピクセル数が多ければ明示する。あまりに多ければカメラ振れと見なし除外する。
    chk = np.count_nonzero(diff > 0)
    if chk > param1 and chk < param2:
        cnt = chk
        print("******* 変化ピクセル数 = ", cnt)
        # 本来はここで画像を記録する
    print("diff count = ",chk) # 処理状況確認用のインジケータ

    # 検出した輪郭付近をBOXで囲む
    showimg = np.array(img2) # img2を複製し表示用に。「showimg = img2」ではimg2にも影響が有った（理由不明）
    for contour in contours:
        rect = cv2.minAreaRect(contour)
        box = cv2.boxPoints(rect)
        box = np.intp(box)
        cv2.drawContours(showimg,[box],0,(0,0,255),2) # 赤枠で囲む

    # 表示
    cv2.imshow('box', showimg) # 差分確認画面(赤枠有り）
    #cv2.imshow('img2',img2) # 確認表示用（赤枠無し）
    cv2.waitKey(1) # キー入力受付

# main proc #############################################
img1 = cam2img() #比較対象１をセット
time.sleep(1)
img2 = cam2img() #比較対象２をセット
while True:
    imgchk() #差分チェック
    img1 = img2 #比較対象２を比較対象１へシフト
    img2 = cam2img() #比較対象２をセット
    if keyboard.is_pressed('q'): # 「q」キー押下で終了
        break

# 終了処理
cap.release()
cv2.destroyAllWindows()

```
キーボードの Ｑ を押すとプログラムは終了します。
## ラズパイの準備
動画処理と言うことで ラズパイZERO2 W と思ったが、逐次 LCD表示する処理を除けば
 ラズパイZERO W でも十分使える。こちらへは 32bit版しかインストールできないが。
### ラズパイ用のマイクロSDカードを作る
Windows下で RaspberryPi Imager を起動し、
①対象のデバイスを選択する（ ZERO2W / ZERO W )
②OSを選択する（ 64bit Light / 32bit Light ）
④次へ進み設定を編集する(Windows からSSH接続で操作する為、WiFiの設定は必須。)
	　ホスト名：zero2w64 (任意の値、ラズパイが複数ある場合の個体識別になる）
	　ユーザ名：pi
	　パスワード：xxxxxx
	　WiFi ssid : xxxxxxxx   (もちろん自分の部屋で使っているものを指定）
	　WiFi passkey : xxxxxxxxx
	　タイムゾーン：asia/tokyo
	　キーボードレイアウト：jp
⑤設定を保存し、「はい」にてSDカードに書き込む

### ラズパイの起動
#### SDカードをラズパイに挿入し、電源を接続する。
#### Windows 側から　TeraTermなどのターミナルを開く。
(初回はラズパイ側で何かやっているので、接続できるまで時間がかかる。3分以上待ってからターミナルを開く)
　　ホスト名：zero2w64.local
　　ユーザ名：pi
　　パスワード：xxxxxx
```  
sudo raspi-config
```
#### 今後必要となるInterfaceをENABLEにしておく

[Interface Options] --> [LegacyCamera]
[Interface Options] --> [SSH]
[Interface Options] --> [SPI]
 
#### SDカード容量をすべて使える様にする			

[Advanced Options] --> [Expand Filesystem]

reboot により設定が反映される。

#### アップデートや必要なソフトのインストール。
細かい部分はバージョンによって変わったりしているので、不要なものも有ると思われる。
```ruby:/home/pi
sudo apt update && sudo apt upgrade "約2分"
sudo apt install python-dev "約1分"
sudo apt install python3-pip   "約1分"
sudo apt install pip --upgrade
sudo pip install -U numpy "60分以上（既に入っている numpy を最新にする。64bit 版では
								　　　-U は不要。やると時間が掛かる）"
sudo apt install libatlas-base-dev
sudo apt install libatlas3-base
sudo pip install pillow
sudo pip install RPi.GPIO
sudo pip install spidev
sudo pip install picamera2 "(純正ができるまでの中継ぎ？よく解らないけどこれを使う)" 
sudo pip install opencv-python==4.5.1.48 "約1分"
  ```
**※ 64bit版はここまででOK**

今回のLCDについては、手引書にopencvのバージョン指定があったので、指定のバージョンをインストールした。ラズパイZER2W 64bitではすんなり行ったが、ラズパイZERO W 32bit では CV2 がインポートエラーとなり、いろいろと追加インストールして解決した。LCD無しであればopencvのバージョンも気にせずデフォルトでインストールすればすんなり動いたかもしれない。以下にラズパイZERO W 32bitで追加インストールした内容を残す。●印は明らかに必要だった。インストール毎に少しずつインポートエラーのモジュール名が変わるので、先に指定したライブラリに依存していたのかどうかは定かではないものがある。それぞれインストールしては python3へ入り import cvをやりながら、エラーが出なくなるまで進めた。エラーが出なければ以下を全てやる必要はない。
```ruby:/home/pi
●sudo apt-get install libopenjp2-7-dev
●sudo apt-get install libopenexr-dev
●sudo apt-get install libvtk7-qt-dev "（これはかなり時間が掛かった）"
sudo apt-get install libjasper-dev
sudo apt-get  install libtiff-dev
sudo apt-get  install libpython3-dev
sudo apt-get install python3-numpy python3-scipy python3-matplotlib
sudo apt-get install libjpeg-dev
sudo apt-get install libpng++-dev
sudo apt-get  install libwebp-dev
sudo apt-get  install libavresample-dev
sudo apt-get  install freeglut3-dev
```		
opencvのライブラリ名称は下記を参照させてもらいました。
#### RaspberryPiへのOpenCVインストール手順
https://qiita.com/wk_/items/8db529a6b24a955888db

※ pip -V で確認すると解るが pip がpython3 に紐づけされていなければ pip3 を使う。
今回は最初に python3 に対して pip をインストールしているので pip で良いと思う。 

#### Windowsとのファイルの受け渡しが便利なのでsambaを入れる
```
sudo apt install samba  約4分
sudo nano /etc/samba/smb.conf
``` 
設定ファイルの最後に追加
※共有名 zero2w64 や共有パス /home/pi やコメントは任意に変更
```        
[zero2w64]
  comment = Raspberry Pi zero2w 64bit
  path = /home/pi
  guest ok = yes
  read only = no
  browsable = yes
  force user = pi
```
path は、普通上記の様に「/home/pi」とするが、横着して Windows のエクスプローラから
ラズパイの中身を覗き見る行為は、ルート「/」を指定すると可能になる。自己責任で。

Sambaを再起動
```
sudo systemctl restart smbd
```

これでWindowsからラズパイのファイルが見える様になったはず。もちろん更新も可能。

#### 一応ここで、立ち上げたOSについて確認しておく

```ruby:zero2w64
 lsb_release -a
 No LSB modules are available.
 Distributor ID: Debian
 Description:    Debian GNU/Linux 11 (bullseye)
 Release:        11
 Codename:       bullseye
 ```

```ruby:zerow32
lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 11 (bullseye)
Release:        11
Codename:       bullseye
```
64bit , 32bit どちらも bullseye となっている

Bullseyeではラズパイ公式のカメラモジュールを使用する場合は、何も設定しなくても「libcamera」を使えるとの事だが、今回はサードパーティ製のRev1.3となっている OV5647センサーのものを使った。
このままでは「libcamera」が動かなかった。プログラムからも使えない可能性があるので
```
sudo nano /boot/config.txt 
```
にて末尾に
dtoverlay=ov5647
を追記して再起動する。
```
libcamera-still -o test.jpg
```
にて静止画を撮影できる様になった。

参考
Camera Module ----------> config.txtに追加する文字

V1 camera (OV5647) ------> dtoverlay=ov5647
V2 camera (IMX219)-------> dtoverlay=imx219
HQ camera (IMX477)------> dtoverlay=imx477
IMX290 and IMX327-------> dtoverlay=imx290,clock-frequency=74250000 or dtoverlay=imx290,clock-frequency=37125000 (both modules share the imx290 kernel driver; please refer to		instructions from the module vendor for the correct frequency)
IMX378---------------------> dtoverlay=imx378
OV9281---------------------> dtoverlay=ov9281


ここまで来たら、後は自分のプログラムを動かすだけ。Windows下で動いたスクリプトをラズパイ用に修正する。と言っても、LCD表示部分以外はほとんどそのまま使える。

LCD表示なしで動体検知カメラを作動させる。（プログラムの表示系はすべてコメントにしてある）

### ユーザプログラムの環境
① Windows 下で作りラズパイ用に手直しした camchkpi0.py を /home/pi/下へコピーする
せっかく samba が使えるので　エクスプローラにて Windows 下より /home/pi/下へ持ってくる

②プログラム起動
```ruby:/home/pi
python3 camchkpi0.py
```
画像変化量を示すインジケータの数値が表示され、規定変化量に達した画像が保存されていく、とりあえず２０画像保存した時点でプログラムが終了するので、カメラの前で手を振るなどして終了させる。
```ruby:camchkpi0.py （LCD表示部分は全てコメントにしてある）
#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 差分の要素数にて画像を保存するか否かを判断する
param1 = 1500   # 画像記録開始要素数-1
param2 = 40000  # 上限要素数（カメラ振れなどの全体的な画像変化の除外）

#import os
#import sys
from picamera2 import Picamera2, Preview
import cv2
import numpy as np
import time
#import spidev as SPI
#import LCD_1inch9
from PIL import Image, ImageDraw, ImageFont

# Raspberry Pi pin configuration:
#RST = 27
#DC = 25
#BL = 18     # Back Light
#bus = 0
device = 0  # camera No.

# display with hardware SPI:
#''' Warning!!!Don't  creation of multiple displayer objects!!! '''
#disp = LCD_1inch9.LCD_1inch9(spi=SPI.SpiDev(bus, device),spi_freq=10000000,rst=RST,dc=DC,bl=BL)
#disp = LCD_1inch9.LCD_1inch9()
#disp.Init() # Initialize library.
#disp.clear() # Clear display.
#disp.bl_DutyCycle(50) # Set the backlight to 100
#canvasimg = Image.new("RGB", (disp.height,disp.width ), "BLUE")
#canvasimg=canvasimg.rotate(0)
#disp.ShowImage(canvasimg)
#Font1 = ImageFont.truetype("Font00.ttf",16)

cnt = 0 # 画像保存に至った要素数のカウンタ  chk は要素数のインジケータ
n=0 # 画像保存数のカウンター

camera = Picamera2()
camera_config = camera.create_preview_configuration(main={"format": 'XRGB8888',"size": (640, 480)})
camera.configure(camera_config)
camera.start()
time.sleep(1)

#def LCD_write():
#    global img2, showimg, n, cnt, chk
#    #img2 = np.delete(img2, 3, axis=2) # alpha channel delete
#    #showimg = np.delete(showimg, 3, axis=2) # alpha channel delete
#    img2_RGB = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) # cv2 image --> pillow image
#    showimg_RGB = cv2.cvtColor(showimg, cv2.COLOR_BGR2RGB) # cv2 image --> pillow image
#    resize1 = cv2.resize(img2_RGB, (150,110))
#    resize2 = cv2.resize(showimg_RGB, (150,110))
#    resize1 = Image.fromarray(resize1) # ndarray --> image これをしないと pasteできない
#    resize2 = Image.fromarray(resize2) # ndarray --> image
#    canvasimg = Image.new("RGB", (disp.height,disp.width ), "GREEN") # 画面を横長に使うための x y 指定
#    canvasimg.paste(resize1, (5,5))     # 画面左側へ貼り付け
#    canvasimg.paste(resize2, (165,5))   # 画面右側へ貼り付け
#    draw = ImageDraw.Draw(canvasimg)
#    msg1 = "画像記録時の差分数 = " + str(cnt)
#    msg2 = "画像累積数 = " + str(n)
#    msg3 = "差分検出数 = " + str(chk)
#    draw.text((10,116), msg1, fill = "WHITE", font = Font1)
#    draw.text((10,140), msg2, fill = "WHITE", font = Font1)
#    draw.text((160,140), msg3, fill = "CYAN", font = Font1)
#    canvasimg = canvasimg.rotate(180) # 画像の向き
#    disp.ShowImage(canvasimg) # LCD出力

def imgchk(): # 画像の差分チェック
    global n, showimg, cnt, chk
    img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
    img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

    # 平均化してノイズの影響を除く（ぼかす）
    averaged_img1 = cv2.blur(img1_gray,(8,8)) # 8x8ピクセルの計64ピクセルを輝度で平均化する
    averaged_img2 = cv2.blur(img2_gray,(8,8))

    # 差分画像を計算
    diff = cv2.absdiff(averaged_img2, averaged_img1)

    # しきい値を設定して大きな違いを検出する
    thresh = 15 # カメラの解像度により調整(param3 = 15～50程度)高解像度ほど大きな値が良い？
    diff[diff < thresh] = 0
    diff[diff >= thresh] = 255

    # 変化した部分の輪郭を見つける(グレイスケールの画像が対象)
    contours, hierarchy = cv2.findContours(diff, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 変化が有った部分をBOXで囲む
    showimg = np.array(img2) # img2をコピーして表示用に使う。「showimg = img2」ではimg2にも影響が有った
    for contour in contours:
        rect = cv2.minAreaRect(contour)
        box = cv2.boxPoints(rect)
        box = np.intp(box)
        cv2.drawContours(showimg,[box],0,(0,0,255),2)

    # 変化の要素数が多ければ記録する。あまりに多ければカメラ振れと見なし除外する。
    chk = np.count_nonzero(diff > 0) # 表素数
    if chk > param1 and chk < param2:
        cnt = chk # cnt は画像保存時の要素数のLCD表示で使う
        print("******* cotours要素数 = ",cnt)
        n=n+1
        fn = 'cap' + str(n) + '.jpg'
        cv2.imwrite(fn,showimg)
    print(chk) # 検出要素数インジケータ    

# main proc #############################################
img1 = camera.capture_array() # 比較対象１をセット
img2 = camera.capture_array() # 比較対象２をセット
while True:
    imgchk() # 差分チェック
    #LCD_write() # 画面表示
    img1 = img2 # 比較対象２を比較対象１へシフト
    img2 = camera.capture_array() # 比較対象２をセット
    if n > 19: # 記録画像が20枚になったら終了する。
        break

# 終了処理
camera.close()
#disp.module_exit()
exit()

```
ラズパイZEROW Light 32bit はここまでで完成となる。LCD表示を行うと多少のもたつき感は有ると思うができない訳ではない。　手持ちのLCDが有れば試して欲しい。
## LCD表示（ラズパイZER2 W 64bit）
動体を検知し、記録できる様になったら表示系の組み込みを行う。
ここでSＤカードのバックアップ（コピー）をとっておくと、途中やらかしても OS がちゃんと動き、カメラが使える状態に戻れるので、気兼ねなく作業できる。

#### Waveshareの1.9インチLCDなので、waveshareのウェブページに従った。
ハードウェア接続
以下に従って、8PIN ケーブルにて LCD を Raspberry Pi に接続。図はラズパイ３Ｂだが
今回はラズパイZERO2 W に置き換えて見てください。
液晶----------------ラズベリーパイ
--------BCM2835---------
VCC-----3.3V--------3.3V
GND----	GND-------GND
DIN------MOSI------19
CLK------SCLK-------23
CS-------CE0---------24
DS-------25----------22
RST------27----------13
BL-------18-----------12
1.9 インチ LCD は GH1.25 8PIN インターフェイスを使用しており、Raspberry Pi に接続可。
(ピンは基盤に記された項目名に従って接続。実際の色は図と異なる。)
![pi2lcd.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810807/024246dd-f7a5-956f-a1de-2ce4efc8fc70.jpeg)

#### 使用するLCDのドライバ組み込み。
今回はpythonで書かれたドライバのみを使用。
pythonのLCDドライバ・デモのダウンロード
```
sudo apt-get install unzip -y
sudo wget https://files.waveshare.com/upload/8/8d/LCD_Module_RPI_code.zip
sudo unzip ./LCD_Module_RPI_code.zip
cd LCD_Module_RPI_code/RaspberryPi/
```
Pythonのデモを実行。
```
cd Python/examples 
sudo python3 1inch9_LCD_test.py
```
LCDにデモ画面が表示される。このデモプログラムを見てLCDへの表示方法を取得した。

専用のディレクトリを作って、必要なものを全て放り込む。
①Python/examples 下の３ファイルを /home/pi/mcam 下へコピーする
		__init__.py
		Font00.ttf
		LCD_1inch9.py
```ruby:/home/pi
	>mkdir mcam
	>cp Python/examples __init__.py Font00.ttf LCD_1inch9.py mcam/
```
Windows 下で作りラズパイ用に手直しした camchkpi.py を /home/pi/mcam 下へコピーする
せっかく samba が使えるので　エクスプローラにて Windows 下より /home/pi/mcam 下へ持ってくる。

```ruby:camchkpi.py
#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 差分の要素数にて画像を保存するか否かを判断する
param1 = 1500   # 画像記録開始要素数-1
param2 = 40000  # 上限要素数（カメラ振れなどの全体的な画像変化の除外）

#import os
#import sys
from picamera2 import Picamera2, Preview
import cv2
import numpy as np
import time
import spidev as SPI
import LCD_1inch9
from PIL import Image, ImageDraw, ImageFont

# Raspberry Pi pin configuration:
RST = 27
DC = 25
BL = 18     # Back Light
bus = 0
device = 0  # camera No.

# display with hardware SPI:
#''' Warning!!!Don't  creation of multiple displayer objects!!! '''
#disp = LCD_1inch9.LCD_1inch9(spi=SPI.SpiDev(bus, device),spi_freq=10000000,rst=RST,dc=DC,bl=BL)
disp = LCD_1inch9.LCD_1inch9()
disp.Init() # Initialize library.
disp.clear() # Clear display.
disp.bl_DutyCycle(50) # Set the backlight to 100
canvasimg = Image.new("RGB", (disp.height,disp.width ), "BLUE")
#canvasimg=canvasimg.rotate(0)
disp.ShowImage(canvasimg)
Font1 = ImageFont.truetype("Font00.ttf",16)

cnt = 0 # 画像保存に至った要素数のカウンタ  chk は要素数のインジケータ
n=0 # 画像保存数のカウンター

camera = Picamera2()
camera_config = camera.create_preview_configuration(main={"format": 'XRGB8888',"size": (640, 480)})
camera.configure(camera_config)
camera.start()
time.sleep(1)

def LCD_write():
    global img2, showimg, n, cnt, chk
    #img2 = np.delete(img2, 3, axis=2) # alpha channel delete
    #showimg = np.delete(showimg, 3, axis=2) # alpha channel delete
    img2_RGB = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) # cv2 image --> pillow image
    showimg_RGB = cv2.cvtColor(showimg, cv2.COLOR_BGR2RGB) # cv2 image --> pillow image
    resize1 = cv2.resize(img2_RGB, (150,110))
    resize2 = cv2.resize(showimg_RGB, (150,110))
    resize1 = Image.fromarray(resize1) # ndarray --> image これをしないと pasteできない
    resize2 = Image.fromarray(resize2) # ndarray --> image
    canvasimg = Image.new("RGB", (disp.height,disp.width ), "GREEN") # 画面を横長に使うための x y 指定
    canvasimg.paste(resize1, (5,5))     # 画面左側へ貼り付け
    canvasimg.paste(resize2, (165,5))   # 画面右側へ貼り付け
    draw = ImageDraw.Draw(canvasimg)
    msg1 = "画像記録時の差分数 = " + str(cnt)
    msg2 = "画像累積数 = " + str(n)
    msg3 = "差分検出数 = " + str(chk)
    draw.text((10,116), msg1, fill = "WHITE", font = Font1)
    draw.text((10,140), msg2, fill = "WHITE", font = Font1)
    draw.text((160,140), msg3, fill = "CYAN", font = Font1)
    canvasimg = canvasimg.rotate(180) # 画像の向き
    disp.ShowImage(canvasimg) # LCD出力

def imgchk(): # 画像の差分チェック
    global n, showimg, cnt, chk
    img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
    img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

    # 平均化してノイズの影響を除く（ぼかす）
    averaged_img1 = cv2.blur(img1_gray,(8,8)) # 8x8ピクセルの計64ピクセルを輝度で平均化する
    averaged_img2 = cv2.blur(img2_gray,(8,8))

    # 差分画像を計算
    diff = cv2.absdiff(averaged_img2, averaged_img1)

    # しきい値を設定して大きな違いを検出する
    thresh = 15 # カメラの解像度により調整(param3 = 15～50程度)高解像度ほど大きな値が良い？
    diff[diff < thresh] = 0
    diff[diff >= thresh] = 255

    # 変化した部分の輪郭を見つける(グレイスケールの画像が対象)
    contours, hierarchy = cv2.findContours(diff, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 変化が有った部分をBOXで囲む
    showimg = np.array(img2) # img2をコピーして表示用に。「showimg = img2」ではimg2にも影響が有った
    for contour in contours:
        rect = cv2.minAreaRect(contour)
        box = cv2.boxPoints(rect)
        box = np.intp(box)
        cv2.drawContours(showimg,[box],0,(0,0,255),2)

    # 変化の要素数が多ければ記録する。あまりに多ければカメラ振れと見なし除外する。
    chk = np.count_nonzero(diff > 0) # 表素数
    if chk > param1 and chk < param2:
        cnt = chk # cnt は画像保存時の要素数のLCD表示で使う
        print("******* cotours要素数 = ",cnt)
        n=n+1
        fn = 'cap' + str(n) + '.jpg'
        cv2.imwrite(fn,showimg)
    print(chk) # 検出要素数インジケータ    

# main proc #############################################
img1 = camera.capture_array() # 比較対象１をセット
img2 = camera.capture_array() # 比較対象２をセット
while True:
    imgchk() # 差分チェック
    LCD_write() # 画面表示
    img1 = img2 # 比較対象２を比較対象１へシフト
    img2 = camera.capture_array() # 比較対象２をセット
    if n > 19: # 記録画像が20枚になったら終了する。
        break

# 終了処理
camera.close()
disp.module_exit()
exit()

```
#### プログラムを動かす
```ruby:/home/pi
cd mcam
python3 camchkpi.py
```
画像変化量を示すインジケータの数値が表示され、規定変化量に達した画像が保存されていく、とりあえず２０画像保存した時点でプログラムが終了する。

動作確認終了後はこの値をもっと大きくし、プログラム終了後に ffmpeg にて動画へ変換すると、検知した動きを動画として確認できる。
また現在、保存している画像は、動きを検出した部分を赤枠で囲んだ画像だが、赤枠を書き込んでない画像を保存する様にしてもよい。
動画検知のレベルは parm1 と param2 の範囲で指定している。まずは parm1 だけを変更して用途に妥当な検知レベルを調整する。
## 使用ハードウエアまとめ
### RaspberryPi ZERO2 W
- TRASKIT Raspberry Pi カメラモジュール 1080P 5M OV5647センサー
- GeeekPi Raspberry Pi ZeroカメラケーブルFFCケーブルフレックスケーブル15Pin to 22Pin 30cmリボンケーブル
- waveshare 1.9inch_LCD_Module(170×320　IPS液晶)
- Geekworm Raspberry Pi Zero 2 W ヒートシンク、10mmアルミ合金 ヒートシンク 
- Gaoominy Abs保護ケース Raspberry Pi Zero/W/Wh用（ブラック）
- スガツネ工業 LAMP ミニフラットトルクヒンジHG-MF08-BL(170-023854) HGMF08BL
（カメラやLCDの角度を自由に設定できて非常に便利）
### RaspberryPi ZERO W
- TRASKIT Raspberry Pi カメラモジュール 1080P 5M OV5647センサー ケース付き
- GeeekPi Raspberry Pi ZeroカメラケーブルFFCケーブルフレックスケーブル15Pin to 22Pin 16cmリボンケーブル
- Gaoominy Abs保護ケース Raspberry Pi Zero/W/Wh用（ブラック）
- スガツネ工業 LAMP ミニフラットトルクヒンジHG-MF08-BL(170-023854) HGMF08BL
![pizeroLCD.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810807/f53460de-3621-5288-c317-decade4bdd0d.jpeg)
## おまけ
せっかくラズパイZEROとミニLCDで小さく作ったのに、このままではPCや携帯からターミナル経由でプログラムを起動しないと使えない。タクトスイッチを1個つけてスタンドアロンで使える様にする。

GPIO26とGNDの間に、タクトスイッチと1mAの定電流ダイオードを直列に接続する。普通に数百Ωの抵抗器で良いのだが、定電流ダイオードが転がっていたのでそのまま使った。こっちの方が安心感がある。

/home/pi 下に次のスクリプトを用意する。
- button.sh：タクトスイッチを受け付けるプログラムを起動するシェル
```ruby:button.sh
echo "** gpio sw ==> rancher **"
python3 button.py
```
- button.py：タクトスイッチを受け付けるプログラム
１回押されたらprog1.shを起動する
２回押されたらprog2.shを起動する
１回目が長押しされたら poweroff する
但し、２回目が押されたら長押しは無効としprog2.shを起動する
```ruby:button.py
#_*_ coding:utf-8 _*_
# for Raspberry Pi
# Startup Program Rancher
import time
import RPi.GPIO as GPIO
import subprocess

sw1 = 26 # GPIO26(Pin37)
GPIO.setmode(GPIO.BCM) # GPIO No.
GPIO.setup(sw1, GPIO.IN,pull_up_down=GPIO.PUD_UP) # pull up

def gpio_sw():
    sw = 1
    on1 = 0
    on2 = 0
    t = 0

    print("ボタン入力待ち")
    while sw == 1: # sw1 が押されるまでループして待つ
        sw = GPIO.input(sw1) # GPIO check  sw1 on ==> low
        time.sleep(0.3) # 待機中のオーバヘッド削減のため大きめの値

    while sw == 0: # sw1 が離されるまでループして待つ
        sw = GPIO.input(sw1) # GPIO check  sw1 on == > low
        on1 = on1 + 1
        time.sleep(0.1)

    while t < 10: # 約１秒間 sw1 の状態を監視する
        sw = GPIO.input(sw1) # GPIO check sw1 on ==> low
        if sw == 0:
            on2 = on2 + 1
        time.sleep(0.1)
        t = t + 1

    if on2 > 1: # １秒の間に確実に２回目が押されたか
        action = "prog2"
    else:
        action = "prog1"

    if on1 > 20 and on2 < 2: # sw1 の長押し判定(2秒以上）かつ2回目押下無し
        action = "shutdown"

    print("on1 = ", on1, "  on2 = ", on2)    

    if action == "prog1":
        subprocess.run('./prog1.sh',shell=True)
    if action == "prog2":
        subprocess.run('./prog2.sh',shell=True)
    if action == "shutdown":
        subprocess.run('sudo poweroff',shell=True)

while True:
    gpio_sw()
# shutdown までループさせるのでGPIO解放は省略
exit()

```
- prog1.sh：タクトスイッチを１回押したときに起動されるシェル
2重起動されるのを防ぎつつ、camchkpi.pyを起動する
```ruby:prog1.sh
#!/bin/bash
# camchkpi.py running check and next process select
echo "running check"
ps=`ps aux | grep camchkpi.py | grep -v grep | wc -l` # ` is [ctrl] + [@] 
if [ $ps -eq 0 ]; then
  echo " **** camchkpi ****"
  cd mcam
  python3 camchkpi.py
else
  echo "** camchkpi.py running **"
fi
```
- prog2.sh：タクトスイッチを２回押したときに起動されるシェル
静止画を1枚撮る
```ruby:prog2.sh
libcamera-still -o licamera.jpg
```
各シェルに実行属性を付与する
```
chmod +x button.sh
chmod +x prog1.sh
chmod +x prog2.sh
```
### ラズパイ起動時にユーザのサービスを起動し、button.shを動かす。

ディレクトリを用意し service ファイルを作成する
```buby:/home/pi
mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/proc1.service

[Unit]
Description=proc1 is autoradiko start  ここは説明文なので何でもよい

[Service]
ExecStart=bash /home/pi/button.sh   絶対パスで指定する

[Install]
WantedBy=default.target

```
サービスを有効化し、起動する
```ruby:/home/pi
systemctl --user enable proc1.service
systemctl --user start proc1.service
```

タクトスイッチ押下によりプログラムが起動されるようになる。次回以降もラズパイ起動後はタクトスイッチを受け付けるようになるので、一応スタンドアロンで使える。