Screaming Halloween Scary Mask with TFT Eyes and image capture

Table of contents

Screaming Halloween Scary Mask with TFT Eyes and image capture

Abstract

SEEED Studio Xiao Round TFT Displays for eyes on a scary Halloween mask with raspberry pi camera and screaming witch sounds.

I started this project as an upgrade to an older scary Halloween project with raspberry pi.

Halloween-of-Things (HoT) And Your Little Internet Too - Hackster.io

I got a new scary mask and some Seeed Studio XIAO round tft displays for eyes.

image  

My plan was to use my old python code to sense a trick-or-treater with a proximity detector, then play a scary maniacal witch's laugh/scream with the raspberry pi and take a photo with the Rpi camera.

In addition, I would show some strange eyes on the tft displays for added effect.

image

I used two Seeed Studio XIAO nRF52840 processors with the tft displays.

image

image

The image converter I used is:

GitHub - notisrac/FileToCArray: Coverts any file to a C style array. (It can also do image color format and size coversion)

I had to make a Left eye array file and a Right eye array file.

image

The XIAO program I used was a modified Arduino GUI example called TFT_flash_jpg.

// Renders images stored in an array in program (FLASH)
// The JPEG images are stored in header files (see jpeg1.h etc.)

// As well as the TFT_eSPI library:
// https://github.com/Bodmer/TFT_eSPI
// the sketch needs the JPEG Decoder library. This can be loaded via the Library Manager.
// or can be downloaded here:
// https://github.com/Bodmer/JPEGDecoder

//----------------------------------------------------------------------------------------------------
#define BOARD_SCREEN_COMBO 501
#include "driver.h"
#include <SPI.h>
#include <TFT_eSPI.h>

TFT_eSPI tft = TFT_eSPI();


// JPEG decoder library
#include <JPEGDecoder.h>

// Return the minimum of two values a and b
#define minimum(a,b)     (((a) < (b)) ? (a) : (b))

// Include the sketch header file that contains the image stored as an array of bytes
// More than one image array could be stored in each header file.
#include "jpeg1.h"

// Count how many times the image is drawn for test purposes
uint32_t icount = 0;
//----------------------------------------------------------------------------------------------------


//####################################################################################################
// Setup
//####################################################################################################
void setup() {
  Serial.begin(115200);
  tft.begin();
}

//####################################################################################################
// Main loop
//####################################################################################################
void loop() {

  tft.setRotation(1);  // portrait
  tft.fillScreen(TFT_RED);

  drawArrayJpeg(ScaryEyeLeft, sizeof(ScaryEyeLeft), 0, 0); // Draw a jpeg image stored in memory at x,y
  delay(2000);

  
}

//####################################################################################################
// Draw a JPEG on the TFT pulled from a program memory array
//####################################################################################################
void drawArrayJpeg(const uint8_t arrayname[], uint32_t array_size, int xpos, int ypos) {

  int x = xpos;
  int y = ypos;

  JpegDec.decodeArray(arrayname, array_size);
  
  jpegInfo(); // Print information from the JPEG file (could comment this line out)
  
  renderJPEG(x, y);
  
  Serial.println("#########################");
}

//####################################################################################################
// Draw a JPEG on the TFT, images will be cropped on the right/bottom sides if they do not fit
//####################################################################################################
// This function assumes xpos,ypos is a valid screen coordinate. For convenience images that do not
// fit totally on the screen are cropped to the nearest MCU size and may leave right/bottom borders.
void renderJPEG(int xpos, int ypos) {

  // retrieve information about the image
  uint16_t *pImg;
  uint16_t mcu_w = JpegDec.MCUWidth;
  uint16_t mcu_h = JpegDec.MCUHeight;
  uint32_t max_x = JpegDec.width;
  uint32_t max_y = JpegDec.height;

  // Jpeg images are draw as a set of image block (tiles) called Minimum Coding Units (MCUs)
  // Typically these MCUs are 16x16 pixel blocks
  // Determine the width and height of the right and bottom edge image blocks
  uint32_t min_w = minimum(mcu_w, max_x % mcu_w);
  uint32_t min_h = minimum(mcu_h, max_y % mcu_h);

  // save the current image block size
  uint32_t win_w = mcu_w;
  uint32_t win_h = mcu_h;

  // record the current time so we can measure how long it takes to draw an image
  uint32_t drawTime = millis();

  // save the coordinate of the right and bottom edges to assist image cropping
  // to the screen size
  max_x += xpos;
  max_y += ypos;

  // read each MCU block until there are no more
  while (JpegDec.readSwappedBytes()) {
	  
    // save a pointer to the image block
    pImg = JpegDec.pImage ;

    // calculate where the image block should be drawn on the screen
    int mcu_x = JpegDec.MCUx * mcu_w + xpos;  // Calculate coordinates of top left corner of current MCU
    int mcu_y = JpegDec.MCUy * mcu_h + ypos;

    // check if the image block size needs to be changed for the right edge
    if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
    else win_w = min_w;

    // check if the image block size needs to be changed for the bottom edge
    if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
    else win_h = min_h;

    // copy pixels into a contiguous block
    if (win_w != mcu_w)
    {
      uint16_t *cImg;
      int p = 0;
      cImg = pImg + win_w;
      for (int h = 1; h < win_h; h++)
      {
        p += mcu_w;
        for (int w = 0; w < win_w; w++)
        {
          *cImg = *(pImg + w + p);
          cImg++;
        }
      }
    }

    // draw image MCU block only if it will fit on the screen
    if (( mcu_x + win_w ) <= tft.width() && ( mcu_y + win_h ) <= tft.height())
    {
      tft.pushRect(mcu_x, mcu_y, win_w, win_h, pImg);
    }
    else if ( (mcu_y + win_h) >= tft.height()) JpegDec.abort(); // Image has run off bottom of screen so abort decoding
  }

  // calculate how long it took to draw the image
  drawTime = millis() - drawTime;

  // print the results to the serial port
  Serial.print(F(  "Total render time was    : ")); Serial.print(drawTime); Serial.println(F(" ms"));
  Serial.println(F(""));
}

//####################################################################################################
// Print image information to the serial port (optional)
//####################################################################################################
void jpegInfo() {
  Serial.println(F("==============="));
  Serial.println(F("JPEG image info"));
  Serial.println(F("==============="));
  Serial.print(F(  "Width      :")); Serial.println(JpegDec.width);
  Serial.print(F(  "Height     :")); Serial.println(JpegDec.height);
  Serial.print(F(  "Components :")); Serial.println(JpegDec.comps);
  Serial.print(F(  "MCU / row  :")); Serial.println(JpegDec.MCUSPerRow);
  Serial.print(F(  "MCU / col  :")); Serial.println(JpegDec.MCUSPerCol);
  Serial.print(F(  "Scan type  :")); Serial.println(JpegDec.scanType);
  Serial.print(F(  "MCU width  :")); Serial.println(JpegDec.MCUWidth);
  Serial.print(F(  "MCU height :")); Serial.println(JpegDec.MCUHeight);
  Serial.println(F("==============="));
}

//####################################################################################################
// Show the execution time (optional)
//####################################################################################################
// WARNING: for UNO/AVR legacy reasons printing text to the screen with the Mega might not work for
// sketch sizes greater than ~70KBytes because 16-bit address pointers are used in some libraries.

// The Due will work fine with the HX8357_Due library.

void showTime(uint32_t msTime) {
  //tft.setCursor(0, 0);
  //tft.setTextFont(1);
  //tft.setTextSize(2);
  //tft.setTextColor(TFT_WHITE, TFT_BLACK);
  //tft.print(F(" JPEG drawn in "));
  //tft.print(msTime);
  //tft.println(F(" ms "));
  Serial.print(F(" JPEG drawn in "));
  Serial.print(msTime);
  Serial.println(F(" ms "));
}

The motion sensor was part of a night light available at the local hardware store.

image

image

I powered the motion detector circuit from the 3.3V from the Rpi and used one of the LED outputs as a GPIO trigger input.

Last time I used the other LED outputs as eyes for the mask.

image

The old code didn't work of course on a Rpi5, so I had to change some things.  The Gmail server won't allow unsecure posts, so I had to remove that code. 

import RPi.GPIO as GPIO
import time
import os
import pygame
from shutil import copyfile
IRinputPin = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(IRinputPin,GPIO.IN, pull_up_down=GPIO.PUD_UP)
count = 10
pygame.mixer.init()
pygame.mixer.music.load("/home/pi/Music/Witch.mp3")
print('System Reset')
while (count>0):
    print(count)
    count-=1
    time.sleep(1)
print('System Armed')
while True:
    print('Waiting for input')
    if (GPIO.input(IRinputPin)):
        time.sleep(1)
    else:
        print('IR input detected')
        os.system("fswebcam -r 960x720 -d /dev/video0 /home/pi/webcam.jpg")
        timestr = time.strftime("%Y%m%d-%H%M%S")
        filename2 = ('/home/pi/'+timestr+'.jpg')
        copyfile('/home/pi/webcam.jpg', filename2)
        print('Saving Picture')
        os.system("python /home/pi/sendnotify.py")
        print('Sending email')
        pygame.mixer.music.play()
        print('Resetting System')
        count = 30
        while (count>0):
            print(count)
            count-=1
            time.sleep(1)
        print('System Armed')        

The camera OS commands no longer worked also, so I had to change that code also.

image

Here's the new code without any email notification.

import RPi.GPIO as GPIO
import time
import os
import pygame
from shutil import copyfile
IRinputPin = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(IRinputPin,GPIO.IN, pull_up_down=GPIO.PUD_UP)
count = 10
pygame.mixer.init()
pygame.mixer.music.load("/home/rpi5/Music/Witch.mp3")
print('System Reset')
while (count>0):
    print(count)
    count-=1
    time.sleep(1)
print('System Armed')
while True:
    print('Waiting for input')
    if (GPIO.input(IRinputPin)):
        time.sleep(1)
    else:
        print('IR input detected')
        pygame.mixer.music.play()
        os.system("rpicam-still -o /home/rpi5/capt.jpg")
        timestr = time.strftime("%Y%m%d-%H%M%S")
        filename2 = ('/home/rpi5/'+timestr+'.jpg')
        copyfile('/home/rpi5/capt.jpg', filename2)
        print('Saving Picture')
        print('Resetting System')
        count = 10
        while (count>0):
            print(count)
            count-=1
            time.sleep(1)
        print('System Armed')        

Here's the scarry witch laugh I found on the net.

We had a great Halloween!

image

image

Enjoy!

References

Take A Look At These Gorgeous And Scary Eyes!

GitHub - notisrac/FileToCArray: Coverts any file to a C style array. (It can also do image color format and size coversion)

Getting Started with Seeed Studio Round Display for XIAO | Seeed Studio Wiki

Getting Started with Seeed Studio XIAO nRF52840 Series | Seeed Studio Wiki

Category : project