Raspberry Pi Zero 2 W – NoIR Camera Progress 1

Spread the love

My next step is to bring the Pi to a re-usable state. A simple web interface to turn the camera on, or off, then a method to refresh the latest image. Then, I had to make a boot script to see if I have internet access, if I do, I am on my home network, if not – create a wifi hotspot.

As a side note, haven’t tested the Wi-Fi hotspot yet, it may have a bug or two in it still. The important part is simple logic, I just haven’t tested the hotspot yet.

While I wont explain how to install things, such as Mariadb for MySql, I will add notes for actions I needed to find the small problems which occurred.

1. The MySQL (Mariadb) Database

The structure is simple at this point, this is mostly proof of concept for the web interaction.

create database local_picam;
use local_picam;
create table pc_options (id int PRIMARY KEY AUTO_INCREMENT, opt varchar(255) NOT NULL, valu varchar(255) NOT NULL);
insert into pc_options (opt, valu) VALUES ('running','false');

Now, it is important to note, I found it easiest to use python 3, and as such you’d need to install python3, and then:

pip3 install mysql-connector-python-rf

Note, the use:

mydb = conn.connect(host="localhost",user="pi",password="", database="local_picam")

You will need to add a user ‘pi‘ with a blank string password for use. Then remember to GRANT ALL ON *.* TO ‘pi’@’localhost’.

2. The Wi-Fi Hotspot

Added to rc.local with sudo privileges set up correctly:

import time
import os
import subprocess

time.sleep(60)

def is_connected():
    res = os.popen('ip addr').read()
    if '<NO-CARRIER,' in res:        
        return False
    return True

if not is_connected():
  os.system('sudo ifconfig hotspot 192.168.1.2 up')

The idea is it can take a moment or two to for the Wi-Fi connection at times, so if we don’t have an assigned IP address we supposedly create the hotspot.

3. My Adjusted _start.py

Note, we will take a look at a few small section adjustments, then you can see the entire code. Always note, I didn’t remove all I would need to, rather, I left things like a few GPIO includes since I will work out how I want to use it soon.

Instead of making a command to start the camera with a shell script out of apache2, we start _start.py immediately.

Let’s start with findBrightness:

def findBrightness():
    print('finding brightness')
    best = 0
    pre = 0
    for (q) in range(5):
        t = q * 10 + 30 # 30, 40, 50, 60, 70 => 2 either side
        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) or (pre < 333):
            best = t
            pre = numChanged
            print(" - changed to %s with %s" % (best, pre))
        else:
            print(numChanged)
        imageData.close()
    return best

I’ve decided that since I’m moving off my RPi 1 onto the quad core of the Zero 2 W I can make an effort to double check brightness levels. The idea is take an image at a few levels, 30% to 70% brightness levels, and try work out which has the most different colors. I know the idea has logic flaws, but I’ve decided to keep experimenting with it.

With the quad cores, I call this after each time it detects motion. I believe I should just put it where I did the ’15’ pictures repetition on line 201, I’m just a little undecided, so I’m experimenting.

The idea is that if ‘running‘ is ‘false‘, we can wait before it starts.

def motion():
    # wait till 'on'
    mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
    com = mydb.cursor()
    com.execute("USE local_picam")
    com.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
    is_running = False
    for x in com:
        print(x[2])
        if x[2] == 'true':
            is_running = True
    while not is_running:
        mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
        com = mydb.cursor()
        com.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
        for x in com:
            print(x[2])
            if x[2] == 'true':
                is_running = True
            else:
                print("waiting to start")
                sleep(60)
    
    # start capture

When it start it will check the database for which value we’ve set it to.

Consider going to a game reserve, much like I’ve done, when you get there you can mount your camera somewhere on your dashboard. You can use a power bank, or even a USB connector you have for your car. Connect to the Wi-Fi hotspot with your phone, navigate the browser to the RPi, and turn the camera on. It will go ‘alright, let’s start detecting.‘ When you want to stop the motion detection, you can just turn it off again. Whenever we stopped to get out the car and turned it off it continued working when turned back on.

The script will still run, it also has another web interface use below, however it will go ‘alright, let’s wait till you want me to detect again.

The sleep(60) might be overkill, since in testing I felt it was a little too long while testing, it has the intention to lower effort the RPi needs to make while it waits.

There are two things that are important with this:

        mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
        com = mydb.cursor()
        com.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
        print("checking")
        for x in com:
            if x[2] == 'false':
                is_running = False
        while not is_running:
            mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
            com_pause = mydb.cursor()
            com_pause.execute("USE local_picam")
            com_pause.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
            for y in com_pause:
                if y[2] == 'true':
                    is_running = True
                else:
                    print("paused")
                    sleep(60)

The pause action could probably take the same structure as the wait at the start of motion(). The important note here is I had a little bug with the logic.

When the same mydb is used for different queries it doesn’t retrieve the new value when you run an update on it. I will likely move it to its own function soon, so it’s a single function call, since it can cleaner code.

With those mentioned, enjoy the messy large file:

#!/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
import mysql.connector as conn
from time import sleep

# 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(5):
        t = q * 10 + 30 # 30, 40, 50, 60, 70 => 2 either side
        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) or (pre < 333):
            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/edg3/Desktop/_photos"
filenamePrefix = "img"
diskSpaceToReserve = 40 * 1024 * 1024 # Keep 40 mb free on disk
# Brightness starts at 50
bestbrightness = findBrightness()
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]] ]

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():
    # wait till 'on'
    mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
    com = mydb.cursor()
    com.execute("USE local_picam")
    com.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
    is_running = False
    for x in com:
        print(x[2])
        if x[2] == 'true':
            is_running = True
    while not is_running:
        mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
        com = mydb.cursor()
        com.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
        for x in com:
            print(x[2])
            if x[2] == 'true':
                is_running = True
            else:
                print("waiting to start")
                sleep(60)
    
    # start capture
    todaycount = 0
    path = '/home/edg3/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
        
        mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
        com = mydb.cursor()
        com.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
        print("checking")
        for x in com:
            if x[2] == 'false':
                is_running = False
        while not is_running:
            mydb = conn.connect(host="127.0.0.1",user="pi",password="", database="local_picam")
            com_pause = mydb.cursor()
            com_pause.execute("USE local_picam")
            com_pause.execute("SELECT * FROM pc_options WHERE opt='running' LIMIT 1")
            for y in com_pause:
                if y[2] == 'true':
                    is_running = True
                else:
                    print("paused")
                    sleep(60)
        bestBrightness = findBrightness()


# Run camera
motion()

4. The Web Interaction

Simple UI

Since I do heavy work in Web Development at the moment, this isn’t pretty enough for me. That being said, I added jQuery for the method to refresh the image with Get Latest.

<?php
  $server = 'localhost';
  $username = 'pi';
  $password = '';
  $conn = new mysqli($server, $username, $password);

  if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error());
  }

  $conn->query('USE local_picam');
  if (isset($_POST['f_value'])) {
    //echo 'here';
    $opt = $_POST['f_value'];
    if ($opt == 'running') {
      // Stop
      $conn->query('UPDATE pc_options SET valu=\'false\' WHERE opt=\'running\'');
      //echo 'updated to false';
    }
    else {
      // Start camera
      $conn->query('UPDATE pc_options SET valu=\'true\' WHERE opt=\'running\'');
      //echo 'updated to true';
    }
  }

  $style = 'running';
  $text = 'Stop camera';

  $result = $conn->query('SELECT valu FROM pc_options WHERE opt=\'running\' LIMIT 1');
  while ($row = $result->fetch_array()) {
    if ($row[0] == 'false') {
      $style = 'stopped';
      $text = 'Start camera';
    }
  }

  $dir = '/home/edg3/Desktop/_photos';
  $fi = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS);
  $count = iterator_count($fi);
?>
<doctype !html>
<html>
<head>
  <title>camera_control</title>
  <script type="text/javascript" src="jquery-latest.min.js"></script>
  <style>
    input { cursor: pointer; }
    input.hidden {display: none;}
    input.stopped {display: block;width: 90%;max-width: 400px;min-width: 300px;padding: 10px;border: 1px solid black;color: #333;background-color: #00c9ff;border-radius: 5px;}
    input.running {display: block;width: 90%;max-width: 400px;min-width: 300px;padding: 10px;border: 1px solid black;color: #333;background-color: #FFc900;border-radius: 5px;}
    .latest img {width:90%;padding-left:5%;height:auto;}
    .get_latest {display: block;border: 1px solid #000;margin-bottom: 15px;width: 130px;text-align: center;padding: 10px;background: #0f6;border-radius: 5px;cursor: pointer;}
  </style>
</head>
<body>
  <h3>Photo count: <?php echo $count; ?></h3>
  <form action="./index.php" method="post">
    <input name="f_value" class="hidden" value="<?php echo $style; ?>">
    <input type="submit" class="<?php echo $style; ?>" value="<?php echo $text; ?>">
  </form>
  <div class="get_latest">Get Latest</div>
  <script>
    $(document).ready(function(){
      $('.get_latest').on('click', function(){
        $('.latest').html('<em>A few moments...</em>');
        $.ajax({
          method: 'get',
          url: 'latest.php',
          success:function(ans){
            $('.latest').html(ans);
          }});
      });
    });
  </script>
</body>
</html>

As you can see, a simple request is made for the image. latest.php handles the rest of it.

The important note is you can see I made it super simple. The interaction will know if the camera is set to running, or not. Then update either direction to turn it on or off. With how fast the Zero 2 W is it knows when to stop immediately. Which is why I almost thing I should make the pause stop refresh after perhaps 5 seconds to lower data interaction.

<?php
  if (file_exists('latest.jpg')) {
    unlink('latest.jpg');
  }
  $path = '/home/edg3/Desktop/_photos';

  $latest_ctime = 0;
  $latest_filename = '';

  $d = dir($path);
  //var_dump($d);
  while (false !== ($entry = $d->read())) {
    //var_dump($entry);
    $filepath = "{$path}/{$entry}";
    if (is_file($filepath) && filectime($filepath) > $latest_ctime) {
      $latest_ctime = filectime($filepath);
      $latest_filename = $entry;
    }
  }
  //var_dump($latest_filename);
  if (!copy($path.'/'.$latest_filename,'/var/www/html/latest.jpg')) die('FAILED TO COPY '.$latest_filename.'...');
  sleep(15);
?>
<img src="latest.jpg" >

As you can see, the interaction is simple here.

  • If we have a latest.jpg remove it
  • Find which file in _photos is newest
  • Selected newest file gets copied to /var/www/html/ where the php file is
  • If it gives an error we share we couldn’t copy it

Note the answer from the request out as an html element. If there isn’t an image and it throws an error it returns the string message.

I wanted to make it a simple interface to double check the latest image whenever you’d like, and turn it on or off as you please. The idea behind this, for me, personally, happens to be making it easier to double check how I mounted my camera.

Note: Copy

Instead of the python copying I had before, I decided to rather use samba and share the whole Desktop folder. It makes it easier to do tons more, I’m just happy it means I can work on my Pi NoIR camera project more often now. I feel the python script to move files to the USB is a little too logically flawed.