Skip to content

Commit

Permalink
Improve fastclass fcc (#30)
Browse files Browse the repository at this point in the history
* small refactor in args section

* fix missing bracket

* rename tk ui section root to parent for better readbility

* cleanup fc_clean - move file suffixes

* refactor some verbose lines to be more concise

* minor refactor of class digits

* some more refactoring

* change buttons in fcc to use ttk to restore button text in macos mojave

* small refactor to revert bad black formatting

* change os.path to pathlib and minor refactor in fcc

* A BIG rewrite of fc_clean. Apologies to anyone reading through the original code. Much cleaner and shorter design, also a nice GUI upgrade
  • Loading branch information
cwerner authored Apr 25, 2020
1 parent 3a32040 commit d39a4f9
Showing 1 changed file with 152 additions and 134 deletions.
286 changes: 152 additions & 134 deletions fastclass/fc_clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
# Christian Werner, 2018-10-23

import click
import glob
import itertools
from collections import deque
from functools import partial
import itertools as it
import os
from pathlib import Path
from PIL import ImageTk, Image
import tkinter as tk
from tkinter import ttk
import shutil

from .imageprocessing import image_pad
Expand All @@ -31,197 +34,211 @@
The counter in the titlebar gives number of classified images\r
vs the total number in the input folder.\r
In the output csv file 1,2 depcit class assignments/ ratings,
In the output csv file 1,2 indicate class assignments/ ratings,
-1 indicates files marked for deletion (if not excluded with -d)."""

# supported suffixes
suffixes = ["jpg", "jpeg", "png", "tif", "tiff"]
suffixes += [x.upper() for x in suffixes]

digits = "123456789"


class Item(object):
def __init__(self, image_path, size):
self.image_path = image_path
self.label = None
self.size = size

def __repr__(self):
return f"Item <{self.image_path} [{self.label if self.label else None}]>"

def show(self):
return ImageTk.PhotoImage(image_pad(self.image_path, self.size))


class ItemList(object):
def __init__(self, items=[], size=(299, 299)):
self._data = deque([Item(i, size) for i in items])
self._initial = True

def __iter__(self):
return iter(self._data)

def __len__(self):
return len(self._data)

def __repr__(self):
return ", ".join([str(x) for x in self._data])

def forward(self):
self._data.rotate(-1)

def backward(self):
self._data.rotate(1)

@property
def current(self):
if len(self._data) > 0:
return self._data[0]

@property
def labels(self):
return [x.label for x in self._data]


class AppTk(tk.Frame):
def __init__(self, *args, **kwargs):
def __init__(self, parent, **kwargs):

INFOLDER = kwargs["infolder"]
INFOLDER = Path(kwargs["infolder"])
OUTFOLDER = kwargs["outfolder"]

if OUTFOLDER:
pass
if OUTFOLDER is None:
OUTFOLDER = INFOLDER.parent / (INFOLDER.name + ".clean")
OUTFOLDER.mkdir(exist_ok=True)
else:
OUTFOLDER = INFOLDER + ".clean"
os.makedirs(OUTFOLDER, exist_ok=True)
OUTFOLDER = Path(OUTFOLDER)

NOCOPY = kwargs["nocopy"]

for e in ["infolder", "outfolder", "nocopy"]:
kwargs.pop(e)
# remove these kwargs before passing them into tk frame
[kwargs.pop(e) for e in ["infolder", "outfolder", "nocopy"]]

tk.Frame.__init__(self, *args, **kwargs)
tk.Frame.__init__(self, parent, **kwargs)
self.parent = parent

# bind keys
args[0].bind("<Key>", self.callback)
self.root = args[0]

# store classification
self._class = {f"c{c}": set() for c in range(1, 10)}
self._delete = set()

# config settings
suffixes = ["jpg", "jpeg", "png", "tif", "tiff"]
suffixes += [x.upper() for x in suffixes]
files = list(
itertools.chain(*[glob.glob(f"{INFOLDER}/*.{x}") for x in suffixes])
)
self.parent.bind("<Key>", self.callback)

files = list(it.chain(*[INFOLDER.glob(f"*.{x}") for x in suffixes]))

self.filelist = sorted(set(files))
if len(self.filelist) == 0:
print("No files in infolder.")
exit(-1)

self.images = ItemList(items=sorted(set(files)), size=(299, 299))

self.outfolder = OUTFOLDER
self.infolder = INFOLDER
self.nocopy = NOCOPY

self._classified = 0
self._index = -1

self.size = (299, 299)

# basic setup
self.setup()

# raise window to top
self.root.lift()
self.root.attributes("-topmost", True)
self.parent.lift()
self.parent.attributes("-topmost", True)

# show first image
self.display_next()
self.display()

@property
def cur_file(self):
return self.images.current

@property
def total(self):
return len(self.filelist)
def no_classified(self):
return sum(1 for x in self.images.labels if x is not None)

@property
def classified(self):
cnt = 0
for c in "123456789":
if self.filelist[self._index] in self._class[f"c{c}"]:
cnt += len(self._class[f"c{c}"])
if self.filelist[self._index] in self._delete:
cnt += len(self._delete)
return cnt
def no_total(self):
return len(self.images)

@property
def title(self):
def get_class():
for c in "123456789":
if self.filelist[self._index] in self._class[f"c{c}"]:
return f"[ {c} ] "
if self.filelist[self._index] in self._delete:
return "[ X ] "
return "[ ] "

return (
os.path.basename(self.filelist[self._index])
+ " - "
+ get_class()
+ f" ({self.classified}/{self.total})"
label = self.images.current.label
if label is None:
label = " "
return f"[ {label} ] "

stats = f"{self.no_classified}/{self.no_total}"
label = (
f"FastClass :: {self.cur_file.image_path.name} - {get_class()} ({stats})"
)
return label

def print_titlebar(self):
self.root.title(self.title)
self.parent.title(self.title)

def button_callback(self, button):
self.images.current.label = button
self.display_next()

def callback(self, event=None):
def button_action(char):
self._class[f"c{char}"].add(self.filelist[self._index])
self.images.current.label = char
self.display_next()

if event.keysym in "123456789":
button_action(event.keysym)
elif event.keysym == "space": #'<space>':
e = event.keysym
if e in digits + "d":
button_action(e)
elif e == "space": #'<space>':
button_action("1")
elif event.keysym == "d":
self._delete.add(self.filelist[self._index])
self.display_next()
elif event.keysym == "Left": #'<Left>':
elif e == "Left": #'<Left>':
self.display_prev()
elif event.keysym == "Right": #'<Right>':
elif e == "Right": #'<Right>':
self.display_next()
elif event.keysym == "x":

# write report file
rows_all = []
rows_clean = []
for f in self.filelist:
row = (f, "?")
for c in "123456789":
if f in self._class[f"c{c}"]:
row = (f, c)
if f in self._delete:
row = (f, "D")
else:
rows_clean.append(row)
rows_all.append(row)

with open(
os.path.join(self.infolder.replace(" ", "_") + "_report_all.csv"), "w"
) as f:
f.write("file;rank\n")
for row in rows_all:
f.write(";".join(row) + "\n")
elif e == "x":
self.save_and_exit()
else:
pass

with open(
os.path.join(self.infolder.replace(" ", "_") + "_report_clean.csv"), "w"
) as f:
def save_and_exit(self):
# write report file
rows_all, rows_clean = [], []
for f in self.images:
row = (f.image_path, f.label if f.label else "?")

if f.label is not "d":
rows_clean.append(row)
rows_all.append(row)

for ftype, rows in zip(["all", "clean"], [rows_all, rows_clean]):
foutname = Path(
str(self.infolder).replace(" ", "_") + f"_report_{ftype}.csv"
)
with open(foutname, "w") as f:
f.write("file;rank\n")
for row in rows_clean:
f.write(";".join(row) + "\n")
for row in sorted(rows, key=lambda x: x[0]):
f.write(";".join([str(x) for x in row]) + "\n")

if not self.nocopy:
for r in rows_clean:
shutil.copy(r[0], self.outfolder)
if not self.nocopy:
for r in rows_clean:
shutil.copy(r[0], self.outfolder)

self.root.destroy()
else:
pass
self.parent.destroy()

def setup(self):
self.Label = tk.Label(self)
self.Label.grid(row=0, column=0, columnspan=6, rowspan=6) # , sticky=tk.N+tk.S)
self.Button = tk.Button(self, text="Prev", command=self.display_prev)
self.Button.grid(row=5, column=7, sticky=tk.S)
self.Button = tk.Button(self, text="Next", command=self.display_next)
self.Button.grid(row=5, column=8, sticky=tk.S)
self.Canvas = tk.Label(self)
self.Canvas.grid(row=0, column=0, columnspan=6, rowspan=6)
ttk.Button(self, text="Prev", command=self.display_prev).grid(row=4, column=6)
ttk.Button(self, text="Next", command=self.display_next).grid(row=4, column=7)
ttk.Button(self, text="Save & Exit", command=self.save_and_exit).grid(
row=5, column=6, columnspan=2
)

def display_next(self):
self.lfdata = ttk.Labelframe(self, padding=(2, 2, 4, 4), text="Selection")
self.lfdata.grid(row=0, column=6, columnspan=2, sticky="ne")
for i, item in enumerate(digits + "d"):
ttk.Button(
self.lfdata, text=item, command=partial(self.button_callback, item)
).grid(in_=self.lfdata, column=6 + i % 2, row=i // 2, sticky="w")

def display(self):
photoimage = self.images.current.show()
self.Canvas.config(image=photoimage)
self.Canvas.image = photoimage
self.print_titlebar()
self._index += 1
try:
f = self.filelist[self._index]
except IndexError:
self._index = -1 # go back to the beginning of the list.
self.display_next()
return

padded_im = image_pad(f, self.size)

photoimage = ImageTk.PhotoImage(padded_im)
self.Label.config(image=photoimage)
self.Label.image = photoimage
self.print_titlebar()
def display_next(self):
self.images.forward()
self.display()

def display_prev(self):

self._index -= 1
try:
f = self.filelist[self._index]
except IndexError:
self._index = -1 # go back to the beginning of the list.
self.display_next()
return

padded_im = image_pad(f, self.size)

photoimage = ImageTk.PhotoImage(padded_im)
self.Label.config(image=photoimage)
self.Label.image = photoimage
self.print_titlebar()
self.images.backward()
self.display()


def main(INFOLDER, OUTFOLDER, nocopy):
Expand All @@ -230,7 +247,8 @@ def main(INFOLDER, OUTFOLDER, nocopy):

app = AppTk(root, infolder=INFOLDER, outfolder=OUTFOLDER, nocopy=nocopy)

app.grid(row=0, column=0)
app.grid(row=0, column=0, columnspan=8, rowspan=6)
app.configure(background="gray90")

# start event loop
root.lift()
Expand Down

0 comments on commit d39a4f9

Please sign in to comment.