UnitVで雨滴を検知してワイパーをコントロール



## はじめに
近年高級車には、レインセンサーを使用したオートワイパーが装備されているようです。
レインセンサーは、雨滴を赤外線の屈折により検知しているようですが、今回別のアプローチを用いて作成しましたので紹介いたします。
## 原理
車のフロントガラスに、UnitVを設置し撮影した画像から、MicroPythone のimg.find_circles()を用いて雨滴の数や雨滴の大きさを、検知することでワイパーを動作させます。
![検出画面.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633706/71bf14d8-56c4-7422-02d2-ea8e0bab7af6.gif)

MaixPy IDEの記録を使用した動画です。
赤い円は、フロントガラスの雨滴を検出している事を表しています。
|     　上部の白枠     |　     　　意味|
|:----------------:|:------------------:
|NUM:             |     円の検出数
|SUMR:            |     円の大きさの合計       
|OFF: ON:         |     　リレーの動作
|LEVEL            |  感度（０−２）

## 回路図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633706/669ad9c9-8862-e714-7997-ec09fa99a056.png)
・シミュレーション
![シミュレーション.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633706/ddb66205-84e2-babe-1945-2a96f1e48e60.gif)

# ソースリスト

```Python
# Automatic wipers - By: kenji.yamasaki - 土 6月 24 2023　Updete
# ov7740

import sensor
import image
import lcd
import time
import utime
import math

from fpioa_manager import fm
from board import board_info
from Maix import GPIO
from modules import ws2812

lcd.init()
#lcd.rotation(1)

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.run(1)

#### Up Low revers
#sensor.set_vflip()

#### LED
fm.register(8)
class_ws2812 = ws2812(8,100)
b = class_ws2812.set_led(0,(0,0,0))

#### リレーセット(pin 35)
fm.register(35, fm.fpioa.GPIOHS0, force=True)
pin35 = GPIO(GPIO.GPIOHS0, GPIO.OUT)

#### Pin34
fm.register(34, fm.fpioa.GPIOHS1, force=True)
pin34 = GPIO(GPIO.GPIOHS1, GPIO.IN)

# Function PushTime
class Sw_sts:
    def __init__(self):
        self.min = 5
        self.max = 600
    def read(self):
        if pin34.value() == 0:
            CNT = time.ticks_ms()
            while(True):
                if pin34.value() == 1:
                    CNT1 = time.ticks_ms()
                    count = CNT1 - CNT
                    if (count > self.min) and (count < self.max):
                        return "S"
                    if count >= (self.max + 1):
                        return "L"

#### File
filename = "/sd/config.txt"
#
#filename = "/flash/config.txt"
class File_acc:
    def __init__(self):
        try:
            conf = open(filename, "r") # file ある場合
        except:
            conf = open(filename, "w") # file ない場合
            set_rec = {"ON_OFF": 0, "LEVEL": 3} # ON 0,OFF 1 LEVEL 1-5 Default->3
            conf.write(str(set_rec))
            conf.close()
    # # ON-0,OFF-1
    def read(self):
        conf = open(filename, "r")
        set_rec = eval(conf.read()) #str -> dic 変換
        on_off = set_rec["ON_OFF"]
        level = set_rec["LEVEL"]
        conf.close()
        return (on_off,level)
    def write(self,on_off,level):
        set_rec = {"ON_OFF": on_off, "LEVEL": level}
        conf = open(filename, "w")
        conf.write(str(set_rec))
        conf.close()


class Led_sts:
    def __init__(self):
        # (32.16,8) # 白　Level 1/32
        # (32,0,0) # 赤　Level 1/32      #赤の最大輝度(255,0,0)
        # (128,64,0) # 黄 Level 1/32
        # (0,64,0) # 緑 Level 1/32       #緑の最大輝度(0,128,0)
        # (128,0,32) # 紫 Level 1/32
        # (0,0,32) # 青 Level 1/32       #青の最大輝度(0,0,64)
        # LEVEL 0 ＝ 緑 LEVEL 1 = 紫　LEVEL 2 = 青
        self.typ = {0:(32,16,8),1:(0,64,0),2:(128,0,32),3:(0,0,32)}

    def on(self,onoff,level):
        if onoff == 1:
            a = class_ws2812.set_led(0,self.typ[0])
        else:
            if level == 0:
                a = class_ws2812.set_led(0,self.typ[1])
            elif level == 1:
                a = class_ws2812.set_led(0,self.typ[2])
            elif level == 2:
                a = class_ws2812.set_led(0,self.typ[3])
        a=class_ws2812.display()
    def off(self):
        a = class_ws2812.set_led(0,(0,0,0))
        a=class_ws2812.display()

class Relay():
    def on(self):
        pin35.value(1)
    def off(self):
        pin35.value(0)

# 動作値定義
num =  (  5,  8,  10)
sumr = (   10,  25,  50)

sw = Sw_sts()
ledsts = Led_sts()
fileacc = File_acc()
relay = Relay()
cnt = 1
onoff = 0
level = 0

# curTim 現在時間(sec)
curTime = 0
# svTim 保存時間(sec) リレーon時の時間と現在時間を比較する。
svTime = 0

while(True):

    # file_sts[0] on_off , file_sts[1] level
    file_sts = fileacc.read()
    onoff = file_sts[0]
    level = file_sts[1]
    ledsts.on(onoff,level)
    sw_time = sw.read()
    if file_sts[0] == 1 and sw_time == "S":
        onoff = 0
        fileacc.write(onoff,level)
        ledsts.on(onoff,level)
    if file_sts[0] == 0 and sw_time == "S":
        level = level + 1
        if level == 3:
            level = 0
        fileacc.write(onoff,level)
        ledsts.on(onoff,level)
        set_rec["LEVEL"] = level

    if sw_time == "L":
        onoff = 1
        fileacc.write(onoff,level)
        ledsts.on(onoff,level)

    img = sensor.snapshot()

    # ヒストグラム平滑化
    img.histeq()

    # サークル検出レベル（画像の中から円を探す）
    obj = img.find_circles(threshold=2100,r_max = 10)

    i = 0
    sum_r = 0
    # 検出された円の表示と半径の合計
    while i < len(obj):
        t = obj[i]
        x = t[0]
        y = t[1]
        r = t[2]
        color = (255,0,0)
        size = 2
        img.draw_circle(x,y,r,color,size)
        sum_r = sum_r + r
        i = i + 1

    # 動作値読み込み
    conf = open(filename, "r")
    set_rec = eval(conf.read()) #str -> dic 変換
    conf.close()
    l = set_rec["LEVEL"]

    # バックホワイト
    img.draw_rectangle(20,0,60,10,color=(255,255,255),fill=True )
    img.draw_rectangle(90,0,70,10,color=(255,255,255),fill=True )
    img.draw_rectangle(180,0,30,10,color=(255,255,255),fill=True )
    img.draw_rectangle(240,0,70,10,color=(255,255,255),fill=True )

    # ステイタス教示
    img.draw_string(20, 0, "NUM: " + str(len(obj)), color=(255,0,0), scale=1)
    img.draw_string(90, 0, "SUMR: " + str(sum_r), color=(255,0,0), scale=1)
    img.draw_string(240, 0, "LEBEL: " + str(l), color=(255,0,0), scale=1)
    lcd.display(img)

    # 円の数 = len(obj) , 円の大きさ合計 = sum_r
    if (len(obj) > num[l]) and (sum_r > sumr[l]) and (onoff == 0):
        curTime = math.floor(utime.ticks_ms() / 100)

        if ((curTime - svTime) > 15 ):
            print("subTime",(curTime - svTime))
            relay.on()
            ledsts.off()
            img.draw_string(180, 0, "ON", color=(255,0,0), scale=1)
            svTime =  math.floor(utime.ticks_ms() / 100)
            time.sleep(0.3)
            relay.off()
            ledsts.off()
        else:
            img.draw_string(180, 0, "OFF", color=(255,0,0), scale=1)
    else:
        img.draw_string(180, 0, "OFF", color=(255,0,0), scale=1)
```
# 動作概要
a) プッシュボタンの機能
　・ショートプッシュによる、感度の調整
　・UnitVのLEDに設置感度を表示(0:　緑　１：紫 2:青)
　・ロングプッシュによる、動作の停止(白)
b) SD Cardへの保存（通電時、記録内容を読み出します。)
　・感度の記録
　・動作停止の記録
　・SD Cardにconfig.txtが無い場合、初期値でFILEを作成
c) サークル検出レベル
　・img.find_circlesで、しきい値と最大サイズ(r_max)を定義（試行錯誤で調整）
# 試験調整
a)フロントガラスとの焦点距離調整
　・フロントガラスの雨滴に焦点を合わせる為、分解しレンズをフリーにします。
　　（UnitVは、レンズが接着剤で固定されているものがあるようです。）
![レンズ調整.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633706/ddac9459-9514-3f39-a69c-2117c61cdc90.gif)
b）設置と調整
　・UnitVは、L型のアルミ板に両面テープで取り付け、フロントガラスに取り付けます。
　・UnitVの角度は、下向きに取り付けます。
　　(真正面や上向きに取り付けと、木漏れ日等でご動作します。)
　・USB-TypeCでPCに接続し、MaixPyで雨滴をモニターし焦点距離や感度レベル調整を行います。
　　(霧吹きで、水を吹きかけソースリストの動作定義(num,sumr)の値をLEBEL別に調整します。)
　・ソースリスト編集後、ToolのSave open script to board(boot.py)でSD Cardに書き込みます。
![試験.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633706/17dd128f-79ef-3810-56d9-6924823f8f0b.gif)

# 動作動画
雨滴の検出レベルに達した場合、UnitVのLEDを消しリレーに動作信号を渡します。
![実写.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633706/a497cf70-b844-4c2b-7d8b-8358aecac7c3.gif)

# M5StickVについて
今回、UnitV(OV2640,OV7720 どちらも可能)を使用しましたが、M5StickVでも動作します。
M5StickVは、Groveケーブルでの電源供給は出来ないようで、USB-TypeCを使用する必要があるようです。
また、UnitVのUSB-TypeCから電源供給した場合、Groveケーブルに接続したリレーは動作できませんでした。
![M5stickV.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633706/fdf9d072-eee6-810c-8a05-f555fe8d2bc7.gif)
# さいごに
雨滴をカメラで検出するため、フロントガラスは撥水処理をすることで感度が上がります。
なお、撥水処理しておくこと60Km以上の速度で雨滴が流れ検知をしないため、ワイパーは動作しません。
(そのほうが、視認性が確保され都合が良いです。）
調べた限り、特許権や著作権の侵害に抵触しないようですが、ご利用は自己責任でお願いします。

# 参考文献
https://qiita.com/Lathe/items/0f0c9b75928d52a7d502

https://wiki.sipeed.com/soft/maixpy/en/api_reference/machine/index.html