2017-08-03

Raspberry Pi Camera: Most Simple Motion Detection Logic


I want to make my newly purchased Pi as a home surveillance appliance.

My requirements are very simple:

  • it is purely camera based (i.e. no need to install Passive Infrared (PIR) motion sensor)
  • it is still photo based (i.e. not video based)
  • it should not consume too much of my Pi resources (CPU or GPU)
  • it will take photos when it detected image changes
  • the photos will be uploaded to Google Drive

I have checked many resources on internet.  The main stream is to either use opencv (or its derivative SimpleCV) or the Motion package (or even the whole OS with motionEyeOS).  Although some ports are said to utilize the Pi's GPU resources, I notice the performance is still not satisfactory (low resolution or low frame rate).

Considering my case that most of the time, my periodically captured photos are nearly the same, except some lighting changes, noises, clock face changes etc and inspired various projects (e.g. brainflakes  or Sarah Henkens' blog), I devise my motion detection algorithm as follows:

(a) the task will be kicked off by cron

(b) use Pi's command line to capture still picture onto the disk

(c) I will compare the grey-scale of each pixel in consecutive snapshots.  Someone said for RGB domain, using the Green channel for comparison is sufficient.  But Pi also supports YUV output.  The Y channel (luminance) actually is the brightness of each pixel.  Pi's YUV format is YUV420 and the first 2/3 of the file contains the Y data.

(d) if the number of pixels with change in Y (luminance) is greater than a limit, then this photo will be uploaded to Google Drive

(e) the photo will be converted to JPEG before the upload.  I use Imagemagick's convert command to do the job.  Remark: although I take my still picture at maximum resolution 3280x2464, because of the padding (horizontal: multiples of 32, vertical: multiples of 16), the resultant YUV file is of resolution of 3296x2464.  Since YUV file has no header to indicate the information, I need to specify the figure in the Imagemagick's convert command.

However, I find the major challenge is how to specify limit in step (d).  Therefore I conduct a data analysis of sensitivity of different values of change of Y.


As shown in the above graph, the bottom group of lines are those image with no motion detection (the variation is due to background noise).  The upper group of lines (with lighter colour) are those image with motion detection.  Finally I choose the following:
  • Change of Y: 50
  • Count: 50
However, after running for several days, I find that the camera is too sensitive that it trigger the capture even though I saw there is no noticeable object movement on the image.  Of course I have tried to adjust the above detection threshold using trial-and-error.  But as you know, I am only struggling between the dilemma of false positive and false negative scenario.  If I decrease the sensitivity significantly , then I may miss a capture if there is real object movement.

Finally I find the false detection scenarios are mainly due to too reasons:
  • overall ambient background brightness change due to sunshine (although it is a indoor environment)
  • there a portion of screen due to a white window curtain, which has a high value of Y and its changes of Y will affect the overall counter significantly
My original logic is to just look into a single point of "Change of Y", assuming that it is on a global applicable trend.  But it is not necessarily true.  Therefore to emphasize the object detection, I adopt a an accumulative approach, by counting the number of pixels with changes greater than a value.  For the second issue, my original logic use absolute logic and therefore a pixel changing from Y=1 to Y=11 has the same effect with another pixel changing from Y=245 to Y=255.  But from a subjective point, I think a relative change is more appropriate.

The following is effect after the change:


After the adjustment,, it can be observed that:

  1. the vertical axis interception is always 8121344, which is the total number of pixel of the image
  2. the lines of the background image (those in lighter color) are nearly straight line (with vertical axis in log scale)
  3. the lines of the image with motion (those in darker color) have a more horizontal line segment

I then choose the following criteria:
  • Relative Change of Y: 100
  • Accumulative count of pixels: 10000
Finally about the details of implementation.  I have only coded two programs.  The first is a C program called yuv_ardiff (viz. yuv's accumulative and relative diff) for step (c) above. [Remark: I am still learning python].  The second is a bash script will implement the whole logic.
The following are the source codes:

/***********
File        : yuv_ardiff.c
Description : To compare the Y space of two yuv files for motion detection
              Accumulative Relative Diff
Usage       : yuv_ardiff file1.yuv file2.yuv
Modification History
2017-08-01  Initial version
2017-08-06  Change counter to accumulative
2017-08-08  Use Relative ratio
*/

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main (int argc, char **argv)
{
struct stat st;
int file1_size, file2_size;
int y_size;
int i;
FILE *fp1;
FILE *fp2;
unsigned char byte1, byte2;
int diff, sum, rdiff;;
int count[256];

if (argc != 3) {
  printf ("Usage: %s file1.yuv file2.yuv\n", argv[0]);
  return 0;
  }


if (stat(argv[1], &st) == -1) {
  printf ("Error: Cannot get the file size of %s\n", argv[1]);
  return 0;
  }
file1_size = st.st_size;
/* printf ("%d\n", file1_size); */

if (stat(argv[2], &st) == -1) {
  printf ("Error: Cannot get the file size of %s\n", argv[2]);
  return 0;
  }
file2_size = st.st_size;
/* printf ("%d\n", file2_size); */

if (file1_size != file2_size) {
  printf ("Error: File size of %s (%d) is not equal to %s (%d)\n", argv[1], file1_size, argv[2], file2_size);
  return 0;
  }

/* The first 2/3 are Y, next 1/6 is U and last 1/6 is V */
if ((file1_size % (3*16*16)) != 0) { /* actual is 1.5 * 32 * 16 */
  printf ("Error: File size (%d) should be multiple of 1.5*32*16\n", file1_size);
  return 0;
  }


fp1 = fopen (argv[1], "r");
if (fp1 == NULL) {
  printf ("Error: Cannot open file (%s)\n", argv[1]);
  return 0;
  }

fp2 = fopen (argv[2], "r");
if (fp2 == NULL) {
  printf ("Error: Cannot open file (%s)\n", argv[2]);
  return 0;
  }

y_size = file1_size * 2 / 3;
/* printf ("y_size = %d\n", y_size); */
for (i=0; i<256; i++)
  count[i] = 0;

for (i=0; i< y_size; i++) {
  if (fread (&byte1, 1, 1, fp1) != 1) {
    printf ("Error: Cannot read byte from (%s)\n", argv[1]);
    return 0;
    }
  if (fread (&byte2, 1, 1, fp2) != 1) {
    printf ("Error: Cannot read byte from (%s)\n", argv[2]);
    return 0;
    }

  diff = abs (byte1 - byte2);
  sum = byte1 + byte2;

  if (sum > 0)
    rdiff = (diff * 256) / sum;
  else
    rdiff = 0;

  if (rdiff > 255) {
    printf ("Error: rdiff (%d) is greater than 255\n", rdiff);
    return 0;
    }

  (count[rdiff])++;

  } // for loop

fclose (fp1);
fclose (fp2);

for (i=254; i>=0; i--)
    count[i] += count[i+1];

for (i=0; i<256; i++)
  printf ("%d ", count[i]);
printf ("\n");

return 0;
} // main

=====================================================
#!/bin/bash
# File: pi_camera_motion.sh
cd /home/whc/motion
rm -f last.yuv
if [ -e current.yuv ]; then
mv current.yuv last.yuv
fi
DATE=`date +"%Y%m%d_%H%M%S"`
raspiyuv -n -h 2464 -w 3280 -o current.yuv
if [ -e last.yuv ]; then
yd=`/home/whc/bin/yuv_adiff last.yuv current.yuv`
echo ${DATE} ${yd} >> /home/whc/yuv_ardiff.log
diff_count=`echo ${yd} | awk '{print \$101}'`
if [ "${diff_count}" -gt "10000" ]; then
convert -size "3296x2464" -depth 8 -sampling-factor "4:2:0" yuv:current.yuv pi_photo_${DATE}.jpg
/home/whc/gdrive-linux-rpi upload --parent xxxxxxxxxxxxxxxxxxxxxxxxxx pi_photo_${DATE}.jpg
fi
fi

2017-07-26

Take periodic photos using Pi camera and upload to Google Drive


Taking photos using the Pi camera is easy.  But how to allow remote access of the photo is a headache.  I do not want to Port Forwarding in my router (especially on SSH ports) because I think my Pi is not yet appropriately hardened.  I decide to upload the photos to the cloud.

Google has well-documented API for the Google Drive access.  In particular, I love Petter Rasmussen's gdrive, which is a CLI (command-line-interface) client for many Operating Systems because it is a static binary which does not need external libraries.

I have tried the Windows 64 bit build and the Raspberry Pi build and it works.  For the Windows build, I even try it behind the proxy server of my company and it works after I set up the http_proxy environment variable.

The github page of the gdrive already provides extensive documentation (with examples) and it is easy to adopt.  The following are my major usage:

(1) Initial setup

gdrive about

gdrive will print out a URL.  Using browser to launch the URL and enter the account password, Google will print out a verification code.  I will enter the verification code to the CLI.

There is documentation on Service Account, which is said to make server-to-Google access with password-less access.  But I find that even using the verification code approach, I find gdrive can refresh the token file automatically.  In other words, I only need to input the verification code once.  Therefore I finally do not bother to set up a Service Account.

(2) List File details

gdrive list --query "name = 'file_name_or_folder_name' "

C:\PortableApps\gdrive>gdrive-windows-x64.exe list --query "name = 'PI_PHOTO_00.jpg' "
Id                             Name              Type   Size     Created
xxxxxxxxxxxxxxxxxxxxxxxxxxxx   PI_PHOTO_00.jpg   bin    4.8 MB   2017-07-26 10:00:16

C:\PortableApps\gdrive>gdrive-windows-x64.exe list --query "name = 'pi-photos' "
Id                             Name        Type   Size   Created
xxxxxxxxxxxxxxxxxxxxxxxxxxxx   pi-photos   dir           2017-07-21 19:50:23

(3) List Files inside a folder

gdrive list --query " 'Parent_Folder_Id' in parents"

C:\PortableApps\gdrive>gdrive-windows-x64.exe list --query " '0Bzo1pJKfp9QEdVZFdVV2RTV2RVE' in parents "
Id                             Name              Type   Size     Created
xxxxxxxxxxxxxxxxxxxxxxxxxxxx   PI_PHOTO_35.jpg   bin    4.8 MB   2017-07-26 10:35:12
xxxxxxxxxxxxxxxxxxxxxxxxxxxx   PI_PHOTO_30.jpg   bin    4.7 MB   2017-07-26 10:30:12

(4) Upload File to specified folder

gdrive --parent Parent_Folder_Id Upload_Filename

(5) Delete File

gdrive delete  File_Id

I then start to code my bash script (which will be run by cron periodically) to take photo and then upload the photo.  I use the minute digits to name my photo filename and originally thought that this nomenclature can automatically recycle my photo copies without an explicit housekeeping job.  But I am wrong.  I find Google Drive allow multiple copies of the same filename (even not using the versioning feature) and these multiple copies will be assigned with different File-Id.

Worst still I find gdrive has no direct command to delete a file (or files) by filename.  I need to first list the File_id by the inputted filename and then delete them (if more than one) one-by-one.  The logic is:

gdrive list --no-header --query "name = 'Filename' " | awk '{print $1}' | xargs -n 1 gdrive delete

The xargs command is to ensure the gdrive is executed one-by-one

The following is my script:

#!/bin/bash
# use 00-59 minutes for filename recycling
DATE_MM=$(date +"%M")
cd  /home/pi/photos
raspistill -n -o PI_PHOTO_${DATE_MM}.jpg
# delete duplicated copies at Google drive
/home/pi/gdrive-linux-rpi list --no-header --query "name = 'PI_PHOTO_${DATE_MM}.jpg'" | awk '{print $1}' | xargs -n 1 /home/pi/gdrive-linux-rpi delete
# upload to pi_photos folder at Google Drive
/home/pi/gdrive-linux-rpi upload --parent xxxxxxxxxxxxxxxxxxxxxxxxxxxx --delete PI_PHOTO_${DATE_MM}.jpg