#!/usr/bin/python3 import sys, re, os, time import sqlite3 import tkinter as tki from tkinter import ttk import PIL from PIL import ImageTk, Image, ImageOps usageInfo = f""" Usage: {sys.argv[0]} Provides a GUI for reviewing images. Looks in a for-review directory for images named 'eolId1 contentId1.ext1', and, for each EOL ID, enables the user to choose an image to keep, or reject all. Also provides image rotation. Chosen images are placed in another directory, and rejected ones are deleted. """ if len(sys.argv) > 1: print(usageInfo, file=sys.stderr) sys.exit(1) imgDir = "imgsForReview/" outDir = "imgs/" extraInfoDbCon = sqlite3.connect("../data.db") extraInfoDbCur = extraInfoDbCon.cursor() def getExtraInfo(eolId): global extraInfoDbCur query = "SELECT names.alt_name FROM" \ " names INNER JOIN eol_ids ON eol_ids.name = names.name" \ " WHERE id = ? and pref_alt = 1" row = extraInfoDbCur.execute(query, (eolId,)).fetchone() if row != None: return f"Reviewing EOL ID {eolId}, aka \"{row[0]}\"" else: return f"Reviewing EOL ID {eolId}" IMG_DISPLAY_SZ = 400 MAX_IMGS_PER_ID = 3 IMG_BG_COLOR = (88, 28, 135) PLACEHOLDER_IMG = Image.new("RGB", (IMG_DISPLAY_SZ, IMG_DISPLAY_SZ), IMG_BG_COLOR) print("Checking output directory") if not os.path.exists(outDir): os.mkdir(outDir) print("Getting input image list") imgList = os.listdir(imgDir) imgList.sort(key=lambda s: int(s.split(" ")[0])) if len(imgList) == 0: print("No input images found") sys.exit(0) class EolImgReviewer: " Provides the GUI for reviewing images " def __init__(self, root, imgList): self.root = root root.title("EOL Image Reviewer") # Setup main frame mainFrame = ttk.Frame(root, padding="5 5 5 5") mainFrame.grid(column=0, row=0, sticky=(tki.N, tki.W, tki.E, tki.S)) root.columnconfigure(0, weight=1) root.rowconfigure(0, weight=1) # Set up images-to-be-reviewed frames self.imgs = [PLACEHOLDER_IMG] * MAX_IMGS_PER_ID # Stored as fields for use in rotation self.photoImgs = list(map(lambda img: ImageTk.PhotoImage(img), self.imgs)) # Image objects usable by tkinter # These need a persistent reference for some reason (doesn't display otherwise) self.labels = [] for i in range(MAX_IMGS_PER_ID): frame = ttk.Frame(mainFrame, width=IMG_DISPLAY_SZ, height=IMG_DISPLAY_SZ) frame.grid(column=i, row=0) label = ttk.Label(frame, image=self.photoImgs[i]) label.grid(column=0, row=0) self.labels.append(label) # Add padding for child in mainFrame.winfo_children(): child.grid_configure(padx=5, pady=5) # Add keyboard bindings root.bind("", self.quit) root.bind("", lambda evt: self.accept(0)) root.bind("", lambda evt: self.accept(1)) root.bind("", lambda evt: self.accept(2)) root.bind("", lambda evt: self.reject()) root.bind("", lambda evt: self.rotate(0)) root.bind("", lambda evt: self.rotate(1)) root.bind("", lambda evt: self.rotate(2)) root.bind("", lambda evt: self.rotate(0, True)) root.bind("", lambda evt: self.rotate(1, True)) root.bind("", lambda evt: self.rotate(2, True)) # Initialise images to review self.imgList = imgList self.imgListIdx = 0 self.nextEolId = 0 self.nextImgNames = [] self.rotations = [] self.getNextImgs() # For displaying extra info self.numReviewed = 0 self.startTime = time.time() def getNextImgs(self): " Updates display with new images to review, or ends program " # Gather names of next images to review for i in range(MAX_IMGS_PER_ID): if self.imgListIdx == len(self.imgList): if i == 0: self.quit() return break imgName = self.imgList[self.imgListIdx] eolId = int(re.match(r"(\d+) (\d+)", imgName).group(1)) if i == 0: self.nextEolId = eolId self.nextImgNames = [imgName] self.rotations = [0] else: if self.nextEolId != eolId: break self.nextImgNames.append(imgName) self.rotations.append(0) self.imgListIdx += 1 # Update displayed images idx = 0 while idx < MAX_IMGS_PER_ID: if idx < len(self.nextImgNames): try: img = Image.open(imgDir + self.nextImgNames[idx]) img = ImageOps.exif_transpose(img) except PIL.UnidentifiedImageError: os.remove(imgDir + self.nextImgNames[idx]) del self.nextImgNames[idx] del self.rotations[idx] continue self.imgs[idx] = self.resizeImgForDisplay(img) else: self.imgs[idx] = PLACEHOLDER_IMG self.photoImgs[idx] = ImageTk.PhotoImage(self.imgs[idx]) self.labels[idx].config(image=self.photoImgs[idx]) idx += 1 # Restart if all image files non-recognisable if len(self.nextImgNames) == 0: self.getNextImgs() return # Update title firstImgIdx = self.imgListIdx - len(self.nextImgNames) + 1 lastImgIdx = self.imgListIdx title = getExtraInfo(self.nextEolId) title += f" (imgs {firstImgIdx} to {lastImgIdx} out of {len(self.imgList)})" self.root.title(title) def accept(self, imgIdx): " React to a user selecting an image " if imgIdx >= len(self.nextImgNames): print("Invalid selection") return for i in range(len(self.nextImgNames)): inFile = imgDir + self.nextImgNames[i] if i == imgIdx: # Move accepted image, rotating if needed outFile = outDir + self.nextImgNames[i] img = Image.open(inFile) img = ImageOps.exif_transpose(img) if self.rotations[i] != 0: img = img.rotate(self.rotations[i], expand=True) img.save(outFile) os.remove(inFile) else: # Delete non-accepted image os.remove(inFile) self.numReviewed += 1 self.getNextImgs() def reject(self): " React to a user rejecting all images of a set " for i in range(len(self.nextImgNames)): os.remove(imgDir + self.nextImgNames[i]) self.numReviewed += 1 self.getNextImgs() def rotate(self, imgIdx, anticlockwise = False): " Respond to a user rotating an image " deg = -90 if not anticlockwise else 90 self.imgs[imgIdx] = self.imgs[imgIdx].rotate(deg) self.photoImgs[imgIdx] = ImageTk.PhotoImage(self.imgs[imgIdx]) self.labels[imgIdx].config(image=self.photoImgs[imgIdx]) self.rotations[imgIdx] = (self.rotations[imgIdx] + deg) % 360 def quit(self, e = None): global extraInfoDbCon print(f"Number reviewed: {self.numReviewed}") timeElapsed = time.time() - self.startTime print(f"Time elapsed: {timeElapsed:.2f} seconds") if self.numReviewed > 0: print(f"Avg time per review: {timeElapsed/self.numReviewed:.2f} seconds") extraInfoDbCon.close() self.root.destroy() def resizeImgForDisplay(self, img): " Returns a copy of an image, shrunk to fit in it's frame (keeps aspect ratio), and with a background " if max(img.width, img.height) > IMG_DISPLAY_SZ: if (img.width > img.height): newHeight = int(img.height * IMG_DISPLAY_SZ/img.width) img = img.resize((IMG_DISPLAY_SZ, newHeight)) else: newWidth = int(img.width * IMG_DISPLAY_SZ / img.height) img = img.resize((newWidth, IMG_DISPLAY_SZ)) bgImg = PLACEHOLDER_IMG.copy() bgImg.paste(img, box=( int((IMG_DISPLAY_SZ - img.width) / 2), int((IMG_DISPLAY_SZ - img.height) / 2))) return bgImg # Create GUI and defer control print("Starting GUI") root = tki.Tk() EolImgReviewer(root, imgList) root.mainloop()