Animated gifs from motion detection stills for HA

This isn’t an HA specific project but instead a project that displays the gifs in HA. I setup a python script to read a local FTP server where my camera’s deposit motion detection stills. The python script reads the stills and remove the duplicates and stitches the different ones into an animated gif. This allows easy viewing of motion detection as seen below. Please let me know if anyone cares for the script.

4 Likes

Looks good. Would you please share your script.

1 Like

Here is the script. I use a cronsjob to run this every 5 mins. I also use a ram disk for ftp and to do much of the work in but that is not necessary.

#! /usr/bin/env python
# inspired by: http://blog.iconfinder.com/detecting-duplicate-images-using-python/


from PIL import Image
from glob import glob
from hashlib import md5
import sys, shutil, os, argparse
import imageio as io
from PIL import ImageFile
import filecmp
import fnmatch
ImageFile.LOAD_TRUNCATED_IMAGES = True

###########move files to work

def gen_find(filepat,top):
    for path, dirlist, filelist in os.walk(top):
        for name in fnmatch.filter(filelist,filepat):
            yield os.path.join(path,name)

# Example use

if __name__ == '__main__':
    src = '/Users/username/.homeassistant/www/ram/ftp/192.168.2.38/' # input
    dst = '/Users/username/.homeassistant/www/ram/ftp/work/' # desired     location
    src1 = '/Users/username/.homeassistant/www/ram/ftp/ch1/' # input
    src2 = '/Users/username/.homeassistant/www/ram/ftp/ch2/' # input
    src3 = '/Users/username/.homeassistant/www/ram/ftp/ch3/' # input
    src4 = '/Users/username/.homeassistant/www/ram/ftp/ch4/' # input
    filesToMove = gen_find("*.jpg",src)
    for name in filesToMove:
        shutil.move(name, dst)
    filesToMove1 = gen_find("*.gif",src1)
    for name in filesToMove1:
        shutil.move(name, dst)
    filesToMove2 = gen_find("*.gif",src2)
    for name in filesToMove2:
        shutil.move(name, dst)
    filesToMove3 = gen_find("*.gif",src3)
    for name in filesToMove3:
        shutil.move(name, dst)
    filesToMove4 = gen_find("*.gif",src4)
    for name in filesToMove4:
        shutil.move(name, dst)


################ remove duplicates
os.chdir('/Users/username/.homeassistant/www/ram/ftp/work')  

DUP_FOLDER = 'duplicates'
KEEP_SUFIX = '_KEPT_'
DELETE_SUFIX = '_GONE_'
KEEP = '%s'+KEEP_SUFIX
DELETE = '%s'+DELETE_SUFIX

def dhash(image, hash_size = 8):
    # Grayscale and shrink the image in one step.
    image = image.convert('L').resize(
        (hash_size + 1, hash_size),
        Image.ANTIALIAS,
    )

    pixels = list(image.getdata())

    # Compare adjacent pixels.
    difference = []
    for row in range(hash_size):
        for col in range(hash_size):
            pixel_left = image.getpixel((col, row))
            pixel_right = image.getpixel((col + 1, row))
            difference.append(pixel_left > pixel_right)

    # Convert the binary array to a hexadecimal string.
    decimal_value = 0
    hex_string = []
    for index, value in enumerate(difference):
        if value:
            decimal_value += 2**(index % 8)
        if (index % 8) == 7:
            hex_string.append(hex(decimal_value)[2:].rjust(2, '0'))
            decimal_value = 0

    return ''.join(hex_string)

class ImgInfo:
    def __init__(self, name, size, cmp_func):
        self.name = name
        self.res = size
        self.cmp_func = cmp_func

    def __lt__(self, other):
        self_val = self.cmp_func(self)
        other_val = self.cmp_func(other)
        return self_val < other_val
    
    def __eq__(self, other):
        self_val = self.cmp_func(self)
        other_val = self.cmp_func(other)
        return self_val == other_val

class ImgHash:
    def __init__(self, val, info, sensitivity=5):
        self.val = val
        self.sensitivity = sensitivity
        self.img_info = info
        
    def __eq__(self, other):
        #Return the Hamming distance between equal-length sequences
        if len(self.val) != len(other.val):
            return false
        hamming_distance = sum(ch1 != ch2 for ch1, ch2 in zip(self.val, other.val))        
        return hamming_distance <= self.sensitivity
        

    def __hash__(self):
        return hash(self.val)
    
    def __str__(self):
        return self.val
    
def resolution(self):
    return self.res[0] * self.res[1]
    
def size(self):
    statinfo = os.stat(self.name)
    return statinfo.st_size

def compa(v1, v2, invert):
    return v1 > v2 if not invert else v2 > v1    


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Compare images base on perceptual similarity.')
    parser.add_argument('-c','--cmp', default=resolution,
                        help='compare images by function and keep higher (resolution, size [resolution])')
    parser.add_argument('-s','--sensitivity', default=0, type=int,
                        help='how similar images must be to be considered duplicates (0 - very similar, 5 - shomehow similar)')
    parser.add_argument('-i','--invert', action='store_true',
                        help='invert the compartison function (keep lower)')
    parser.add_argument('-d','--dry_run', action='store_true',
                        help='just print the pairs')
    parser.add_argument('-u','--undo', action='store_true',
                        help='put the moved files back')
    args = parser.parse_args()

    if args.sensitivity < 0 or args.sensitivity > 5:
        print('Invalid sensitivity value %d (0, 5)', args.sensitivity)
        sys.exit(1)
    
    if args.undo:
        images = glob(os.path.join(DUP_FOLDER, '*'))
        for img_path in images:            
            if KEEP_SUFIX in img_path:
                os.remove(img_path)
            if DELETE_SUFIX in img_path:
                file_name = img_path.split(DELETE_SUFIX)[-1]
                shutil.move(img_path, file_name)
                print('recovered %s' % file_name)
        try:
            os.rmdir(DUP_FOLDER)
        except OSError: pass
        sys.exit(0)
    
    img_list = []
    images = []
    
    types = ('*.jpg', '*.png', '*.gif', '*.jpeg')
    for files in types:
        images.extend(glob(files))
        images.extend(glob(files.upper()))
    print('Found %d files.'%len(images))
    
    count = 0
    duplicates = 0
    for img_path in images:
        sys.stdout.write("\r%d%%" % (count*100/len(images)))
        sys.stdout.flush()
        count += 1
        try:
            img = Image.open(img_path)
        
            comp = getattr(sys.modules[__name__], args.cmp) if type(args.cmp) is str else args.cmp
	
            ii1 = ImgInfo(img_path, img.size, comp)
            a = ImgHash(dhash(img), ii1, args.sensitivity)
            try:
                index = img_list.index(a)
            except ValueError:
                index = -1
            if index > -1: # hamming_distance comparison using specified sensitivity
                duplicates += 1
                if not os.path.exists(DUP_FOLDER) and not args.dry_run: os.mkdir(DUP_FOLDER)
                ii2 = img_list[index].img_info
                if not args.dry_run:
                    # prefix files with the same hash to make them a pair
                    prefix = md5((ii1.name + ii2.name).encode('utf-8')).hexdigest()[:5]
                    if compa(ii1, ii2, args.invert):
                        shutil.copy(ii1.name, os.path.join(DUP_FOLDER, KEEP % prefix + ii1.name))
                        shutil.move(ii2.name, os.path.join(DUP_FOLDER, DELETE % prefix + ii2.name))
                        img_list[index] = a # new file was kept
                    else:
                        shutil.move(ii1.name, os.path.join(DUP_FOLDER, DELETE % prefix + ii1.name))
                        shutil.copy(ii2.name, os.path.join(DUP_FOLDER, KEEP % prefix + ii2.name))
                    print("\r%s and %s are too similar" % (ii2.name, ii1.name))
            else:
                img_list.append(a)
        except IOError:
            print("\rerror processing files:", sys.exc_info())

    print("\rFound %d duplicates"%duplicates)
    
os.chdir('/Users/username/.homeassistant/www/ram/ftp/work')    



os.chdir('/Users/username/.homeassistant/www/ram/ftp/work')     
#if not os.path.exists(DUP_FOLDER): shutil.rmtree('duplicates')





###########convert new files before moving continues  os.system('mogrify    -format gif   -ordered-dither o4x4,8,8,4 +map  *.jpg')
os.chdir('/Users/username/.homeassistant/www/ram/ftp/work')
os.system('mogrify    -format gif    *.jpg')
filelistd = [ f for f in os.listdir("/Users/username/.homeassistant/www/ram/ftp/192.168.2.38") if f.endswith(".jpg") ]
for f in filelistd:
	os.remove(f)


###########move files back/Users/username/.homeassistant/www/ram/ftp/work/
    
    
if __name__ == '__main__':
    src = '/Users/username/.homeassistant/www/ram/ftp/work/' # input
    dst4 = '/Users/username/.homeassistant/www/ram/ftp//ch4' # desired     location

    filesToMove4 = gen_find("HCVR_ch4*.gif",src)
    for name in filesToMove4:
        shutil.move(name, dst4)

if __name__ == '__main__':
    dst3 = '/Users/username/.homeassistant/www/ram/ftp/ch3' # desired     location

    filesToMove3 = gen_find("HCVR_ch3*.gif",src)
    for name in filesToMove3:
        shutil.move(name, dst3)

if __name__ == '__main__':
    dst2 = '/Users/username/.homeassistant/www/ram/ftp/ch2' # desired     location

    filesToMove2 = gen_find("HCVR_ch2*.gif",src)
    for name in filesToMove2:
        shutil.move(name, dst2)

if __name__ == '__main__':
    dst1 = '/Users/username/.homeassistant/www/ram/ftp/ch1' # desired     location

    filesToMove1 = gen_find("HCVR_ch1*.gif",src)
    for name in filesToMove1:
        shutil.move(name, dst1)
        
        

c1 =  len([f for f in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch1')
                if os.path.isfile(os.path.join('/Users/username/.homeassistant/www/ram/ftp/ch1', f))])
c2 =  len([f for f in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch2')
                if os.path.isfile(os.path.join('/Users/username/.homeassistant/www/ram/ftp/ch2', f))])
c3 =  len([f for f in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch3')
                if os.path.isfile(os.path.join('/Users/username/.homeassistant/www/ram/ftp/ch3', f))])
c4 =  len([f for f in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch4')
                if os.path.isfile(os.path.join('/Users/username/.homeassistant/www/ram/ftp/ch4', f))])





file_names1 = sorted((fn for fn in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch1') if fn.startswith('HCVR_ch1_')))
file_names2 = sorted((fn for fn in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch2') if fn.startswith('HCVR_ch2_')))
file_names3 = sorted((fn for fn in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch3') if fn.startswith('HCVR_ch3_')))
file_names4 = sorted((fn for fn in os.listdir('/Users/username/.homeassistant/www/ram/ftp/ch4') if fn.startswith('HCVR_ch4_')))


###### make animated gif if  > 2 files

if c1 > 1:
    os.chdir('/Users/username/.homeassistant/www/ram/ftp/ch1')
    os.system('convert -delay 50 -loop 0 -dither None -colors 80 "/Users/username/.homeassistant/www/ram/ftp/ch1/*.*" -fuzz "40%" -layers OptimizeFrame "/Users/username/.homeassistant/www/ram/ftp/work/CAM1.gif"')
    filelist1 = [ f for f in os.listdir("/Users/username/.homeassistant/www/ram/ftp/ch1") if f.endswith(".gif") ]
    for f in filelist1:
        os.remove(f)
elif c2 > 1:
    os.chdir('/Users/username/.homeassistant/www/ram/ftp/ch2') 
    os.system('convert -delay 50 -loop 0 -dither None -colors 80 "/Users/username/.homeassistant/www/ram/ftp/ch2/*.*" -fuzz "40%" -layers OptimizeFrame "/Users/username/.homeassistant/www/ram/ftp/work/CAM2.gif"')
    filelist2 = [ f for f in os.listdir("/Users/username/.homeassistant/www/ram/ftp/ch2") if f.endswith(".gif") ]
    for f in filelist2:
        os.remove(f)
elif c3 > 1:
    os.chdir('/Users/username/.homeassistant/www/ram/ftp/ch3')  
    os.system('convert -delay 50 -loop 0 -dither None -colors 80 "/Users/username/.homeassistant/www/ram/ftp/ch3/*.*" -fuzz "40%" -layers OptimizeFrame "/Users/username/.homeassistant/www/ram/ftp/work/CAM3.gif"')
    filelist3 = [ f for f in os.listdir("/Users/username/.homeassistant/www/ram/ftp/ch3") if f.endswith(".gif") ]
    for f in filelist3:
        os.remove(f)
elif c4 > 1:
    os.chdir('/Users/username/.homeassistant/www/ram/ftp/ch4')  
    os.system('convert -delay 50 -loop 0 -dither None -colors 80 "/Users/username/.homeassistant/www/ram/ftp/ch4/*.*" -fuzz "40%" -layers OptimizeFrame "/Users/username/.homeassistant/www/ram/ftp/work/CAM4.gif"')
    filelist4 = [ f for f in os.listdir("/Users/username/.homeassistant/www/ram/ftp/ch4") if f.endswith(".gif") ]
    for f in filelist4:
        os.remove(f)

        
#####moved  animation to www

    
    
os.chdir('/Users/username/.homeassistant/www/ram/ftp/work')  
file1 = '/Users/username/.homeassistant/www/ram/ftp/work/CAM1.gif'
file2 = '/Users/username/.homeassistant/www/ram/ftp/work/CAM2.gif'
file3 = '/Users/username/.homeassistant/www/ram/ftp/work/CAM3.gif'
file4 = '/Users/username/.homeassistant/www/ram/ftp/work/CAM4.gif'


if os.path.isfile(file1):
    if os.path.getsize(file1) > 10 * 1024:
        os.remove("/Users/username/.homeassistant/www/ram/www/CAM1-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM1-5.gif", "/Users/username/.homeassistant/www/ram/www/CAM1-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM1-4.gif", "/Users/username/.homeassistant/www/ram/www/CAM1-5.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM1-3.gif", "/Users/username/.homeassistant/www/ram/www/CAM1-4.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM1-2.gif", "/Users/username/.homeassistant/www/ram/www/CAM1-3.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM1-1.gif", "/Users/username/.homeassistant/www/ram/www/CAM1-2.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM1.gif", "/Users/username/.homeassistant/www/ram/www/CAM1-1.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/ftp/work/CAM1.gif", "/Users/username/.homeassistant/www/ram/www/CAM1.gif")
#        os.remove("/Users/username/.homeassistant/www/ram/ftp/work/CAM1.gif")
#os.system('gifsicle --optimize --colors 32 --batch /Users/username/.homeassistant/www/ram/www/CAM1.gif')

if os.path.isfile(file2):
    if os.path.getsize(file2) > 10 * 1024:
        os.remove("/Users/username/.homeassistant/www/ram/www/CAM2-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM2-5.gif", "/Users/username/.homeassistant/www/ram/www/CAM2-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM2-4.gif", "/Users/username/.homeassistant/www/ram/www/CAM2-5.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM2-3.gif", "/Users/username/.homeassistant/www/ram/www/CAM2-4.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM2-2.gif", "/Users/username/.homeassistant/www/ram/www/CAM2-3.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM2-1.gif", "/Users/username/.homeassistant/www/ram/www/CAM2-2.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM2.gif", "/Users/username/.homeassistant/www/ram/www/CAM2-1.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/ftp/work/CAM2.gif", "/Users/username/.homeassistant/www/ram/www/CAM2.gif")
#        os.remove("/Users/username/.homeassistant/www/ram/ftp/work/CAM2.gif")
#os.system('gifsicle --optimize --colors 32 --batch /Users/username/.homeassistant/www/ram/www/CAM2.gif')
    
if os.path.isfile(file3):
    if os.path.getsize(file3) > 10 * 1024:
        os.remove("/Users/username/.homeassistant/www/ram/www/CAM3-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM3-5.gif", "/Users/username/.homeassistant/www/ram/www/CAM3-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM3-4.gif", "/Users/username/.homeassistant/www/ram/www/CAM3-5.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM3-3.gif", "/Users/username/.homeassistant/www/ram/www/CAM3-4.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM3-2.gif", "/Users/username/.homeassistant/www/ram/www/CAM3-3.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM3-1.gif", "/Users/username/.homeassistant/www/ram/www/CAM3-2.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM3.gif", "/Users/username/.homeassistant/www/ram/www/CAM3-1.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/ftp/work/CAM3.gif", "/Users/username/.homeassistant/www/ram/www/CAM3.gif")
#        os.remove("/Users/username/.homeassistant/www/ram/ftp/work/CAM3.gif")
#os.system('gifsicle --optimize --colors 32 --batch /Users/username/.homeassistant/www/ram/www/CAM3.gif')
    
if os.path.isfile(file4):
    if os.path.getsize(file4) > 10 * 1024:
        os.remove("/Users/username/.homeassistant/www/ram/www/CAM4-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM4-5.gif", "/Users/username/.homeassistant/www/ram/www/CAM4-6.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM4-4.gif", "/Users/username/.homeassistant/www/ram/www/CAM4-5.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM4-3.gif", "/Users/username/.homeassistant/www/ram/www/CAM4-4.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM4-2.gif", "/Users/username/.homeassistant/www/ram/www/CAM4-3.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM4-1.gif", "/Users/username/.homeassistant/www/ram/www/CAM4-2.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/www/CAM4.gif", "/Users/username/.homeassistant/www/ram/www/CAM4-1.gif")
        shutil.move("/Users/username/.homeassistant/www/ram/ftp/work/CAM4.gif", "/Users/username/.homeassistant/www/ram/www/CAM4.gif")


try:
    shutil.rmtree('/Users/username/.homeassistant/www/ram/ftp/work')
    os.mkdir('/Users/username/.homeassistant/www/ram/ftp/work') 
except OSError: pass
1 Like

Thank you. Much appreciated.

So this are not gif made from video, but from photo, right?

Right, my camera’s have a feature that takes snapshots when it see’s movement. I tried the video but it was too big of a file and didn’t add too much more than snapshots. I record all video so I can go back and get the video if need be.

Funny I was looking at some like this as well. I basically created the same thing but taking the jpg and creating a GIF so I can display in card in HA. (converts to png and then to gif, as I was having an issue with the jpeg file). It runs every 30 minutes currently. Be kind it’s my first go at this.
I used Appdaemon to do so. Below is the setup if you are interested. Below is the conifg for Appdaemon.

{
“disable_auto_token”: false,
“system_packages”: [
“libcurl”,
“zlib-dev”,
“libjpeg”,
“libwebp”,
“libjpeg-turbo”,
“tk”,
“tiff-dev”,
“openjpeg”,
“python3-dev”,
“curl-dev”,
“gcc”,
“g++”
],
“python_packages”: [
“imageio”,
“Image”
]
}

And then updated the apps.yaml file and created a save_gif.py file.
apps.yaml

save_gif:
module: save_gif
class: Savegif

save_gif.py

import appdaemon.plugins.hass.hassapi as hass
import os
import imageio
import shutil
import datetime
import glob
import time
from stat import S_ISREG, ST_CTIME, ST_MODE
from PIL import Image

class Savegif(hass.Hass):

def initialize(self):

  self.run_daily_c()

def run_daily_c(self):

   # Waits for 30 minutes
   time.sleep(1800)

   self.png_dir = '/config/www/deepstack_person_images/'
   self.png_dir1 = '/config/www/png/'
   self.images = []

   # Deletes old files from copied folder
   for file in os.scandir(self.png_dir1):
       if file.name.endswith(".PNG"):
            os.unlink(file)

    # Sorts by file name
    for self.file_name in sorted(os.listdir(self.png_dir)):
        
        # Copies jpg to new folder and converts to png
        if not self.file_name.startswith("."):
            self.file_path = os.path.join(self.png_dir, self.file_name)
            self.filename1 = os.path.splitext(self.file_name)[0] 
            self.file_path1 = os.path.join(self.png_dir1, self.filename1 + '.PNG')
            print (self.file_path)
            print (self.file_path1)
            self.im = Image.open(self.file_path)
            self.im.save(self.file_path1, "PNG")
            self.images.append(imageio.imread(self.file_path1,'.PNG'))
    
    # Creates gif file
    imageio.mimsave('/config/www/output.gif',self.images,fps=.6)

    # Deletes png files again
    for file in os.scandir(self.png_dir1):
        if file.name.endswith(".PNG"):
            os.unlink(file)
    while True:    
        self.initialize()