Package and refactor photocat
This commit is contained in:
parent
58cfd41f43
commit
68d679d6ce
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
__pycache__
|
||||
venv
|
||||
yolov3
|
||||
data/photos
|
||||
.*
|
||||
|
||||
0
photocat/__init__.py
Normal file
0
photocat/__init__.py
Normal 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)
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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
29
photocat/main.py
Normal file → Executable 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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user