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__
venv
yolov3
data/photos
.*

0
photocat/__init__.py Normal file
View File

View File

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

View File

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

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

View File

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