Possessed Portrait - Updated

DIY jump scare portrait from scratch using Raspberry Pi 3 B, Python and AtmosFX Videos unliving portraits.

projectImage

Things used in this project

Hardware components

HARDWARE LIST
1 Raspberry Pi 3 Model B
1 PIR Motion Sensor (generic)
1 Used 19" LCD Monitor
Raspberry Pi Camera Module

Hand tools and fabrication machines

-Miter Box
Cheap Miter box from Walmart to cut the frame pieces.


-Brad Nailer
Used to assemble the frame pieces.


-Tape Measure


-Drill

Story

 

It's October again and I wanted to come up with a Halloween project using a Raspberry Pi. I saw a few haunted portraits scattered here and there but none of them really presented much in the way of a scare, just some simple movement.

 

I decided to expand on that idea and started looking for some good jump scare type videos I could use. This lead me to AtmosFx they have some really cool Unliving Portrait videos that are only $7.99 each. These were perfect for what I had in mind and allowed me to have more than one jump scare video that I could select manually or have it run each one at random.

 

Here is a video of the finished project.


 

I didn't want to write the PIR code from scratch so I search the web for examples of accessing the PIR with Python. I found an old article By Arc Software that demonstrated a similar project.

 

The code I am presenting is mostly from their example but I made several modifications to it to suit my needs.

 

 

 

STEP 1: Build the LCD Frame

 

After disassembling the LCD monitor and removing the LCD panel and electronics I measure the exact size of the display 17 X 11, in portrait orientation.

 

I used this online tool to figure out measuring my frame cuts to fit my LCD panel.

 

I built a wood frame using 1" x 2" wood that would have an inner dimension of 17" x 11", that would hold the LCD. I cut 4 pieces that when framed together would be the exact size of my LCD and mounted the LCD into the frame and made the LCD flush with the LCD frame. The picture frame attaches to the LCD frame and leaves all the electronics accessible from the back.

 

After staining the picture frame and letting it dry I used a brad nailer to attach the picture frame to the LCD frame.

 

Assembled LCD Frame

 

projectImage

Next I mounted the Raspberry Pi using a nice little mount from Thingiverse.com (Pi Side Mount) that I printed with my 3D printer.

projectImage

I used Mirror holders to bolt the LCD into place to keep it from shifting and keeping it flush with the front of the frame against the picture frame.

 

The final step of assembly was to drill a whole for the PIR sensor and attach it to the GPIO header of the Pi. The PIR is pretty simple, it has a hot, ground and sensor pin.

projectImage

STEP 2: Images, Videos and Code

 

I used three of the Unliving Portrait videos from AtmosFX in my project.

 

The first hurdle was to get the video to play when there was motion detected not just loop constantly on the screen. I could load the video and then pause it on the first frame and then when there is motion make it continue playing and when complete reset and start all over again.

 

It would be simpler to display a still of the first frame and then when motion is detected fire up OMXPlayer to play the appropriate video file. The advantage to this is that when OMXPlayer exited the loaded still would still be in the framebuffer and be on screen.

 

To display the initial image I used the Linux FBI (framebuffer imageviewer).

 

The player used is OMXPlayer and while it does support pausing there is no command line command that I could call in Python to pause and play without implementing something like DBuscontrol which would overly complicate the project.

 

 

 

Folder Structure:

 

The folder structure below matches the paths in the script to access the images and videos. The path can be changed as long as the paths in the scripts are updated to match.

projectImage

Images:

 

So for each video I loaded it up in VLC and did a screen cap of the first frame in the same resolution the video was in so that they would overlay perfectly on the screen with the video when it played.

 

The three videos were of a Man, Woman and Child so I took a screen cap of each and named them MaleStart.png, FemaleStart.png and ChildStart.png. I created a folder in my Halloween project called ScareMedia and uploaded the 3 stills.

 

 

 

Videos:

 

Next I named each video MaleScare.mp4, FemaleScare.mp4 and ChildScare.mp4, and uploaded them to the ScareMedia folder.

 

 

 

Code:

There are 2 scripts need to automate the videos on motion detection.

pirDetect.py

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import os
class detector(object):
def __init__(self, sensor):
self.callBacks = []
self.sensor = sensor
self.currState = False
self.prevState = False
GPIO.setmode(GPIO.BOARD)
               GPIO.setup(self.sensor, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
def read(self):
self.prevState = self.currState
self.currState = GPIO.input(self.sensor)
def printState(self):
print( "GPIO pin {0} is {1}".format(self.sensor, "HIGH" if self.currState else "LOW"))
def subscribe(self, callBack):
self.callBacks.append(callBack)
def callBack(self, state):
for fn in self.callBacks:
                       fn(state)
def start(self):
try:
self.read()
self.printState()
while True:
self.read()
if self.currState != self.prevState:
self.printState()
self.callBack(self.currState)
                                time.sleep(.1)
#Since fbi doesn't restore the console correctly when the application is exited we do a little clean up and handle the KeyboardInterrupt event.
except (KeyboardInterrupt, SystemExit):
os.system('stty sane')

#!/usr/bin/python
import subprocess as sp
import time
import os
from pirDetect import *
import sys
video = ["omxplayer", "filename", "-o", "both", "--win", "0 0 1280 720", "--aspect-mode", "fill", "--no-osd", "--orientation" ,"180","--vol", "-600"]
scareFile = "/home/pi/Projects/Halloween/ScareMedia/{0}ScareV.mp4".format(sys.argv[1])
print(scareFile)
def onMotion(currState):
if currState:
       video[1] = scareFile
       subVideo = sp.Popen(video)
while subVideo.poll() is None:
           time.sleep(.1)
def showImage():
   os.system("sudo fbi -T 1 -d /dev/fb0 -noverbose -once /home/pi/Projects/Halloween/ScareMedia/{0}Start.png".format(
       sys.argv[1]))
showImage()
objDetect = detector(7)
objDetect.subscribe(onMotion)
objDetect.start()
os.system("sudo killall -9 fbi")

Bringing It All Together:

 

The scare script can be passed a single parameter with the video sequence you want to play. Later I will automate this to play any of the three randomly.

 

There is also a modification to add a Pi camera and take a 5 second video of the person activating the motion and save it to the Pi each time motion is detected. (Not implemented yet).

 

Using SSH and logging into the Pi and execute the scare script using ./scare.py passing Male,Female,Child an argument.

 

Update: Recording victim with Pi Camera

 

I am attaching scare2.py which now has the required code to record a video of your victim using a Pi camera and then it plays it back to them so they can see their own reaction. It records a 5 second video which can be adjusted in the script.

 

I drilled a whole in the center top of the frame and mounted the Pi camera to the back using two zip tie anchors I 3D printed and a zip tie.

 

Also need to create a new folder under the Halloween folder called Recordings or modify the code to point to where you want the files to be saved. You could mount a network share and have it write them to it.

 

As luck would have it the project Video made it on the Daily Planet show on the Discovery Channel (VIDEO LINK) at the 44 minute mark.

Code

 

scareRandom.py

Python

This version of the scare script will allow you to rotate between the list of videos in a random order.
This version of the script will allow you to easily add additional videos or use different videos altogether.
Read the comment in the top of the script for details.

CODE
#!/usr/bin/python
import subprocess as sp
import time
import os
import datetime
from pirDetect import *
import sys
import random
"""
This script will play any of the videos listed in a random order.
Usage: python ./scareRandom.py [VideoName] [Minutes]

[VideoName] is any of video prefixes in the video_prefix list.
[Minutes] is the time value in minutes of how often you want to rotate to a different video.

Example usage would be : python ./scareRandom.py Male 5.

After each trigger of the on_motion event the script will check and determine if the time elapsed is greater than the 
value you provided in argument 2 and if the elapsed time is longer than your time setting it will randomly pick a new
video prefix and will recursively attempt to choose one that is NOT the current video prefix so it doesn't play the same 
video more than one time in sequence.

To add more or different videos just add to or modify the video_prefix list below.
If adding more videos or changing the defaults you will have to create a start image for each additional video.
The naming structure for the start images and videos are as follows.

[Prefix]ScareV.m4v (MaleScareV.m4v) or [Prefix]ScareV.mp4 (MaleScareV.mp4)
[Prefix]Start.png (MaleStart.png) 
"""


#  initialize variables

video_prefix = ["Male", "Female", "Child"] # This is the list of videos prefixes, you can add additional video
# prefixes here.
video = ["omxplayer", "filename", "-o", "both", "--win", "0 0 1280 720", "--aspect-mode", "fill", "--no-osd",
         "--orientation", "180", "--vol", "-600"]
record = ["raspivid", "-o", "filename", "-n", "-t", "5000", "-rot", "180"]
scare_file = ""
current_prefix = ""
new_prefix = ""
image_name = ""
start_time = time.time()


def change_video():
    global start_time
    global scare_file
    global current_prefix
    global new_prefix
    elapsed_time = time.time() - start_time
    print(str("\nTime since last rotation: {0}".format(datetime.timedelta(seconds=elapsed_time))))
    if elapsed_time > (int(sys.argv[2]) * 60):
        while new_prefix == current_prefix:  # make sure we don't choose the same video
            new_prefix = video_prefix[random.randrange(len(video_prefix))]
        current_prefix = new_prefix
        scare_file = "/home/pi/Projects/Halloween/ScareMedia/{0}ScareV.m4v".format(current_prefix)
        start_time = time.time()
        show_image(current_prefix)
        print("\nUpdating Video to: {0}\n".format(current_prefix))


def getfilename():
    return "/home/pi/Projects/Halloween/Recordings/" + datetime.datetime.now().strftime("%Y-%m-%d_%H.%M.%S.h264")


def sub_proc_wait(params):
    sub = sp.Popen(params)
    while sub.poll() is None:
        time.sleep(.1)


def on_motion(curr_state):
    if curr_state:
        auto_file_name = getfilename()  # Get a time stamped file name
        record[2] = auto_file_name
        sub_record = sp.Popen(record)  # Start recording to capture their fright
        video[1] = scare_file
        sub_proc_wait(video)  # Play the video to scare them
        video[1] = auto_file_name
        sub_proc_wait(video)  # Play back the video we just recorded
        change_video()


def show_image(_image_name):
    os.system("sudo fbi -T 1 -d /dev/fb0 -noverbose -once /home/pi/Projects/Halloween/ScareMedia/{0}Start.png".format(
        _image_name))


def start_up():
    global scare_file
    global image_name
    image_name = arg1
    scare_file = "/home/pi/Projects/Halloween/ScareMedia/{0}ScareV.m4v".format(image_name)
    show_image(image_name)
    obj_detect = detector(7)
    obj_detect.subscribe(on_motion)
    obj_detect.start()
    os.system("sudo killall -9 fbi")


if __name__ == "__main__":

    try:

        arg1 = sys.argv[1]
        if arg1 not in video_prefix:
            raise ValueError('first argument must be Male,Female or Child')
        if sys.argv[2].isdigit():
            arg2 = int(sys.argv[2])
        else:
            raise ValueError('Second argument must be a number')
    except IndexError:
        print("Usage: python ./scareRandom.py [VideoName] [Minutes]")
        sys.exit(1)
    except ValueError as x:
        print(x.message + "\nUsage: python ./scareRandom.py [VideoName] [Minutes]")
        sys.exit(1)

start_up()

pirDetect.py

Python

This handles all the motion detection and wires up the Montion event that is activated in the scare script.

CODE
#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import os

class detector(object):
        def __init__(self, sensor):
                self.callBacks = []
                self.sensor = sensor
                self.currState = False
                self.prevState = False

                GPIO.setmode(GPIO.BOARD)
                GPIO.setup(self.sensor, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

        def read(self):
                self.prevState = self.currState
                self.currState = GPIO.input(self.sensor)

        def printState(self):
                print( "GPIO pin {0} is {1}".format(self.sensor, "HIGH" if self.currState else "LOW"))

        def subscribe(self, callBack):
                self.callBacks.append(callBack)

        def callBack(self, state):
                for fn in self.callBacks:
                        fn(state)

        def start(self):
                try:
                        self.read()
                        self.printState()
                        while True:
                                 self.read()
                                 if self.currState != self.prevState:
                                         self.printState()
                                         self.callBack(self.currState)
                                 time.sleep(.1)

                except (KeyboardInterrupt, SystemExit):
	#Since fbi doesn't restore the console correctly when the application is exited we do a little clean up.
						os.system('stty sane')

scare.py

Python

This is the script that gets run to initiate the video when motion is detected.

CODE
#!/usr/bin/python
import subprocess as sp
import time
import os
from pirDetect import *
import sys

video = ["omxplayer", "filename", "-o", "both", "--win", "0 0 1280 720", "--aspect-mode", "fill", "--no-osd", "--orientation" ,"180","--vol", "-600"]
scareFile = "/home/pi/Projects/Halloween/ScareMedia/{0}ScareV.mp4".format(sys.argv[1])
print(scareFile)

def onMotion(currState):
    if currState:
        video[1] = scareFile
        subVideo = sp.Popen(video)
        while subVideo.poll() is None:
            time.sleep(.1)


def showImage():
    os.system("sudo fbi -T 1 -d /dev/fb0 -noverbose -once /home/pi/Projects/Halloween/ScareMedia/{0}Start.png".format(
        sys.argv[1]))


showImage()
objDetect = detector(7)
objDetect.subscribe(onMotion)
objDetect.start()
os.system("sudo killall -9 fbi")

scare2.py

Python

This is the updated version of the scare.py script that includes recording a video of your victim using a Pi camera.

CODE
#!/usr/bin/python
import subprocess as sp
import time
import os
import datetime
from pirDetect import *
import sys

video = ["omxplayer", "filename", "-o", "both", "--win", "0 0 1280 720", "--aspect-mode", "fill", "--no-osd", "--orientation" ,"180","--vol", "-600"]
record = ["raspivid", "-o", "filename", "-n", "-t", "5000", "-rot","180"]
scareFile = "/home/pi/Projects/Halloween/ScareMedia/{0}ScareV.mp4".format(sys.argv[1])

def getFileName():
    return "/home/pi/Projects/Halloween/Recordings/" + datetime.datetime.now().strftime("%Y-%m-%d_%H.%M.%S.h264")

def subProcWait(params):
        sub = sp.Popen(params)
        while sub.poll() is None:
            time.sleep(.1)

def onMotion(currState):
    if currState:
        autoFileName = getFileName()  # Get a time stamped file name
        record[2] = autoFileName
        subRecord = sp.Popen(record)  # Start recording to capture their fright
        video[1] = scareFile
        subProcWait(video)  # Play the video to scare them
        video[1] = autoFileName
        subProcWait(video)  # Play back the video we just recorded

def showImage():
    os.system("sudo fbi -T 1 -d /dev/fb0 -noverbose -once /home/pi/Projects/Halloween/ScareMedia/{0}Start.png".format(
        sys.argv[1]))


showImage()
objDetect = detector(7)
objDetect.subscribe(onMotion)
objDetect.start()
os.system("sudo killall -9 fbi")

The article was first published in hackster, October 8, 2017

cr: https://www.hackster.io/dominick-marino/possessed-portrait-updated-32a7a6#toc-images--3

author: Dominick Marino

License
All Rights
Reserved
licensBg
0