Sorting your photographs can be very time consuming. I made this script to make it a little less so.
The script will show your photos with an added histogram of the exposure. Pure whites (#FFFFFF) will be marked red and pure blacks (#000000) will be marked blue.
Each image you can reject, approve or skip sorting by pressing the left, right or down arrows respectively.
Photographs that were rejected will be moved to a folder named `n' in the current directory, photographs that were appoved will be moved to a folder named `y'.
This Python3 script depends on an external library, Pillow, and makes use of tkinter, which both have to be installed before you can use this script. Tkinter is part of the standard python installation, but is excluded from the python package in some linux distributions so you may need to install it separately.
On Ubuntu tkinter can be installed by using the following command:
sudo apt-get install python3-tk
Pillow can be installed by using:
sudo apt-get install python3-pil python3-pil.imagetk
Here's the script:
#!/usr/bin/env python3 # # Copyright © 2015 Hans Alves <halves@localhost> # This work is free. You can redistribute it and/or modify it under the # terms of the Do What The Fuck You Want To Public License, Version 2, # as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. # import argparse import glob import os import os.path import random import shutil import subprocess import tkinter # PIL or Pillow, preferrably Pillow as PIL is no longer maintained # see https://pypi.python.org/pypi/Pillow/3.0.0 import PIL import PIL.ExifTags import PIL.Image import PIL.ImageDraw import PIL.ImageTk description='''photosort.py Easily sort photos. photosort will show you your photographs with their histogram and pure whites and blacks marked as red and blue respectively. For each photograph press <Left> to reject the photo, press <Right> to approve the photo or press <Down> to skip the photo. Photos that are approved are moved to a folder named `y', photos that are rejected are moved to a folder name `n'. Press <Space> to show the photo without histogram or marked colours. If no list of photos is provided photosort will sort all files with the extensions `.JPG', `.jpg', `.JPEG' and `.jpeg'. ''' def mkdir(name): if not os.path.exists(name): os.mkdir(name) class Application(tkinter.Frame): def __init__(self, master, photos): tkinter.Frame.__init__(self, master) self.master = master self.photos = photos self.path = None self.tkimg = None self.createwidgets() self.pack() self.master.update() self.load() self.status = 1 self.master.bind('<Escape>', self.exit) self.master.bind('<Left>', self.reject) self.master.bind('<Right>', self.approve) self.master.bind('<Down>', self.next) self.master.bind('<space>', self.swap) def createwidgets(self): self.panel = tkinter.Label(self.master, image=self.tkimg) #The Pack geometry manager packs widgets in rows or columns. self.panel.pack(side="top", fill="both", expand="yes") def load(self): self.status = -1 if not self.photos: self.master.destroy() return self.path = self.photos.pop() print('Loading', self.path) self.master.title('photosort | ' + self.path) img = PIL.Image.open(self.path) exif=dict((PIL.ExifTags.TAGS[k], v) for k, v in img._getexif().items() if k in PIL.ExifTags.TAGS) orientation = exif.get('Orientation', 1) if orientation == 3: img = img.rotate(180) elif orientation == 6: img = img.rotate(-90, expand=True) elif orientation == 8: img = img.rotate(90, expand=True) w, h = img.size print('orig size', w, h) maxw = self.panel.winfo_width() maxh = self.panel.winfo_height() print('max size', maxw, maxh) if w > maxw: h *= maxw / w w *= maxw / w if h > maxh: w *= maxh / h h *= maxh / h print('scale to', w, h) self.img = img.resize((int(w), int(h))) # Creates a Tkinter-compatible photo image, which can be used # everywhere Tkinter expects an image object. self.tkimg = PIL.ImageTk.PhotoImage(self.img) self.panel.configure(image=self.tkimg) self.calc() def calc(self): datas = self.img.getdata() new = [] for pix in datas: if pix == (255, 255, 255): # make pure whites red new += (255, 0, 0) elif pix == (0, 0, 0): # and pure black blue new += (0, 0, 255) else: # leave other colors new += pix self.imgb = PIL.Image.frombuffer("RGB", self.img.size, bytes(new), "raw", "RGB", 0, 1) g = self.img.convert(mode='L') # convert to greyscale hist = g.histogram() # get the histogram from the greyscale image # draw the histogram on the image w, h = self.imgb.size draw = PIL.ImageDraw.Draw(self.imgb) draw.line((10, h - 10, 265, h - 10), fill=(0, 0, 0)) draw.line((10, h - 9, 265, h - 9), fill=(255, 255, 255)) draw.line((9, h - 110, 9, h - 10), fill=(0, 0, 0)) draw.line((8, h - 110, 8, h - 10), fill=(255, 255, 255)) m = max(hist) print('histogram max value', m) for i in range(len(hist)): draw.line((10 + i, h - 10 - int(hist[i] / m * 100), 10 + i, h - 10), fill=(0, 0, 0)) draw.point((10 + i, h - 11 - int(hist[i] / m * 100)), fill=(255, 255, 255)) del draw self.tkimgb = PIL.ImageTk.PhotoImage(self.imgb) self.status = 2 self.panel.configure(image=self.tkimgb) def exit(self, *args): self.master.destroy() def approve(self, *args): shutil.move(self.path, 'y') print('move', self.path, 'to y') self.load() def reject(self, *args): shutil.move(self.path, 'n') print('move', self.path, 'to n') self.load() def next(self, *args): self.load() def swap(self, *args): if self.status == 1: self.panel.configure(image=self.tkimgb) self.status = 2 elif self.status == 2: self.panel.configure(image=self.tkimg) self.status = 1 def main(): parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter,) mutex = parser.add_mutually_exclusive_group(required=False) mutex.add_argument('-r', '--random', dest='random', action='store_true', default=False, help='Show the photos in random order.') mutex.add_argument('-o', '--ordered', dest='ordered', action='store_true', default=False, help='Show the photos ordered by filename.') parser.add_argument('photos', nargs='*', help='The photos to sort') args = parser.parse_args() mkdir('y') mkdir('n') if args.photos: photos = args.photos else: photos = (glob.glob('*.JPG') + glob.glob('*.jpg') + glob.glob('*.JPEG') + glob.glob('*.jpeg')) if args.random: random.shuffle(photos) if args.ordered: photos.sort(reverse=True) root = tkinter.Tk() root.title('photosort') root.geometry("1000x1000") root.configure(background='grey') app = Application(root, photos) app.master.mainloop() if __name__ == '__main__': main()