Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Simple Image Annotator

## Description
All image annotators I found either didn't work or had some overhead in the setup. So, I tried to make this one simple to run and simple to use.
I modified a great simple image annotator developed by [Sebastian Perez](https://github.com/sgp715) to facilitate browsing forward and backward and to output data in per-image files in KITTI format for easy consumption for object detection using detectnet.
![action](./actionshot.png)

## Install
Expand All @@ -16,22 +16,24 @@ $ pip install Flask
```
$ python app.py /images/directory
```
* you can also specify the file you would like the annotations output to (out.csv is the default)
* you can also specify the directory you would like the annotations output to (image dir is the default)
```
$ python app.py /images/directory --out test.csv
$ python app.py /images/directory --out /labels
```
* open http://127.0.0.1:5000/tagger in your browser
* specify port if you want (5555 is default)
```
$ python app.py /images/directory --out /labels --port 5556
```
* open http://127.0.0.1:5555/tagger in your browser
* only tested on Chrome

## Output
* in keeping with simplicity, the output is to a csv file with the following fields
* *id* - id of the bounding box within the image
* *name* - name of the bounding box within the image
* *image* - image the bounding box is associated with
* *xMin* - min x value of the bounding box
* *xMax* - max x value of the bounding box
* *yMin* - min y value of the bounding box
* *yMax* - max y value of the bounding box
* This branch outputs data in KITTI format for easy consumption in DIGITS using detectnet
* filename is the same as the input image file prefix changed to .txt
* pertinent columns for detectnet are 0: label; 1: truncated; 4: xmin; 5: ymin; 6: xmax; 7: ymax
```
person 0.2 0 0.0 114.0 650.0 227.0 796.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
```

## HOWTOs
* draw a bounding box
Expand All @@ -43,8 +45,10 @@ $ python app.py /images/directory --out test.csv
* enter the name you want in the input field
* press enter
* move to next image
* click the blue arrow button at the bottom of the page (depending on the size of the image you may have to scroll down)
* click the blue right arrow button at the top of the page (depending on the size of the image you may have to scroll down)
* move to the previous image
* click the left arrow button at the top of the page
* remove label
click the red button on the label you would like to remove
* check generated data
* at the top level of the directory where the program was run, there should be a file called out.csv that contains the generate data
* the output directory should contain one .txt file per image with KITTI format data
Binary file modified actionshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 56 additions & 25 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,66 @@
import sys
import os.path
from os import walk
import imghdr
import csv
import argparse
import re

from flask import Flask, redirect, url_for, request
from flask import render_template
from flask import send_file


app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
app.config['CURFIL'] = ""

def writeLabels():
image = app.config["FILES"][app.config["HEAD"]]
with open(app.config["OUTDIR"]+re.sub('\.(jpg|jpeg|png)','.txt',image),'w') as f:
for label in app.config["LABELS"]:
if label["name"] == "":continue
trunc = "0.0"
if "-trunc-" in label["name"]:
trunc = re.sub('.*-','',label["name"])
label["name"] = re.sub('-.*','',label["name"])
f.write(label["name"] + " " +
trunc + " "
"0 0.0 " +
str(round(float(label["xMin"]))) + " " +
str(round(float(label["yMin"]))) + " " +
str(round(float(label["xMax"]))) + " " +
str(round(float(label["yMax"]))) + " " +
"0.0 0.0 0.0 0.0 0.0 0.0 0.0\n")
f.close()

@app.route('/tagger')
def tagger():
if (app.config["HEAD"] == len(app.config["FILES"])):
return redirect(url_for('bye'))
if (app.config["HEAD"] < 0):
app.config["HEAD"] = 0
return redirect(url_for('tagger'))
directory = app.config['IMAGES']
image = app.config["FILES"][app.config["HEAD"]]
labels = app.config["LABELS"]
# [{"id":"1", "name":None, "ymin":0, "ymax":2, "xmin":0, "ymax":5},
# {"id":"2", "name":"image", "ymin":0, "ymax":20, "xmin":6, "ymax":50}]
if not image == app.config["CURFIL"]:
app.config["CURFIL"] = image
current_file = app.config["OUTDIR"]+re.sub('\.(jpg|jpeg|png)','.txt',app.config["CURFIL"])
if os.path.isfile(current_file):
for idx,line in enumerate(open(current_file,'r').readlines()):
larr = line.strip().split(" ")
lname = larr[0]
if not larr[1] == "0.0":
lname = lname + "-trunc-" + larr[1]
app.config["LABELS"].append({"id":idx+1, "name":lname, "xMin":float(larr[4]), "yMin":float(larr[5]), "xMax":float(larr[6]), "yMax":float(larr[7])})
not_end = not(app.config["HEAD"] == len(app.config["FILES"]) - 1)
print(not_end)
return render_template('tagger.html', not_end=not_end, directory=directory, image=image, labels=labels, head=app.config["HEAD"] + 1, len=len(app.config["FILES"]))
not_begin = app.config["HEAD"] > 0
return render_template('tagger.html', not_end=not_end, not_begin=not_begin, directory=directory, image=image, labels=labels, head=app.config["HEAD"] + 1, len=len(app.config["FILES"]), filename=image)

@app.route('/next')
def next():
image = app.config["FILES"][app.config["HEAD"]]
writeLabels()
app.config["HEAD"] = app.config["HEAD"] + 1
with open(app.config["OUT"],'a') as f:
for label in app.config["LABELS"]:
f.write(image + "," +
label["id"] + "," +
label["name"] + "," +
str(round(float(label["xMin"]))) + "," +
str(round(float(label["xMax"]))) + "," +
str(round(float(label["yMin"]))) + "," +
str(round(float(label["yMax"]))) + "\n")
app.config["LABELS"] = []
return redirect(url_for('tagger'))

Expand Down Expand Up @@ -68,21 +91,23 @@ def label(id):
app.config["LABELS"][int(id) - 1]["name"] = name
return redirect(url_for('tagger'))

# @app.route('/prev')
# def prev():
# app.config["HEAD"] = app.config["HEAD"] - 1
# return redirect(url_for('tagger'))
@app.route('/prev')
def prev():
writeLabels()
app.config["HEAD"] = app.config["HEAD"] - 1
app.config["LABELS"] = []
return redirect(url_for('tagger'))

@app.route('/image/<f>')
def images(f):
images = app.config['IMAGES']
return send_file(images + f)


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('dir', type=str, help='specify the images directory')
parser.add_argument("--out")
parser.add_argument("--out", help='specify labels director')
parser.add_argument("--port", help='specify port to run on')
args = parser.parse_args()
directory = args.dir
if directory[len(directory) - 1] != "/":
Expand All @@ -91,18 +116,24 @@ def images(f):
app.config["LABELS"] = []
files = None
for (dirpath, dirnames, filenames) in walk(app.config["IMAGES"]):
files = filenames
files = sorted(filenames)
break
if files == None:
print("No files")
exit()
app.config["FILES"] = files
app.config["HEAD"] = 0
if args.out == None:
app.config["OUT"] = "out.csv"
app.config["OUTDIR"] = args.dir
else:
app.config["OUTDIR"] = args.out
if not app.config["OUTDIR"].endswith("/"):
app.config["OUTDIR"] += "/"
if args.port == None:
app.config["PORT"] = 5555
else:
app.config["OUT"] = args.out
app.config["PORT"] = args.port
print(files)
with open("out.csv",'w') as f:
f.write("image,id,name,xMin,xMax,yMin,yMax\n")
app.run(debug="True")
app.run(host='0.0.0.0',debug="True",port=int(app.config["PORT"]))
167 changes: 167 additions & 0 deletions tagger.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<!doctype html>
<html style="height:100%;">
<head>
<title>Tagger</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"></link>
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/cerulean/bootstrap.min.css" rel="stylesheet"></link>
</head>
<body style="height:100%;">
<nav id="sidebar" style="
width: 25%;
height: 100%;
float: left;
z-index: 8000;
margin-bottom: 0px;">
<div class="panel panel-default" style="height: 100%;">
<div class="panel-heading">
<h3 class="panel-title">Labels</h3>
</div>
<script>
var label = function(id, name) {
window.location.replace("/label/" + id + "?name=" + name);
}
</script>
<div class="panel-body">
<div class="list-group">
{% for label in labels %}
<div class="list-group-item">
<div class="input-group">
<span class="input-group-addon" id="id">{{ label.id }}</span>
{% if label.name %}
<text style="background-color:#E5E7E9;" class="form-control custom-control" style="resize:none">{{ label.name }}</text>
<span class="input-group-btn">
<!-- <button class="btn btn-danger" type="button">-</button> -->
</span>
{% else %}
<input id= "{{ label.id }}" onkeydown="if (event.keyCode == 13) { label(this.id, this.value); }" type="text" class="form-control" placeholder="label name" ></input>
{% endif %}
<span class="input-group-btn">
<button id= "{{ label.id }}" class="btn btn-danger" onclick="window.location.replace('/remove/' + this.id)" type="button">-</button> if (event.keyCode == 13) { label(this.id, this.value); }
</span>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</nav>
<div id="content" class="container" style="
width: 75%;
height: 100%;
float: right;
z-index: 8000;
margin-bottom:
0px;">
<div class="row">
<text> {{ head }} / {{ len }} [{{ filename }}]</text>
{% if not_end %}
<a href="/next" class="btn btn-primary" style="float:right;" type="button">
<span class="glyphicon glyphicon-arrow-right"></span>
</a>
{% else %}
<a href="/next" class="btn btn-primary" style="float:right;" type="button">
<span class="glyphicon glyphicon-ok"> </span>
</a>
{% endif %}
{% if not_begin %}
<a href="/prev" class="btn btn-primary" style="float:right;" type="button">
<span class="glyphicon glyphicon-arrow-left"></span>
</a>
{% endif %}
</div>
<div style="overflow: scroll">
<canvas id="canvas" style="width:100%; height:80%; margin: 0; padding: 0;"></canvas>
</div>
<!-- <a href="/next" class="btn btn-primary" style="float:left;" type="button">
<span class="glyphicon glyphicon-arrow-left"></span>
</a> -->
<script>
var labels = {{ labels|tojson|safe }};
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var drawLabels = function(id, xMin, xMax, yMin, yMax) {
ctx.strokeStyle = "pink";
ctx.fillStyle = "pink";
ctx.rect(xMin, yMin, xMax - xMin, yMax - yMin);
ctx.lineWidth="3";
ctx.stroke();
ctx.font = "20px Arial";
ctx.fillText("id: " + id, xMin,yMin);
};
var image = new Image();
console.log(image);
image.onload = function(e) {
ctx.canvas.width = image.width;
ctx.canvas.height = image.height;
c.width = image.width;
c.height = image.height;
ctx.drawImage(image, 0, 0);
console.log(labels);
for (i = 0; i < labels.length; i++){
drawLabels(labels[i].id, labels[i].xMin, labels[i].xMax, labels[i].yMin, labels[i].yMax);
}
};
image.style.display="block";
image.src = "image/{{ image }}";

var clicked = false;
var fPoint = {};
c.onclick = function(e) {
console.log(clicked);
if (!clicked) {
var x = (image.width / c.scrollWidth) * e.offsetX;
var y = (image.height / c.scrollHeight) * e.offsetY;
console.log(e);
ctx.strokeStyle = "pink";
ctx.fillStyle = "pink";
ctx.beginPath();
ctx.arc(x, y, 3, 0, 2*Math.PI, false);
ctx.fill();
fPoint = {
x: x,
y: y
};
} else {
var x = (image.width / c.scrollWidth) * e.offsetX;
var y = (image.height / c.scrollHeight) * e.offsetY;
var xMin;
var xMax;
var yMin;
var yMin;
if (x > fPoint.x) {
xMax = x;
xMin = fPoint.x;
} else {
xMax = fPoint.x;
xMin = x;
}
if (y > fPoint.y) {
yMax = y;
yMin = fPoint.y;
} else {
yMax = fPoint.y;
yMin = y;
}
fPoint = {};
window.location.replace("/add/" + (labels.length + 1) +
"?xMin=" + xMin +
"&xMax=" + xMax +
"&yMin=" + yMin +
"&yMax=" + yMax);
}
clicked = !clicked;
};

// document.onkeydown = function(e) {
// if ("key" in e) {
// if(e.key == "Escape" || e.key == "Esc") {
// clicked = false;
// fPoint = {};
// }
// }
// };
</script>
</div>
</body>
</html>
7 changes: 6 additions & 1 deletion templates/tagger.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ <h3 class="panel-title">Labels</h3>
<span class="glyphicon glyphicon-ok"> </span>
</a>
{% endif %}
{% if not_begin %}
<a href="/prev" class="btn btn-primary" style="float:right;" type="button">
<span class="glyphicon glyphicon-arrow-left"></span>
</a>
{% endif %}
</div>
<div style="overflow: scroll">
<canvas id="canvas" style="width:100%; height:80%; margin: 0; padding: 0;"></canvas>
Expand All @@ -81,7 +86,7 @@ <h3 class="panel-title">Labels</h3>
ctx.rect(xMin, yMin, xMax - xMin, yMax - yMin);
ctx.lineWidth="3";
ctx.stroke();
ctx.font = "10px Arial";
ctx.font = "20px Arial";
ctx.fillText("id: " + id, xMin,yMin);
};
var image = new Image();
Expand Down