Skip to content

Commit

Permalink
Merge pull request #3 from hlgirard/dev/multiuser
Browse files Browse the repository at this point in the history
Multiuser capability
  • Loading branch information
hlgirard committed Apr 4, 2019
2 parents 7f9dbdc + 0251a9a commit 27e8d9c
Show file tree
Hide file tree
Showing 6 changed files with 835 additions and 183 deletions.
68 changes: 39 additions & 29 deletions README.md
Expand Up @@ -3,9 +3,9 @@

Graphical tool to manually label images in distinct categories to build training datasets.
Simply pass a list of categories, a directory containing images and start labelling.
Supports keyboard bindings to label even faster!
Supports multiple users, reconciliation and keyboard bindings to label even faster!

![screenshot](docs/screenshot_190228.png)
![screenshot](docs/screenshot_190404.png)

## Installation

Expand Down Expand Up @@ -34,16 +34,45 @@ pip install .

## Usage

### Command line tools
### Quick start

Pass the categories and image directory on the command line to start labelling. Use the on-screen buttons to select a label for the current image and advance to the next one. Number keys correspond to labels and can be used instead. A 'remove' label is automatically added to the list of passed categories.
Pass the labels and image directory on the command line to start labelling. Use the on-screen buttons to select a label for the current image and advance to the next one. Number keys correspond to labels and can be used instead.

```
simplabel --categories dog cat bird --directory path/to/image/directory
simplabel --labels dog cat bird --directory path/to/image/directory
```

After the first use, labels are stored in 'labels.pkl' and there is no need to pass the '--categories' argument unless you want to add labels.
You can also use '--reset' to delete the saved labels and dictionary from the directory before execution.
After the first use, labels are stored in `labels.pkl` and the `--labels` argument is ignored.

### Command line arguments

- `-d, --directory <PATH/TO/DIRECTORY>` sets the directory to search for images and save labels to. Defaults to the current working directory.
- `-l, --labels <label1 label2 label3 ...>` sets the categories for the labelling task. Only passed on the first use in a given directory.
- `-u, --user <USERNAME>` sets the username. Defaults to the OS login name if none is passed.
- `-r, --redundant` does not display other labelers selections for independent labelling. Reconciliation and Make Master are unavailable in this mode.
- `-v, --verbose` increases the verbosity level.
- `--remove-label` tries to safely remove a label from the list saved in `labels.pkl`.
- `--reset-lock` overrides the lock preventing the same username from being used multiple times simultaneously.
- `--delete-all` removes all files created by simplabel in the directory

### Multiuser

The app relies on the filesystem to save each user's selection and display other user's selections. It works best if the working directory is on a shared drive or in a synced folder (Dropbox, Onedrive...). The Reconcile workflow allows any user to see and resolve conflicts. The Make Master option can be used to create and save a master dictionary - `labeled_master.pkl` - containing all labeled images (after reconciliation).

### Import saved labels

The app saves a `labeled_<username>.pkl` file that contains a pickeled dictionary {image_name: label}. To import the dictionary, use the following sample code:

```python
import pickle

with open("labeled_user1.pkl","rb") as f:
label_dict = pickle.load(f)
```

## Advanced usage

### Utilities

Once you are done labelling, use the flow_to_directory tool to copy images to distinct directories by label

Expand All @@ -53,6 +82,8 @@ flow_to_directory --rawDirectory data/raw --outDirectory data/labeled

### Python object

The Tkinter app can also be started from a python environment

```python
from simplabel import ImageClassifier
import tkinter as tk
Expand All @@ -62,25 +93,4 @@ directory = "data/raw"
categories = ['dog', 'cat', 'bird']
MyApp = ImageClassifier(root, directory, categories)
tk.mainloop()
```

### Saved labels

The app saves a labeled.pkl file that contains a pickeled dictionary {image_name: label}. To import the dictionary, use the following sample code:

```python
import pickle

with open("labeled.pkl","rb") as f:
label_dict = pickle.load(f)
```

### Move labeled images to discrete directories

This utility copies labeled images from the raw directory to discrete folders by label in the labelled directory using the dictionary created by simplabel.

```python
from simplabel import utils

utils.flow_to_dict(rawDirectory, labelledDirectory)
```
```
90 changes: 77 additions & 13 deletions bin/simplabel
Expand Up @@ -3,33 +3,97 @@
import os
import argparse
import tkinter as tk
import sys
import pickle

import simplabel

#Setup parser
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--directory", default=os.getcwd(), help="Path of the directory")
ap.add_argument("-c", "--categories", nargs='*', default=None, help="List of categories")
ap.add_argument("-v", "--verbose", action='store_true', help="Enable verbose mode")
ap.add_argument("--reset", action='store_true', help="Deletes any existing data and starts a new labelling task")
ap.add_argument("-l", "--labels", nargs='+', default=None, help="List of labels")
ap.add_argument("-v", "--verbose", action='count', default=0, help="Enable verbose mode")
ap.add_argument("-u", "--user", help="Set username for the current session")
ap.add_argument("-r", "--redundant", action='store_true', help="Redundant mode: do not show other labeler's selections")
ap.add_argument("--delete-all", action='store_true', help="Deletes all files created by simplabel in a directory, this resets the labels and all saved data")
ap.add_argument("--reset-lock", action='store_true', help="Overrides the lock in case of incorrect lockout")
ap.add_argument("--remove-label", help="Remove a label from the list")



args = ap.parse_args()

# Get the variables from parser
rawDirectory = args.directory
categories = args.categories
verbosity = 1 if args.verbose else 0
categories = args.labels
verbosity = args.verbose
username = args.user
bResetLock = args.reset_lock
bRedundant = args.redundant

def delete_all_files(directory):
'''Deletes all files created by simplabel in a directory, this resets the labels and all saved data'''

save_files = [f for f in os.listdir(directory) if (f.endswith('.pkl') and f.startswith('label'))]
save_files.extend([f for f in os.listdir(directory) if f.startswith('.') and f.endswith('_lock.txt')])
if len(save_files) > 0:
response = input("Are you sure you want to delete all saved files: {}? (y/n)".format(save_files))
if response == 'y':
for f in save_files:
os.remove(os.path.join(directory,f))
print("Successfully deleted all saved files")
else:
print("Cancelled deletion, your files are exactly where you left them ;)")
else:
print("No files found in {}".format(directory))
sys.exit(0)

def remove_label(labelName):
'''Removes a label from the label file after verifying it isn't in use'''

labelToRemove = labelName.strip().lower().capitalize()

# Load the label file to check the presence of the label to remove
labelFile = rawDirectory + '/labels.pkl'
if os.path.isfile(labelFile):
with open(labelFile, 'rb') as f:
labels = pickle.load(f)
if labelToRemove not in labels:
print("No such label in labels.pkl")
sys.exit(0)
else:
print("No label file found.")
sys.exit(0)

# Get a list of users
users = [f.split('_')[1].split('.')[0] for f in os.listdir(rawDirectory) if (f.endswith('.pkl') and f.startswith('labeled_'))]

# Load each user's dictionary and check for the presense of the label to remove
for user in users:
dictPath = rawDirectory + "/labeled_" + user +".pkl"
with open(dictPath, "rb") as f:
userDict = pickle.load(f)
if labelToRemove in userDict.values():
print("Label {} is used by {}, cannot remove it from the list".format(labelToRemove, user))
sys.exit(0)

# If the check have passed, remove the label from the list and resave the list
labels.remove(labelToRemove)
with open(labelFile, 'wb') as f:
pickle.dump(labels, f)

print("Successfully removed label {} from the list".format(labelToRemove))
sys.exit(0)

# Reset all saved data if requested
if args.reset:
if os.path.exists(rawDirectory + "/labeled.pkl"):
print("Deleting labelling dictionary...")
os.remove(rawDirectory + "/labeled.pkl")
if os.path.exists(rawDirectory + "/labels.pkl"):
print("Deleting label list...")
os.remove(rawDirectory + "/labels.pkl")
if args.delete_all:
delete_all_files(rawDirectory)

# Remove label
if args.remove_label:
remove_label(args.remove_label)

# Launch the app
root = tk.Tk()
MyApp = simplabel.ImageClassifier(root, rawDirectory, categories, verbosity)
MyApp = simplabel.ImageClassifier(root, rawDirectory, categories = categories, verbose = verbosity, username = username, bResetLock = bResetLock, bRedundant = bRedundant)
tk.mainloop()
Binary file added docs/screenshot_190404.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -4,7 +4,7 @@
long_description = f.read()

setup(name='simplabel',
version='0.0.4',
version='0.1.0',
description='Simple tool to manually label images in disctinct categories to build training datasets.',
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
4 changes: 3 additions & 1 deletion simplabel/__init__.py
@@ -1 +1,3 @@
from .simplabel import *
from .simplabel import *

__version__ = '0.1.0'

0 comments on commit 27e8d9c

Please sign in to comment.