Introduction: Cat-a-way - Computer Vision Cat Sprinkler

Problem - Cats using your garden as a toilet

Solution - Spend a too much time over engineering a cat sprinkler with auto youtube upload feature

This is not a step by step, but an overview of construction and some code

#BeforeYouCallPETA - The cats are fine, its a low pressure sprinkler just like rain, that they can outrun before it even pivots around to them. The project is not to soak a cat, but to shew them away before they litter my garden with the cat version of Barkers Eggs.

Step 1: Main Items Required

Raspberry Pi zero & SD card

Raspberry Pi camera


555 timer.... (or an arduino and another relay if your 555 timers don't arrive)



Some kind of housing for the electronics

A willingness to hit a metaphorical nail with a 6 tonne sledge hammer

A camera with so little resolution you can barely see the water, but can still see the cats running for cover

Step 2: The System

1, Pi camera detects a cat sized object moving for a few camera frames (exaplined in next step)

2, Pi sets off sprinkler

3, Cat runs for cover

4, Video automatically uploaded to youtube for viewing pleasure

Step 3: Coding Time

Using openCV using frame subtraction you can find areas of the frame that change over time, using some nifty functions you can figure out how big these changes are and if they persist over time, and most importantly find out if they are cat sized.

There are quite a few tutorials on frame subtraction that go into great detail if you do a quick google search.

Overview of how the code works

1, The camera keeps taking frames and comparing them to the last

2, If a cat sized shape is detected it is noted

3, If the cat sized change persists over around 4 frames the pi uses its GPIO to power relay to start arduino

4, The arduino sends a signal to power the second relay for 5 seconds which activates the solenoid

5, The solenoid when powered allows water to the sprinkler

6, While sprinkler is active camera stops detecting and records video

7, Video is uploaded to Youtube

8, Stills uploaded to dropbox for fine tuning system

Note - Why i ended up using 2 relays and an arduino to turn on a solenoid for 5 seconds.....

1,The pi cannot start and stop the solenoid while recording the video as the python scrips pauses until the video is finished, hence the need for the arduino (or 555 timer) to allow the solenoid to open and close independant to the script while the video is still recording.

2,The first relay and arduino can be replaced with a 555 timer but that didn't come in the post in time for this project, 555 would save a lot of time money and steps.

3,The pi cannot trigger the solenoid directly as the Pi GPIO works on 3.3v and 51mA max, and the solenoid wants 5V and much more than 51mA to trigger.

4,Each frame can be cropped to remove motion detection in unwanted areas, such as a neigbors garden. Failure to do this will result in said neighbour giving confused looks into your garden because the sprinkler goes off every time he wants to go into his shed.

5,I probably missed something obvious and wasted my time setting it up like this.

Code below

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


Step 4: Putting It Together

Cram the electrics into a waterproof housing, screw things into walls and use lots of duct tape and hot glue

Step 5: Results

When it works it works

Step 6: False Positives

When it doesn't it sprays cat shadows, your wife and your daughter.

Pro-tip - Put a switch by the door that pauses the motion detection program..... then forget to use it and get soaked when putting the bins out.

Hope I made any experts in programming, electronics and DIY cringe with all the mistakes I probably made, and especially hope you enjoyed all my spelling mistakes.