diff --git a/.gitignore b/.gitignore index 82adb58..149a660 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__ venv +yolov3 +data/photos +.* diff --git a/photocat/__init__.py b/photocat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/photocat/fs.py b/photocat/fs.py index 4eed20a..f9d246d 100644 --- a/photocat/fs.py +++ b/photocat/fs.py @@ -1,4 +1,5 @@ import os +import datetime from toolz import compose from toolz.curried import filter, map @@ -20,3 +21,9 @@ def list_images(folder): map(lambda f: os.path.join(folder, f)), filter(lambda f: os.path.splitext(f)[-1].lower() in IMG_EXT) )(files) + + +def last_modified(filename): + epoch = os.path.getmtime(filename) + return datetime.datetime.fromtimestamp(epoch) + diff --git a/photocat/group.py b/photocat/group.py index c2b75ff..186c381 100644 --- a/photocat/group.py +++ b/photocat/group.py @@ -1,30 +1,33 @@ -from collections import namedtuple +import datetime +from dataclasses import dataclass from itertools import groupby from toolz import curry, compose from toolz.curried import map, filter +from photocat.photo import Photo -PhotoGroup = namedtuple( - 'PhotoGroup', - ['name', 'datetimes', 'photos'] -) +@dataclass(init = False) +class PhotoGroup: + name: str + min_datetime: datetime.datetime + max_datetime: datetime.datetime + photos: list[Photo] + + def __init__(self, photos: list[Photo]): + self.min_datetime = min(photos, key=lambda p: p.datetime).datetime + self.max_datetime = max(photos, key=lambda p: p.datetime).datetime + self.name = str(self.min_datetime) + '-' + str(self.max_datetime) # TODO + self.photos = photos @curry def _group(key, photos): - def create_group(k, photos): - min_dt = min(photos, key=lambda p: p.datetime).datetime - max_dt = max(photos, key=lambda p: p.datetime).datetime - name = str(min_dt) + '-' + str(max_dt) # TODO - return PhotoGroup(name, (min_dt, max_dt), photos) - return [ - create_group(k, list(v)) - for k, v in groupby(sorted(photos, key=key), key=key) + PhotoGroup(list(v)) + for _, v in groupby(sorted(photos, key=key), key=key) ] -photos_by_month = _group(lambda p: p.datetime.month) - +photos_by_month = _group(lambda p: (p.datetime.year, p.datetime.month)) diff --git a/photocat/image.py b/photocat/image.py deleted file mode 100644 index 39456f7..0000000 --- a/photocat/image.py +++ /dev/null @@ -1,34 +0,0 @@ -import io -from PIL import Image, ExifTags - - -def read(filename, resize=None): - """Read and optionally resize an image.""" - img = Image.open(filename) - cur_width, cur_height = img.size - if resize: - new_width, new_height = resize - scale = min(new_height/cur_height, new_width/cur_width) - img = img.resize((int(cur_width*scale), int(cur_height*scale)), Image.ANTIALIAS) - return img - - -def read_exif(filename): - """Read EXIF data.""" - img = Image.open(filename) - exif = img.getexif() - if exif is None: - raise Exception("No EXIF data for image %s" % filename) - return { - ExifTags.TAGS[k]: v - for k, v in exif.items() - if k in ExifTags.TAGS - } - - -def to_bytes(img): - """Convert image to PNG format and return as byte-string object.""" - bio = io.BytesIO() - img.save(bio, format="PNG") - return bio.getvalue() - diff --git a/photocat/main.py b/photocat/main.py old mode 100644 new mode 100755 index a09e6e0..c2e05ab --- a/photocat/main.py +++ b/photocat/main.py @@ -1,24 +1,22 @@ +#!/usr/bin/env python3 + #import PySimpleGUI as sg import PySimpleGUIQt as sg import os from toolz import compose from toolz.curried import map -import fs -import image -import photo -import group +from photocat import fs, photo, group MAX_ROWS = 100 -MAX_COLS = 5 -IMG_SIZE = (100, 100) +MAX_COLS = 4 -NA_FILE = os.path.join( +NA_FILENAME = os.path.join( os.path.dirname(__file__), 'na.jpg' ) -NA_IMG = image.to_bytes(image.read(NA_FILE, resize=IMG_SIZE)) +NA_PHOTO = photo.Photo(NA_FILENAME) def main(): @@ -27,12 +25,12 @@ def main(): [sg.Listbox(values=[], enable_events=True, size=(40,20),key='GROUP LIST')] ] image_view = [ - [sg.Image(key='PHOTO %d' % (i*MAX_COLS+j), data=NA_IMG, visible=False, enable_events=True) for j in range(MAX_COLS)] + [sg.Image(key='PHOTO %d' % (i*MAX_COLS+j), data=NA_PHOTO.to_bytes(), visible=False, enable_events=True) for j in range(MAX_COLS)] for i in range(MAX_ROWS) ] group_view = [ [sg.Text('Group: ')], - [sg.Column(image_view, scrollable=True, size=(650, 700), element_justification='l')] + [sg.Column(image_view, scrollable=True, size=(900, 700), element_justification='l')] ] layout = [[ @@ -52,7 +50,7 @@ def main(): # Process input photos into groups groups = compose( group.photos_by_month, - map(photo.read_photo), + map(lambda f: photo.Photo(f)), fs.list_images )(values['FOLDER']) window['GROUP LIST'].update(values=[g.name for g in groups]) @@ -64,16 +62,13 @@ def main(): # Assert number of photos n_photos = len(current_group.photos) assert n_photos <= MAX_ROWS*MAX_COLS - # Reset image view + # Update image view for idx in range(MAX_ROWS*MAX_COLS): if idx < n_photos: - window['PHOTO %d' % idx].update(data=NA_IMG, visible=True) + p = current_group.photos[idx] + window['PHOTO %d' % idx].update(data=p.to_bytes(), visible=True) else: window['PHOTO %d' % idx].update(visible=False) - # Load and display images - for idx, p in enumerate(current_group.photos): - img_data = image.to_bytes(image.read(p.filename, resize=IMG_SIZE)) - window['PHOTO %d' % idx].update(data=img_data) elif event.startswith('PHOTO'): idx = int(event.split(' ')[-1]) print("Selected photo %d" % idx) diff --git a/photocat/photo.py b/photocat/photo.py index 89bc8bd..0d8fede 100644 --- a/photocat/photo.py +++ b/photocat/photo.py @@ -1,32 +1,62 @@ -import os import datetime -from collections import namedtuple -from dateutil import parser +import io +from dataclasses import dataclass +from PIL import Image, ExifTags -from image import read_exif +from photocat import fs -Photo = namedtuple( - 'Photo', - ['filename', 'datetime', 'exif', 'features', 'selected'] -) +IMG_SIZE = (200, 200) + +EXIF_DATETIME_KEY = 'DateTime' +EXIF_DATETIME_FORMAT = '%Y:%m:%d %H:%M:%S' def _exif_dt(exif): try: - return parser.parse(exif['DateTimeOriginal']) + return datetime.datetime.strptime( + exif[EXIF_DATETIME_KEY], + EXIF_DATETIME_FORMAT + ) except Exception: return None -def _last_modified_dt(filename): - epoch = os.path.getmtime(filename) - return datetime.datetime.fromtimestamp(epoch) +@dataclass(init = False) +class Photo: + filename: str + exif: dict + datetime: datetime.datetime + thumbnail: Image + features: list + selected: bool = True -def read_photo(filename): - exif = read_exif(filename) - print(filename, exif) - dt = _exif_dt(exif) or _last_modified_dt(filename) - features = [] # TODO - return Photo(filename, dt, exif, features, True) + def __init__(self, filename: str): + self.filename = filename + img = Image.open(filename) + + exif = img.getexif() + if exif is None: + raise Exception("No EXIF data for image %s" % filename) + self.exif = { + ExifTags.TAGS[k]: v + for k, v in exif.items() + if k in ExifTags.TAGS + } + + self.datetime = _exif_dt(self.exif) or fs.last_modified(filename) + + cur_width, cur_height = img.size + new_width, new_height = IMG_SIZE + scale = min(new_height/cur_height, new_width/cur_width) + self.thumbnail = img.resize((int(cur_width*scale), int(cur_height*scale)), Image.ANTIALIAS) + + self.features = [] # TODO + print("Loaded", filename, "at", self.datetime, "with exif", self.exif) + + def to_bytes(self) -> bytes: + """Convert image to PNG format and return as byte-string object.""" + bio = io.BytesIO() + self.thumbnail.save(bio, format="PNG") + return bio.getvalue()