Raspberry Pi NoIR Camera – Reusable System

Spread the love

I decided I need to get back on track this year, I’ve missed several weekends. The easiest place to start catching up on content would be a fun project I love working on.

That brings us back today to the Pi NoIR camera project and builds. There are a few major idea I had at play.

First: WiFi Hotspot

My Raspberry Pi – Model B is officially retired, but it didn’t retire in my view, I can still have tons of fun with it. I figured it would make sense to set up a WiFi hotspot with a USB attachment, then I can control the Pi camera while on the move.

I looked around at what I could buy for a small cost, then was handed a Belkin SURF N300 XR we had lying around. I plugged it into my Pi, put it on, then went through the RaspAP steps to install. You could perhaps sort out what I found a slight problem, I had Apache installed already for the other steps. Hence, I cant access the controls for changing the hotspot name, or password, for myself.

Web interface

Take note, I used the /var/www/html/ directory, I can see index with the login, though we make a /var/www/html/camera/ directory below.

For safety, make sure you’ve changed your password for the “pi” user when you do this yourself. It’s always best to put safety first. I also enabled SSH access so I could manage things remotely.

Second: Remove Automation

The idea I got I felt we needed to adjust the python scripts. It had a simple purpose:

  • Have a way to see the camera’s view before I start the motion detection
  • Allow photography when there were already photos there
  • Make it easy to just plug in the USB I always use and copy everything off

You’ll take note, we create a single file on the desktop for step 1:

#!/bin/bash
sudo rm /var/www/html/camera/cam.jpg
sudo raspistill -w 480 -h 270 -vf -hf -o /var/www/html/camera/cam.jpg

You can note, I broke the naming conventions. I shouldn’t call it get.py, but I allow it to execute.

Our web server will refresh the image seen in 10 seconds

You’ll note in the Third below we have a delay on the php to show the image, it’s just given for the time it needs to take a new photo.

We then add start.py, same as above:

#!/bin/bash
sudo python /home/pi/Desktop/_start.py

Which calls the motion detection in _start.py:

#!/usr/bin/python
import os
import io
import subprocess
import time
from datetime import datetime
from PIL import Image
import shutil
import RPi.GPIO as GPIO

# GPIO Setup
GPIO.setwarnings(False)

GPIO.setmode(GPIO.BCM)
GPIO.setup(23, GPIO.OUT) # green
GPIO.output(23, True)
# this is needed to reset awb every boot
os.system('sudo vcdbg set awb_mode 0')

def findBrightness():
    print('finding brightness')
    best = 0
    pre = 0
    for (q) in range(8):
        t = q * 10 + 10
        print(t)
        briSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise'" % (t)
        print(briSettings)
        command = "raspistill %s -w 64 -h 48 -t 200 -e bmp -n -o -" % (briSettings)
        imageData = io.BytesIO()
        imageData.write(subprocess.check_output(command, shell=True))
        imageData.seek(0)
        im = Image.open(imageData)
        buffer = im.load()
        startAt = buffer[0,0]
        numChanged = 0
        for (r) in range(63): # 0 to 62 => 1 to 63
            for (s) in range(47): # 0 to 46 => 1 to 47
                if (startAt <> buffer[r,s]):
                    numChanged = numChanged + 1
        if (numChanged > pre):
            best = t
            pre = numChanged
            print(" - changed to %s with %s" % (best, pre))
        else:
            print(numChanged)
        imageData.close()
    return best


# Motion detection settings:
# Threshold          - how much a pixel has to change by to be marked as "changed"
# Sensitivity        - how many changed pixels before capturing an image, needs to be higher if noisy view
# ForceCapture       - whether to force an image to be captured every forceCaptureTime seconds, values True or False
# filepath           - location of folder to save photos
# filenamePrefix     - string that prefixes the file name for easier identification of files.
# diskSpaceToReserve - Delete oldest images to avoid filling disk. How much byte to keep free on disk.
# cameraSettings     - "" = no extra settings; "-hf" = Set horizontal flip of image; "-vf" = Set vertical flip; "-hf -vf" = both horizontal and vertical flip
threshold = 15
sensitivity = 15
forceCapture = True
forceCaptureTime = 60 * 15 # Once every 15min
filepath = "/home/pi/Desktop/_photos"
filenamePrefix = "img"
diskSpaceToReserve = 40 * 1024 * 1024 # Keep 40 mb free on disk
# -- added by edg3: works out best brightness for most differences
print('finding brightness...')
bestbrightness = findBrightness()
print("best brightness: %s" % (bestbrightness))
cameraSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise' " % (bestbrightness)

# settings of the photos to save
saveWidth   = 3280
saveHeight  = 2464
saveQuality = 100 # Set jpeg quality (0 to 100)

# Test-Image settings
testWidth = 100
testHeight = 75

# this is the default setting, if the whole image should be scanned for changed pixel
testAreaCount = 1
testBorders = [ [[1,testWidth],[1,testHeight]] ]  # [ [[start pixel on left side,end pixel on right side],[start pixel on top side,stop pixel on bottom side]] ]
# testBorders are NOT zero-based, the first pixel is 1 and the last pixel is testWith or testHeight

print('Preparing')

# in debug mode, a file debug.bmp is written to disk with marked changed pixel an with marked border of scan-area
# debug mode should only be turned on while testing the parameters above
debugMode = False # False or True

# Capture a small test image (for motion detection)
def captureTestImage(settings, width, height):
    command = "raspistill %s -w %s -h %s -t 200 -e bmp -n -o -" % (settings, width, height)
    #imageData = StringIO.StringIO()
    imageData = io.BytesIO()
    imageData.write(subprocess.check_output(command, shell=True))
    imageData.seek(0)
    im = Image.open(imageData)
    buffer = im.load()
    imageData.close()
    return im, buffer

# Keep free space above given level
def keepDiskSpaceFree(bytesToReserve):
    if (getFreeSpace() < bytesToReserve):
        sys.exit()

# Get available disk space
def getFreeSpace():
    st = os.statvfs(filepath + "/")
    du = st.f_bavail * st.f_frsize
    return du

# Save a full size image to disk
def saveImage(settings, width, height, quality, diskSpaceToReserve, count):
    if (getFreeSpace() > diskSpaceToReserve):
        time = datetime.now()
        filename = filepath + "/" + filenamePrefix + "-%06d.jpg" % count
        subprocess.call("raspistill %s -w %s -h %s -t 200 -e jpg -q %s -n -o %s" % (settings, width, height, quality, filename), shell=True)
        print("Captured %s" % filename)

def motion():
    todaycount = 0
    path = '/home/pi/Desktop/_photos/'
    count = len([f for f in os.listdir(path)if os.path.isfile(os.path.join(path, f))])
    count = count + 1
    cameraSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise'" % bestbrightness

    # Get first image
    print(cameraSettings)
    image1, buffer1 = captureTestImage(cameraSettings, testWidth, testHeight)

    # Reset last capture time
    lastCapture = time.time()

    while (True):
        # Get comparison image
        image2, buffer2 = captureTestImage(cameraSettings, testWidth, testHeight)

        # Count changed pixels
        changedPixels = 0
        takePicture = False

        if (debugMode): # in debug mode, save a bitmap-file with marked changed pixels and with visible testarea-borders
            debugimage = Image.new("RGB",(testWidth, testHeight))
            debugim = debugimage.load()

        for z in range(0, testAreaCount): # = xrange(0,1) with default-values = z will only have the value of 0 = only one scan-area = whole picture
            for x in range(testBorders[z][0][0]-1, testBorders[z][0][1]): # = xrange(0,100) with default-values
                for y in range(testBorders[z][1][0]-1, testBorders[z][1][1]):   # = xrange(0,75) with default-values; testBorders are NOT zero-based, buffer1[x,y] are zero-based (0,0 is top left of image, testWidth-1,testHeight-1 is botton right)
                    if (debugMode):
                        debugim[x,y] = buffer2[x,y]
                        if ((x == testBorders[z][0][0]-1) or (x == testBorders[z][0][1]-1) or (y == testBorders[z][1][0]-1) or (y == testBorders[z][1][1]-1)):
                            # print "Border %s %s" % (x,y)
                            debugim[x,y] = (0, 0, 255) # in debug mode, mark all border pixel to blue
                    # Just check green channel as it's the highest quality channel
                    pixdiff = abs(buffer1[x,y][1] - buffer2[x,y][1])
                    if pixdiff > threshold:
                        changedPixels += 1
                        if (debugMode):
                            debugim[x,y] = (0, 255, 0) # in debug mode, mark all changed pixel to green
                    # Save an image if pixels changed
                    if (changedPixels > sensitivity):
                        takePicture = True # will shoot the photo later
                    if ((debugMode == False) and (changedPixels > sensitivity)):
                        break  # break the y loop
                if ((debugMode == False) and (changedPixels > sensitivity)):
                    break  # break the x loop
            if ((debugMode == False) and (changedPixels > sensitivity)):
                break  # break the z loop
        print(takePicture)
        if (debugMode):
            debugimage.save(filepath + "/debug.bmp") # save debug image as bmp
            print("debug.bmp saved, %s changed pixel" % changedPixels)
        # else:
        #     print "%s changed pixel" % changedPixels

        # Check force capture
        if forceCapture:
            if time.time() - lastCapture > forceCaptureTime:
                takePicture = True

        if takePicture:
            lastCapture = time.time()
            saveImage(cameraSettings, saveWidth, saveHeight, saveQuality, diskSpaceToReserve, count)
            count = count + 1
            todaycount = todaycount + 1
            if (todaycount > 15):
                print('recalibrating')
                todaycount = 0
                newbright = findBrightness()
                print("brightness recalibrate: %s" % (newbright))
                cameraSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise' " % (newbright)



        # Swap comparison buffers
        image1 = image2
        buffer1 = buffer2

# Run camera
motion()

And then copy.py:

#!/bin/bash
sudo python /home/pi/Desktop/_copy.py

Which calls _copy.py:

# copy to USB if plugged in here, and so on
import os
import RPi.GPIO as GPIO
import time
import shutil

GPIO.setwarnings(False)

GPIO.setmode(GPIO.BCM)
GPIO.setup(22,GPIO.OUT)
path = '/home/pi/Desktop/_photos/'
GPIO.output(22, True)
num_files = len([f for f in os.listdir(path)if os.path.isfile(os.path.join(path, f))])
while num_files > 0:
    time.sleep(1)
    dest = '/cake'
    for r, d, f in os.walk('/media/pi/'):
        for folder in d:
            if os.path.isdir(os.path.join('/media/pi/', folder)) and folder == 'FAD0-42B83':
                dest = os.path.join('/media/pi/', folder)
    if dest == '/cake':
        time.sleep(15)
    else:
        try:
            if os.path.isdir(dest):
                files = os.listdir(path)
                f1 = files[0]
                shutil.copy(path+f1, dest)
                if os.path.isdir(dest):
                    os.remove(path+f1)
                num_files = len([f for f in os.listdir(path)if os.path.isfile(os.path.join(path, f))])
        except Exception as e:
            print(e)
            time.sleep(10)
GPIO.output(22, False)

I put it all in first for the working copy-paste that some usually go to like I do, first.

def findBrightness():
    print('finding brightness')
    best = 0
    pre = 0
    for (q) in range(8):
        t = q * 10 + 10
        print(t)
        briSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise'" % (t)
        print(briSettings)
        command = "raspistill %s -w 64 -h 48 -t 200 -e bmp -n -o -" % (briSettings)
        imageData = io.BytesIO()
        imageData.write(subprocess.check_output(command, shell=True))
        imageData.seek(0)
        im = Image.open(imageData)
        buffer = im.load()
        startAt = buffer[0,0]
        numChanged = 0
        for (r) in range(63): # 0 to 62 => 1 to 63
            for (s) in range(47): # 0 to 46 => 1 to 47
                if (startAt <> buffer[r,s]):
                    numChanged = numChanged + 1
        if (numChanged > pre):
            best = t
            pre = numChanged
            print(" - changed to %s with %s" % (best, pre))
        else:
            print(numChanged)
        imageData.close()
    return best

First I thought about it and felt like adjusting brightness. That first comes through findBrightness. When this function is called it takes a tiny image at each –brightness, in increments of 10 excluding 0 and 100, to count the changes it can see in the 64×48 pixel image it took.

We run it at the start, which as a note adds a delay to seeing the first motion detection images on the counter in Third below, then we run it after a certain number of motion detections.

# -- added by edg3: works out best brightness for most differences
print('finding brightness...')
bestbrightness = findBrightness()
print("best brightness: %s" % (bestbrightness))
cameraSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise' " % (bestbrightness)

Take note that’s above, it currently does it twice, I need to fix naming somewhere:

def motion():
    todaycount = 0
    path = '/home/pi/Desktop/_photos/'
    count = len([f for f in os.listdir(path)if os.path.isfile(os.path.join(path, f))])
    count = count + 1
    cameraSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise'" % bestbrightness

Then we adjust the takePicture:

        if takePicture:
            lastCapture = time.time()
            saveImage(cameraSettings, saveWidth, saveHeight, saveQuality, diskSpaceToReserve, count)
            count = count + 1
            todaycount = todaycount + 1
            if (todaycount > 15):
                print('recalibrating')
                todaycount = 0
                newbright = findBrightness()
                print("brightness recalibrate: %s" % (newbright))
                cameraSettings = "--brightness %s --contrast 50 --awb 'sun' --exposure 'backlight' --imxfx 'denoise' " % (newbright)

Every 15 pictures we take detecting motion we take a moment to recalibrate the brightness we use for the camera shots. I found, for instance on Valentines Day, the sun came out the clouds and set at the same brightness we lost hundreds of motion detection photos since it got way more light. We got awesome photos, I just felt perhaps it’s better to adjust brightness slowly over time.

We also made it more sensitive, so to speak, since the birds I want to catch flying in and out their nest were too small in the images.

The other note is and folder == ‘FAD0-42B83’ inside _copy.py. I found my newer, larger, and faster, USB drive only shows as FLASH1 with multiple other FLASH numbers, it started to delete photos by accident. I guessed I could use my older USB alone, since it’s tiny, and make sure I’m copying the photos.

While the repetition at the start is a touch troublesome for the first brightness reading, I’ve enjoyed making this work properly and cleaned the code a little for us.

Third: Web Control

The point of the executables above was to make a way we could control the Pi remotely wherever I feel like going and using it. You’d note above I installed apache, and I just made a simple set of php files to run on the web server to control it.

All below in /var/www/html/camera/:

<html>
<head><title>Pi NoIR Camera : edg3</title></head>
<body>
  <h3>Pi NoIR Camera : edg3</h3><hr />
  <?php
    $directory = '/home/pi/Desktop/_photos/';
    $filecount = 0;
    $files = glob($directory . '*');
    if ($files) {
      $filecount = count($files);
    }
    echo "There are $filecount photos.";
?>
  <hr />
  <a href="start.php">Start Camera</a><br /><br />
  <a href="move.php">Move to USB</a><br /><br />
  <a href="get.php">Get Image</a><br /><br />
  <hr />
  v1.2
</body>
</html>
<?php
  shell_exec('sudo /home/pi/Desktop/start.py > /dev/null &');
  header("Location: index.php");
<?php
  shell_exec('sudo /home/pi/Desktop/copy.py > /dev/null &');
  header("Location: index.php");
<?php
  shell_exec('sudo /home/pi/Desktop/get.py > /dev/null &');
  sleep(10);
?>
<html>
  <head><title>Pi NoIR Preview</title></head>
  <body>
    <h3>Current View:</h3><hr />
    <br />
    <a href="index.php">Home</a><br />
    <br />
    <hr />
    <br />
    <img src="cam.jpg" />
  </body>
</html>

See? The sleep(10) I’ve found is the perfect time to make sure we’ve received the small preview photo. Take note, _move.py, _start.py, and get.py, need to be executable.

Fourth: A New Smaller Box

I decided I need to move it away from this box:

Original box I made

So, now the box is easier to handle:

New box I made
Back side of the new box

Final Words

I’ll be using it over the next few days to see how the adjustments fare, so obviously I’ll share more photos here soon. I look forward to my tests this week.