En esta ocasión vamos a ver como utilizar los metadatos EXIF para organizar imágenes y eliminar duplicados gracias al HASH.

Estoy haciendo limpieza digital, borrando archivos, apps antiguas, maquinas virtuales y sobre todo, organizando fotos y contenido multimedia.

El problema que arrastro desde hace años, supongo es común a la mayoria, son las fotos, tengo muchisimas y replicadas por discos duros, pendrives, tarjetas sd y las más antiguas en CD.

Tanta copia y dispositivo ocupa espacio físico y lo peor, es que no están organizadas y al final no encuentro alguna cuando las busco.

El siguiente código recorre todo el contenido de un directorio, analiza cada imagen y la copia a una nueva ubicación basandose en la fecha de creación o momento en el que se tomó la fotografía.

Para evitar duplicados el archivo de imagen se renombra con el HASH del mismo.

Si el código encuentra varias veces la misma imagen no se duplicará aunque el nombre sea distinto. Al calcular el hash y recuperar los datos exif, el nombre del archivo resultante y la nueva carpeta destino serán el mismo, por lo que podemos ejecutar el código sobre tantos directorios origen y copias como queramos sin volver a crear información duplicada.

# fx paths
import os
from pathlib import Path
import shutil
# fx imagen y exif
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
#fx hash
import hashlib
   
def get_exif(filename):
	try:
		exif = Image.open(filename)._getexif()
	
		if exif is not None:
			for key, value in exif.items():
				name = TAGS.get(key, key)
				exif[name] = exif.pop(key)
	
			if 'GPSInfo' in exif:
				for key in exif['GPSInfo'].keys():
					name = GPSTAGS.get(key,key)
					exif['GPSInfo'][name] = exif['GPSInfo'].pop(key)

		return exif
	    
	except Exception as e:
		print("Error: %s" % (e))
		return None
	except:
		print("Error desconocido")
		return None

def getmd5file(filename):
	try:
		hashmd5 = hashlib.md5()
		with open(filename, "rb") as f:
			for bloque in iter(lambda: f.read(4096), b""):
				hashmd5.update(bloque)
		return hashmd5.hexdigest()
	except Exception as e:
		print("Error: %s" % (e))
		return ""
	except:
		print("Error desconocido")
		return ""
        
def _newfilename(md5, datetimeimg, ext):
	stmp=datetimeimg.replace(" ","")
	stmp=stmp.replace(":","")
	#la vble extension debe incluir el "."
	return stmp + "_" + md5 + ext

def _newsubdir(datetimeimg):
	stmp=datetimeimg.replace(" ","")
	stmp=stmp.replace(":","")
	stmp=stmp[0:6]
	return stmp

def _createdir(newdir):
	try:
		if not os.path.exists(newdir):
		    os.makedirs(newdir)	
		return True
	except OSError as e:
		return False
	
def _copyfilerep(oldfile, newdir, newfile):
	snewpath = newdir + "/" + newfile
	if _createdir(newdir):
		print("_copyfilerep: " + oldfile + " --> " + snewpath)
		if not os.path.exists(snewpath):
			shutil.copy2(oldfile, snewpath)
	else:
		print("_copyfilerep ERROR: " + oldfile + " --> " + snewpath)

       
def _checkdir(spathdir, snewdir):
	nfiles=0
	nfilesexifs=0	
	for root, dirs,files in os.walk(spathdir):
		for file in [f for f in files if f.lower().endswith(('.jpg', '.jpeg'))]:
			nfiles+=1
			sfilepath=os.path.join(root, file)
			print(sfilepath)
			datetimeimg = "19000101000000"
			md5=getmd5file(sfilepath)
			print(md5)
			exif=get_exif(sfilepath)
			if exif is None:
				datetimeimg = "19020101000000"
			else:
				# si hay datos exif:
				nfilesexifs+=1
				if "DateTimeOriginal" in exif:
					datetimeimg = exif["DateTimeOriginal"]
					print(datetimeimg)

			snewfilename=_newfilename(md5, datetimeimg, ".jpg")
			snewsubdir=_newsubdir(datetimeimg)
			print(sfilepath + ">>" + snewdir + "/" + snewsubdir + "/" + snewfilename)
			_copyfilerep(sfilepath, snewdir + "/" + snewsubdir, snewfilename)
				
		print("total archivos: %d / %d" % (nfilesexifs, nfiles))


# MAIN
spathrep="/media/lubuntu/2106-0208/cmrep"
spathimgs="/home/lubuntu/Pictures/Imagenes"
_checkdir(spathimgs, spathrep)

print("end!")

La función que realiza toda la magia es _checkdir, el primer parámetro indica el directorio raiz origen de las imágenes y el segundo el repositorio destino.

Las funciones utilizadas no tienen mucho que comentar, recorrer directorios recursivamente, copiar ficheros, calcular hash, leer datos exif… creo que más o menos se entiende.

En breve veremos como utilizar las coordenadas GPS de la imagen para conocer dónde tomamos la fotografía y cómo guardar la información de las imágenes en bd con SQLite para hacer búsquedas y agrupaciones más complejas.

Como siempre, cualquier crítica, duda o propuesta ya sabeis.

Si no lo has hecho ya, puede que te interese leer estas entradas:

Saludos!