猫咪走开:计算机视觉喷水驱猫装备

防火防盗防猫咪

Cherry

喜欢

4036
浏览
1
喜欢

> 更多图片

项目状态:已完成
开放度:公开
所属分类:电子
发布时间:2019-06-13
最近更新:2019-06-19

详细说明


不得不承认,Maker 撩猫那可是一把好手。咱们的撩猫系列教程,前有《LaserKitty:激光逗猫神器》,后有《Petoi Nybble:树莓派猫奴玩家的福音》。
下面要介绍的这个用来为你看家护院,驱赶外来的随地大小便的入侵猫!

虽然本教程没有对计算机视觉的工作原理作特别详细的介绍,但会将项目构建的过程和代码分享给大家。
首先,这个喷水驱猫装备是一个低压的喷头,而且转动的速度比猫的反应要快。但请放心,它不会把猫咪喷成落汤猫,只是产生威慑效果,打消它们在后院大小便的念头。

来看一段简单视频:
https://v.qq.com/x/page/e0876huqkum.html

链接表


教程

组件清单
  • 树莓派 Zero(Raspberry Pi Zero) × 1
  • SD卡 × 1
  • 树莓派摄像头 × 1
  • 继电器 × 1
  • 555定时器 × 1(可以用 Arduino 和一个继电器代替)
  • 电磁阀 × 1
  • 喷水头 × 1
  • 电子产品的外壳 × 1
  • 锤子 × 1
  • 低分辨率的摄像头 × 1

材料清单


树莓派 Zero(Raspberry Pi Zero)×1
SD卡×1
树莓派摄像头×1
继电器×1
555定时器×1(可以用 Arduino 和一个继电器代替)
电磁阀×1
喷水头×1
电子产品的外壳×1
锤子×1
低分辨率的摄像头×1

系统介绍

1、树莓派摄像头检测到类似一只猫大小的物体的移动了数帧(下一步会说明)。
2、树莓派引爆喷水头。
3、猫落荒而逃。
4、视频自动上传到 Youtube 供大家伙儿围观。

编程


使用 openCV 的帧减法,你可以找到随时间变化的帧的区域,使用一些实用的函数,确定物体的大小以及其变化,最重要的是确实物体是否是猫。
你也可以自己在网上搜索有关帧减法的详细教程。

代码工作原理
1、摄像头要保持拍摄并将画面进行比较。检测到猫大小的物体将引起注意。
2、猫大小的物体变化持续超过 4 帧,树莓派将用 GPIO 为继电器供电并启动 Arduino。
3、Arduino 发送信号给第二个继电器供电五秒钟并激活电磁阀。电磁阀在通电时启动喷水头。
4、当喷水头启动时,摄像头会停止检测并记录视频。
5、将视频上传到你的 Youtube,同时上传到 Dropbox 进行微调系统。

使用两个继电器和一个 Arduino 来打开电磁阀的原因如下
1、录制视频时树莓派不能开关电磁阀,python 脚本直到视频结束后才能停止。因此当视频还在录制时,需要 Arduino 或555定时器作为独立的脚本来关闭电磁阀。
2、第一个继电器和 Arduino 可以用 555 定时器替换,它可以节省大量的时间和金钱。
3、树莓派不能直接触发电磁阀,因为树莓派的 GPIO 工作在 3.3v 和 51mA (最大值)上,而电磁阀需要 5V 并且大于 51mA 才触发。
4、可以裁剪每个画面的大小,除去不需要检测的区域,例如隔壁家的花园。
5、可能会错过一些画面,浪费时间来设置它。
代码如下

import cv2
import numpy as np
import argparse #cat
import time
import RPi.GPIO as GPIO
import os
import dropbox
from picamera.array import PiRGBArray
from picamera import PiCamera

#------------------------------------------------Upload to youtube---------------------------------------
def HDtoYoutube():
    ctime = time.strftime("_%H-%M-%S")
    cdate = time.strftime("_%d-%m-%Y")
    vidname = ctime + cdate
    #Trigger relay
    GPIO.output(11,True)
    time.sleep(.5)
    GPIO.output(11,False)
    print("Taking Video")
    try:
        #Take Video
        os.system('raspivid -w 1640 -h 922 -o vid{0}.h264 -t 15000'.format(vidname))
        #Upload to youtube
        print("Uploading to YouTube")
        os.system('sudo youtube-upload --title="Cat Got Wet {0}" --client-secrets=client_secret.json vid{0}.h264'.format(vidname))
        #Remove video file when done
        os.remove('vid{0}.h264'.format(vidname))
        print("Video uploaded and removed from Pi")
    except:
        pass

#------------------------------------------------Stills to dropbox---------------------------------------
def StillsToDropbox():
    print("Uploading Still To Dropbox Function")
    access_token = 'Ah ah ah, you didn't say the magic word...Ah ah ah, you didn't say the magic word'
    ctime = time.strftime("%H:%M:%S")
    cdate = time.strftime("%d-%m-%Y")
    try:
        filename = "/Motion/{0}/DetectedAt_{1}.jpg".format(cdate, ctime)
        print(filename)
        client = dropbox.client.DropboxClient(access_token)
        image = open("ToDropbox.jpg", 'rb')
        client.put_file(filename, image)
        image.close()
        os.remove("ToDropbox.jpg")
    except:
        pass

#------------------------------------------------Detect motion-----------------------------------------
def DetectMotion():
    #Define vars
    min_area = 400
    tolarance = 25 #change in pixel
    bluramount = 21
    timetoforget = 0.5
    kernel = np.ones((5,5),np.uint8) #used for dialate
    MotionCounter = 0
    MinTargetArea = 600 #smallest size to detect
    MaxTargetArea = 5000 #Largest size to detect
    now = time.time()
    then = time.time()

    #initialise camera
    camera = PiCamera()
    camera.resolution = (640,480)
    camera.framerate = 10
    rawCapture = PiRGBArray(camera, size=(640,480))

   #warmup camera
    time.sleep(1)

   #Grab first frame & prep it to go into cv2.acumulate weight
    camera.capture(rawCapture, format="bgr")
    avg = rawCapture.array

   #Crop out unwanted region
    PolyCrop = np.array( [[[362,480],[613,365],[628,161],[498,0],[640,0],[640,480]]], dtype=np.int32 )
    cv2.fillPoly(avg, PolyCrop, 0,0,0)
    #Process image
    avg = cv2.cvtColor(avg, cv2.COLOR_BGR2GRAY)
    avg = cv2.GaussianBlur(avg, (bluramount, bluramount), 0)
    avg = avg.copy().astype("float")
    rawCapture.truncate(0)
    print("Ready to detect")

    #capture frames
    for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):
        #Pause Switch
        loopgo = GPIO.input(PauseNow)
        #print(loopgo)
        while loopgo == 0:
            #print(loopgo)
            loopgo = GPIO.input(PauseNow)
            time.sleep(1)

       #grabs raw numpy array
        currentframe = frame.array
        key = cv2.waitKey(1) & 0xFF
        #Crop out unwanted region
        cv2.fillPoly(currentframe, PolyCrop, 0,0,0)
        rawCapture.truncate(0) #Clear frame buffer for next loop
        currentgray = cv2.cvtColor(currentframe, cv2.COLOR_BGR2GRAY)
        currentgray = cv2.GaussianBlur(currentgray, (bluramount, bluramount), 0)
        #make time average frame
        cv2.accumulateWeighted(currentgray, avg, timetoforget)
        #get difference in frame
        frameDelta = cv2.absdiff(currentgray, cv2.convertScaleAbs(avg))
        thresh = cv2.threshold(frameDelta, tolarance, 255, cv2.THRESH_BINARY)[1]
        #Turn to blob
        thresh = cv2.dilate(thresh, kernel, iterations = 10) #dilate
        thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel) #close holes
        thresh = cv2.erode(thresh, kernel, iterations = 5) #erode

      #contours
        _, cnts, _= cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        # loop over the contours
        for c in cnts:
            # if the contour is too small, ignore it
            if cv2.contourArea(c) < min_area:
                continue
            # compute the bounding box for the contour, draw it on the frame,
            # and update the textq
            (x, y, w, h) = cv2.boundingRect(c)

          #Too small : Red Box
            if cv2.contourArea(c) < MinTargetArea:
                cv2.rectangle(currentframe, (x, y), (x + w, y + h), (0, 0, 255), 2)
                #MotionCounter = MotionCounter + 1 #Debug take all the pictures
                print("MotionDetected")
            #Just right : Green Box
            if cv2.contourArea(c) >= MinTargetArea and cv2.contourArea(c) <= MaxTargetArea:
                cv2.rectangle(currentframe, (x, y), (x + w, y + h), (0, 255, 0), 2)
                MotionCounter = MotionCounter + 1 #Debug take all the pictures
                print("MotionDetected")
            #Too big : Blue Box
            if cv2.contourArea(c) > MaxTargetArea:
                cv2.rectangle(currentframe, (x, y), (x + w, y + h), (255, 0, 0), 2)
                #MotionCounter = MotionCounter + 1 #Debug take all the pictures
                print("MotionDetected")

        #Keep now up to date
        now = time.time()
        #MotionCounterTimer
        if (MotionCounter > 0):
            if (now - then > 10):
                MotionCounter = 0
                then = time.time()
        #Break loop on pressing Q
        if key == ord("q"):
            break

        #If motion persists save current frame and activate countermeasures
        if MotionCounter >= 4:
            MotionCounter = 0
            cv2.imwrite('ToDropbox.jpg', currentframe)
            camera.close()
            return True

#------------------------------------------------Main---------------------------------------
try:
    #Set Pins
    GPIO.setmode(GPIO.BOARD)
    PauseNow=12
    GPIO.setup(11,GPIO.OUT)
    GPIO.setup(PauseNow,GPIO.IN,pull_up_down=GPIO.PUD_UP)

    while True:
        MotionDetected = False
        MotionDetected = DetectMotion()
        if MotionDetected == True:
            HDtoYoutube()
            StillsToDropbox()

except KeyboardInterrupt:
    print("Keyboard Interupt")    
except:
    print("Other Error")    
finally:
    GPIO.cleanup()

组装





1、将所有电器安装防水外壳,并使用胶带和热熔胶固定到位。


2、开始投入运转。

误报的情况


设备也会存在误报的情况,也许它也会喷到你的家人或朋友。因此提供一个专业的意见——在门上安装一个关闭设备的开关。

教程中如有误的地方请多多包涵与指教。希望你喜欢这个项目。