Package and refactor photocat

This commit is contained in:
Michael Soukup 2021-07-16 11:54:58 +02:00
parent 58cfd41f43
commit 68d679d6ce
7 changed files with 88 additions and 84 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
__pycache__ __pycache__
venv venv
yolov3
data/photos
.*

0
photocat/__init__.py Normal file
View File

View File

@ -1,4 +1,5 @@
import os import os
import datetime
from toolz import compose from toolz import compose
from toolz.curried import filter, map from toolz.curried import filter, map
@ -20,3 +21,9 @@ def list_images(folder):
map(lambda f: os.path.join(folder, f)), map(lambda f: os.path.join(folder, f)),
filter(lambda f: os.path.splitext(f)[-1].lower() in IMG_EXT) filter(lambda f: os.path.splitext(f)[-1].lower() in IMG_EXT)
)(files) )(files)
def last_modified(filename):
epoch = os.path.getmtime(filename)
return datetime.datetime.fromtimestamp(epoch)

View File

@ -1,30 +1,33 @@
from collections import namedtuple import datetime
from dataclasses import dataclass
from itertools import groupby from itertools import groupby
from toolz import curry, compose from toolz import curry, compose
from toolz.curried import map, filter from toolz.curried import map, filter
from photocat.photo import Photo
PhotoGroup = namedtuple( @dataclass(init = False)
'PhotoGroup', class PhotoGroup:
['name', 'datetimes', 'photos'] 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 @curry
def _group(key, photos): 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 [ return [
create_group(k, list(v)) PhotoGroup(list(v))
for k, v in groupby(sorted(photos, key=key), key=key) 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))

View File

@ -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()

29
photocat/main.py Normal file → Executable file
View File

@ -1,24 +1,22 @@
#!/usr/bin/env python3
#import PySimpleGUI as sg #import PySimpleGUI as sg
import PySimpleGUIQt as sg import PySimpleGUIQt as sg
import os import os
from toolz import compose from toolz import compose
from toolz.curried import map from toolz.curried import map
import fs from photocat import fs, photo, group
import image
import photo
import group
MAX_ROWS = 100 MAX_ROWS = 100
MAX_COLS = 5 MAX_COLS = 4
IMG_SIZE = (100, 100)
NA_FILE = os.path.join( NA_FILENAME = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'na.jpg' 'na.jpg'
) )
NA_IMG = image.to_bytes(image.read(NA_FILE, resize=IMG_SIZE)) NA_PHOTO = photo.Photo(NA_FILENAME)
def main(): def main():
@ -27,12 +25,12 @@ def main():
[sg.Listbox(values=[], enable_events=True, size=(40,20),key='GROUP LIST')] [sg.Listbox(values=[], enable_events=True, size=(40,20),key='GROUP LIST')]
] ]
image_view = [ 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) for i in range(MAX_ROWS)
] ]
group_view = [ group_view = [
[sg.Text('Group: ')], [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 = [[ layout = [[
@ -52,7 +50,7 @@ def main():
# Process input photos into groups # Process input photos into groups
groups = compose( groups = compose(
group.photos_by_month, group.photos_by_month,
map(photo.read_photo), map(lambda f: photo.Photo(f)),
fs.list_images fs.list_images
)(values['FOLDER']) )(values['FOLDER'])
window['GROUP LIST'].update(values=[g.name for g in groups]) window['GROUP LIST'].update(values=[g.name for g in groups])
@ -64,16 +62,13 @@ def main():
# Assert number of photos # Assert number of photos
n_photos = len(current_group.photos) n_photos = len(current_group.photos)
assert n_photos <= MAX_ROWS*MAX_COLS assert n_photos <= MAX_ROWS*MAX_COLS
# Reset image view # Update image view
for idx in range(MAX_ROWS*MAX_COLS): for idx in range(MAX_ROWS*MAX_COLS):
if idx < n_photos: 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: else:
window['PHOTO %d' % idx].update(visible=False) 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'): elif event.startswith('PHOTO'):
idx = int(event.split(' ')[-1]) idx = int(event.split(' ')[-1])
print("Selected photo %d" % idx) print("Selected photo %d" % idx)

View File

@ -1,32 +1,62 @@
import os
import datetime import datetime
from collections import namedtuple import io
from dateutil import parser from dataclasses import dataclass
from PIL import Image, ExifTags
from image import read_exif from photocat import fs
Photo = namedtuple( IMG_SIZE = (200, 200)
'Photo',
['filename', 'datetime', 'exif', 'features', 'selected'] EXIF_DATETIME_KEY = 'DateTime'
) EXIF_DATETIME_FORMAT = '%Y:%m:%d %H:%M:%S'
def _exif_dt(exif): def _exif_dt(exif):
try: try:
return parser.parse(exif['DateTimeOriginal']) return datetime.datetime.strptime(
exif[EXIF_DATETIME_KEY],
EXIF_DATETIME_FORMAT
)
except Exception: except Exception:
return None 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): def __init__(self, filename: str):
exif = read_exif(filename) self.filename = filename
print(filename, exif) img = Image.open(filename)
dt = _exif_dt(exif) or _last_modified_dt(filename)
features = [] # TODO exif = img.getexif()
return Photo(filename, dt, exif, features, True) 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()