Working image grid view.
This commit is contained in:
commit
58cfd41f43
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
venv
|
||||||
22
photocat/fs.py
Normal file
22
photocat/fs.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import os
|
||||||
|
from toolz import compose
|
||||||
|
from toolz.curried import filter, map
|
||||||
|
|
||||||
|
|
||||||
|
IMG_EXT = (
|
||||||
|
'.png',
|
||||||
|
'.jpeg',
|
||||||
|
'.jpg',
|
||||||
|
'.svg',
|
||||||
|
'.apng',
|
||||||
|
'.gif',
|
||||||
|
'.webp',
|
||||||
|
'.avif'
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_images(folder):
|
||||||
|
_, _, files = next(os.walk(folder))
|
||||||
|
return compose(
|
||||||
|
map(lambda f: os.path.join(folder, f)),
|
||||||
|
filter(lambda f: os.path.splitext(f)[-1].lower() in IMG_EXT)
|
||||||
|
)(files)
|
||||||
30
photocat/group.py
Normal file
30
photocat/group.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
from itertools import groupby
|
||||||
|
from toolz import curry, compose
|
||||||
|
from toolz.curried import map, filter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
PhotoGroup = namedtuple(
|
||||||
|
'PhotoGroup',
|
||||||
|
['name', 'datetimes', '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)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
photos_by_month = _group(lambda p: p.datetime.month)
|
||||||
|
|
||||||
|
|
||||||
34
photocat/image.py
Normal file
34
photocat/image.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
85
photocat/main.py
Normal file
85
photocat/main.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#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
|
||||||
|
|
||||||
|
|
||||||
|
MAX_ROWS = 100
|
||||||
|
MAX_COLS = 5
|
||||||
|
IMG_SIZE = (100, 100)
|
||||||
|
|
||||||
|
NA_FILE = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
'na.jpg'
|
||||||
|
)
|
||||||
|
NA_IMG = image.to_bytes(image.read(NA_FILE, resize=IMG_SIZE))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
group_select = [
|
||||||
|
[sg.Text('Input folder'), sg.In(size=(25,1), enable_events=True, key='FOLDER'), sg.FolderBrowse()],
|
||||||
|
[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)]
|
||||||
|
for i in range(MAX_ROWS)
|
||||||
|
]
|
||||||
|
group_view = [
|
||||||
|
[sg.Text('Group: ')],
|
||||||
|
[sg.Column(image_view, scrollable=True, size=(650, 700), element_justification='l')]
|
||||||
|
]
|
||||||
|
|
||||||
|
layout = [[
|
||||||
|
sg.Column(group_select, element_justification='c'),
|
||||||
|
sg.VSeperator(),
|
||||||
|
sg.Column(group_view, element_justification='c')
|
||||||
|
]]
|
||||||
|
window = sg.Window('photocat', layout, resizable=True)
|
||||||
|
|
||||||
|
groups = []
|
||||||
|
current_group = None
|
||||||
|
while True:
|
||||||
|
event, values = window.read()
|
||||||
|
if event in (sg.WIN_CLOSED, 'Exit'):
|
||||||
|
break
|
||||||
|
if event == 'FOLDER':
|
||||||
|
# Process input photos into groups
|
||||||
|
groups = compose(
|
||||||
|
group.photos_by_month,
|
||||||
|
map(photo.read_photo),
|
||||||
|
fs.list_images
|
||||||
|
)(values['FOLDER'])
|
||||||
|
window['GROUP LIST'].update(values=[g.name for g in groups])
|
||||||
|
elif event == 'GROUP LIST':
|
||||||
|
# Find photo group
|
||||||
|
group_name = values['GROUP LIST'][0]
|
||||||
|
print("Selected group: ", group_name)
|
||||||
|
current_group = next(g for g in groups if g.name == group_name)
|
||||||
|
# Assert number of photos
|
||||||
|
n_photos = len(current_group.photos)
|
||||||
|
assert n_photos <= MAX_ROWS*MAX_COLS
|
||||||
|
# Reset image view
|
||||||
|
for idx in range(MAX_ROWS*MAX_COLS):
|
||||||
|
if idx < n_photos:
|
||||||
|
window['PHOTO %d' % idx].update(data=NA_IMG, 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)
|
||||||
|
|
||||||
|
window.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
photocat/na.jpg
Normal file
BIN
photocat/na.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
32
photocat/photo.py
Normal file
32
photocat/photo.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from collections import namedtuple
|
||||||
|
from dateutil import parser
|
||||||
|
|
||||||
|
from image import read_exif
|
||||||
|
|
||||||
|
|
||||||
|
Photo = namedtuple(
|
||||||
|
'Photo',
|
||||||
|
['filename', 'datetime', 'exif', 'features', 'selected']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _exif_dt(exif):
|
||||||
|
try:
|
||||||
|
return parser.parse(exif['DateTimeOriginal'])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _last_modified_dt(filename):
|
||||||
|
epoch = os.path.getmtime(filename)
|
||||||
|
return datetime.datetime.fromtimestamp(epoch)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user