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.
Looks good. Would you please share your script.
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
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 Imageclass 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()