diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0cd082 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Photo Ranking With Python + +![Screenshot](screenshot.jpg) + +## What is this? + +This is a tool that uses the [`Elo Ranking System`](http://en.wikipedia.org/wiki/Elo_rating_system) written in Python using: + +1. [Matplotlib](https://matplotlib.org/) +2. [Numpy](https://www.numpy.org/) +3. [Pillow](https://python-pillow.org/) +4. [exifread](https://pypi.python.org/pypi/ExifRead) + +### Features: + +- Auto image rotation that the camera recored in the EXIF meta data +- Persistent state from execution to execution so you can pickup where you left off +- New photos that are added to the photo dir after initial ranking are picked up + +## Install dependencies + +Use your system's package manager to install Matplotlib & Numpy if you don't +already have them installed. + +`$ pip install Matplotlib [--user]` <---------------------This should also install [Numpy](https://www.numpy.org/) + +Next, you can use pip to install the EXIF image reader package [exifread](https://pypi.python.org/pypi/ExifRead). + +`$ pip install exifread [--user]` - The --user flag is optional and it depends on your environment. + +> Note for Windows Users - You may need to install [Pillow](https://python-pillow.org/) for jpg support. +> `$ pip install Pillow [--user]` + +## How to rank photos + +Once you have to dependencies installed, run `rank_photos.py` on the command +line passing it the directory of photos. + +``` + $ ./rank_photos.py -h + + usage: rank_photos.py [-h] [-r N_ROUNDS] [-f FIGSIZE FIGSIZE] photo_dir + + Uses the Elo ranking algorithm to sort your images by rank. The program globs + for .jpg images to present to you in random order, then you select the better + photo. After n-rounds, the results are reported. + + positional arguments: + photo_dir The photo directory to scan for .jpg images + + optional arguments: + -h, --help show this help message and exit + -r N_ROUNDS, --n-rounds N_ROUNDS + Specifies the number of rounds to pass through the + photo set (3) + -f FIGSIZE FIGSIZE, --figsize FIGSIZE FIGSIZE + Specifies width and height of the Matplotlib figsize + (20, 12) +``` + +For example, iterate over all photos three times: + +```bash +$ ./rank_photos.py -r 3 ~/Desktop/example/ +``` + +After the number of rounds complete, **`ranked.txt`** is written into the photo dir. + +## Ranking work is cached + +After completing N rounds of ranking, a file called `ranking_table.json` is +written into the photo dir. The next time `rank_photos.py` is executed with +the photo dir, this table is read in and ranking can continue where you left +off. + +You can also add new photos the the directory and they will get added to the +ranked list even though they weren't included previously. + +## Example + +Suppose there is a dir containing some photos: + +```bash +$ ls -1 ~/Desktop/example/ + + 20160102_164732.jpg + 20160109_151557.jpg + 20160109_151607.jpg + 20160109_152318.jpg + 20160109_152400.jpg + 20160109_152414.jpg + 20160109_153443.jpg +``` + +The photos haven't been ranked yet. Let's rank them in 1 round, shall we? + +
$ ./rank_photos.py -r 1 ~/Desktop/example/
+ +Once the number of rounds is complete, the ranked list is dumped to the console: + +```bash + Final Ranking: + Rank Score Matches Win % Filename + 1 1433 2 100.00 20160109_152414.jpg + 2 1414 3 66.67 20160109_151557.jpg + 3 1401 2 50.00 20160109_153443.jpg + 4 1400 2 50.00 20160102_164732.jpg + 5 1387 3 33.33 20160109_151607.jpg + 6 1383 3 33.33 20160109_152318.jpg + 7 1382 3 33.33 20160109_152400.jpg +``` + +The ranked list is also written to the file **`ranked.txt`**. The raw data is +cached to the file **`ranking_table.json`**: + +```bash + + $ cat ~/Desktop/example/ranking_table.json + + { + "photos" : [ + { + "matches" : 2, + "wins" : 2, + "score" : 1432.736306793522, + "filename" : "20160109_152414.jpg" + }, + { + "matches" : 3, + "wins" : 2, + "score" : 1413.760501639972, + "filename" : "20160109_151557.jpg" + }, + { + "matches" : 2, + "wins" : 1, + "score" : 1400.736306793522, + "filename" : "20160109_153443.jpg" + }, + { + "matches" : 2, + "wins" : 1, + "score" : 1400.0336900375303, + "filename" : "20160102_164732.jpg" + }, + { + "matches" : 3, + "wins" : 1, + "score" : 1387.00607880615, + "filename" : "20160109_151607.jpg" + }, + { + "matches" : 3, + "wins" : 1, + "score" : 1383.263693206478, + "filename" : "20160109_152318.jpg" + }, + { + "matches" : 3, + "wins" : 1, + "score" : 1382.4634227228255, + "filename" : "20160109_152400.jpg" + } + ] + } +``` + +> If you run the program again, the cached data is loaded and new match-ups can +> be continued using the cached data. If new photos were added, they also get +> added to the table data and are included in match-ups. diff --git a/rank_photos.py b/rank_photos.py index b37bb31..9de4b14 100755 --- a/rank_photos.py +++ b/rank_photos.py @@ -303,15 +303,15 @@ class EloTable: n_photos = len(self._photos) - keys = self._photos.keys() + keys = list(self._photos.keys()) - for i in xrange(n_iterations): + for i in range(n_iterations): np.random.shuffle(keys) n_matchups = n_photos / 2 - for j in xrange(0, n_photos - 1, 2): + for j in range(0, n_photos - 1, 2): match_up = j / 2 @@ -482,12 +482,12 @@ better photo. #-------------------------------------------------------------------------- # dump ranked list to screen - print "Final Ranking:" + print("Final Ranking:") with open(ranked_txt, 'r') as fd: text = fd.read() - print text + print(text)