58 Commits
Author SHA1 Message Date
nilsonholger e8f7284bbb cursor is cross while making/editing labels, change zoom behaviour, thx @isaacgerg 2016-08-03 18:57:11 +02:00
Antonino Simone Di Stefano 06feb9bfeb Integrate horizontal/vertical help lines in RectItemInserter. 2016-08-03 18:41:46 +02:00
Antonino Simone Di Stefano 2b49500f64 Select and open multiple files in GUI dialog window 2016-08-03 18:36:05 +02:00
baeuml 4c39b89ca7 Merge pull request #85 from neingeist/feature/polygon-enumerator-plugin
Add a plugin to enumerate the corners of polygon items
2014-10-08 16:47:29 +02:00
Mike Gerber d47cd43948 Add a plugin to enumerate the corners of polygon items 2014-10-08 15:41:41 +02:00
baeuml 1b0a6eaa24 Merge pull request #81 from neingeist/feature/append-only-new-files
Append only new files
2014-09-01 09:41:57 +02:00
baeuml 46e80011c3 Merge pull request #83 from neingeist/fix/json-newline-at-end
Add a newline at the end of the JSON file
2014-09-01 09:40:06 +02:00
Mike Gerber 3bf65b9b26 Add a newline add the end of the JSON file
Editors like Vim add a newline at the end of the file when saving. git
then complains loudly about the "change":

  -]
  \ No newline at end of file
  +]

Add a newline to the JSON file to be compatible with editors.
2014-08-29 15:55:19 +02:00
Mike Gerber af0829baf5 Don't append duplicate files on cmd line multiple times
If a file is specified multiple time on the command line, do not
append it multiple times.
2014-08-28 19:46:05 +02:00
Mike Gerber e6927cbc04 Append only new files
When appending files, check if that file is already in the label file.
Do not append it if it's already there.
2014-08-28 19:46:05 +02:00
Martin Baeuml 8d9a6af7bf fix sphinx warnings 2014-04-16 23:02:08 +02:00
Martin Baeuml 8c711fa447 add a bit of documentation to setup.py 2014-04-16 23:01:59 +02:00
Martin Baeuml 138c9abaa1 bump version number in documentation, rename labeltool --> sloth 2014-04-16 23:01:36 +02:00
Martin Baeuml 013a2a4bc3 edit and update the documentation 2014-04-16 22:48:45 +02:00
Martin Baeuml bde36785b6 add build image in markdown 2014-04-15 23:38:43 +02:00
Martin Baeuml ebc40deb0a PEP8 changes 2014-04-15 23:33:13 +02:00
Martin Baeuml bad6e9ccda add numpy to setup.py dependencies 2014-04-15 23:27:18 +02:00
Martin Baeuml 184791a795 add TravisCI build status button 2014-04-15 23:24:37 +02:00
Martin Baeuml ad05875f0b Implement a more general version of the merge command
Also, add tests for this command.

This fixes #71.
2014-04-15 23:21:05 +02:00
Martin Baeuml d6a66eb0e6 remove empty file 2014-04-15 22:39:00 +02:00
Martin Baeuml 0dc837dc66 factor out common finishing code 2014-04-12 09:00:13 +02:00
baeuml 63e841ec12 Merge pull request #82 from neingeist/feature/polygons
Add the missing polygon functionality
2014-04-12 08:51:11 +02:00
Mike Gerber 26543b7a6d Remove default prefix 2014-04-10 23:36:06 +02:00
Mike Gerber 4feb87c72b When handling keys, only pressing Return finishes the polygon 2014-04-10 23:26:52 +02:00
Mike Gerber e810cd09af Allow finishing a polygon using a double click 2014-04-10 23:26:52 +02:00
Mike Gerber e2d61febc3 Allow aborting when inserting a polygon 2014-04-10 23:26:52 +02:00
Mike Gerber 8fbb44218d Remove unused variable _init_pos 2014-04-10 23:26:52 +02:00
Mike Gerber 32a4bbce76 No need for _current_image_item anymore
The insertion does not happen in abort() anymore, so PolygonItemInserter
does not need to save the current image item anymore for later use.
2014-04-10 23:26:52 +02:00
Mike Gerber 2579c4f7b0 Fix the start point of the polygon 2014-04-10 23:26:52 +02:00
Mike Gerber e0fa0534d5 Use the Enter key to finish the polygon
Using abort() (aka Escape) is not the right way to finish the polygon.
Use the Enter key instead. It works this way in Inkscape. Display a
message to instruct the user to use the Enter key to finish the
polygon.
2014-04-10 23:26:52 +02:00
Mike Gerber 3358d6d579 Add the missing PolygonItem
Add the functionality to add labels of polygon shape. That means:

- Fix the non-working PolygonItemInserter class
- Add the missing PolygonItem class
- Add the "polygon" class to the default configuration
2014-04-10 23:26:52 +02:00
baeuml 0d63d1ea34 Merge pull request #78 from neingeist/fix-relative-filenames
Add relative image filenames to the label file
2014-04-02 14:20:47 +02:00
baeuml 53a971b9ce Merge pull request #79 from neingeist/fix-trailing-whitespace
Avoid trailing whitespace in the JSON output
2014-04-02 14:17:51 +02:00
Mike Gerber 5ef339d7a5 Avoid trailing whitespace in the JSON output
The default separators used in the Python JSON library's dump() write
output with trailing whitespace. Use custom separators to avoid that, as
described in the documentation:

  http://docs.python.org/2/library/json.html
2014-03-30 10:58:06 +02:00
Mike Gerber 2afa69a2e4 Make image filenames relative to the label file
The image filenames written to the label files were absolute pathnames.
Change this to use filenames relative to the label file.
2014-03-30 10:53:17 +02:00
Martin Baeuml 19caf08ae1 add simple example on how to subclass RectItem for customization 2014-03-19 23:00:36 +01:00
Martin Baeuml 6b5dc54188 allow using QPen as argument for item.setPen(), fixes #77 2014-03-19 23:00:12 +01:00
Martin Baeuml cf0b79afed more cleanup 2014-03-19 22:59:04 +01:00
Martin Baeuml fa578410dc add missing imports 2014-03-19 22:58:53 +01:00
Martin Baeuml 9eb3e888c5 remove empty file 2014-03-19 22:58:21 +01:00
Martin Baeuml 86ba971a9c add missing travis requirements 2014-03-19 22:58:09 +01:00
Martin Baeuml bcd5415b0a fix bug in factory 2014-03-19 22:35:59 +01:00
Martin Baeuml 9b3ba3cb9f remove obsolete container tests 2014-03-19 22:35:50 +01:00
Martin Baeuml db1dc22036 cleanup/pep8 2014-03-19 22:35:42 +01:00
Martin Baeuml 957daf4c84 remove importlib copy, use stdlib version 2014-03-19 22:11:00 +01:00
Martin Baeuml 5d207711de pep8 2014-03-19 22:00:51 +01:00
Martin Baeuml 1c0387a192 pep8 2014-03-19 14:53:47 +01:00
Martin Baeuml a811b9cccf remove more mutable default arguments 2014-03-19 14:32:09 +01:00
Martin Baeuml 9bb08470cc fix mutable default arguments
Mutable default arguments are usually a bad idea, and certainly not intended here.
2014-03-19 14:16:19 +01:00
Martin Baeuml 638c3db45d pep8 changes 2014-03-19 14:15:06 +01:00
Martin Baeuml 841ffee22d pep8 fixes 2014-03-19 13:56:29 +01:00
Mika Fischer 5e1e1ffd38 Add Tooltips with names of facial landmarks 2014-03-10 17:29:51 +01:00
Mika Fischer 402ccb556e Add support for reading annotations in msgpack format 2014-03-10 17:14:09 +01:00
Mika Fischer a78591a9cd Add support for displaying facial landmarks in common notation 2014-03-10 17:13:37 +01:00
Martin Baeuml 2ca1f84156 pep8 fixes 2014-03-03 10:24:17 +01:00
Martin Baeuml 16b5e9ca4a move docstrings to appropriate locations 2014-03-03 10:24:04 +01:00
Tobias Gehrig 015b6f3afb Make attributes in LabelEditor sorted 2014-02-25 15:47:57 +01:00
Martin Baeuml 5df2ff66e0 add release to README, use markdown now 2013-11-29 10:14:48 +01:00
44 changed files with 898 additions and 420 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ python:
# install dependencies and sloth
install:
- sudo apt-get install python-imaging python-qt4 python-numpy python3-numpy
- sudo apt-get install python-imaging python-qt4 python-numpy python3-numpy python-yaml msgpack-python
- pip install Pillow
- pip install .
-3
View File
@@ -1,3 +0,0 @@
sloth is a tool for labeling image and video data for computer vision research.
The documentation can be found at http://sloth.readthedocs.org/
+15
View File
@@ -0,0 +1,15 @@
sloth
=====
[![Build Status](https://travis-ci.org/cvhciKIT/sloth.svg)](https://travis-ci.org/cvhciKIT/sloth)
sloth is a tool for labeling image and video data for computer vision research.
The documentation can be found at http://sloth.readthedocs.org/ .
Latest Releases
===============
2013/11/29 v1.0: 2e69fdae40f89050fbaeef22491eee2a92e78b4f [.zip](https://github.com/cvhciKIT/sloth/archive/v1.0.zip) [.tar.gz](https://github.com/cvhciKIT/sloth/archive/v1.0.tar.gz)
For a full list, visit https://github.com/cvhciKIT/sloth/releases
+16 -16
View File
@@ -4,7 +4,7 @@
Concepts
========
In this section, we will introduce some high-level concepts of Sloth.
We start by introducing some high-level concepts of Sloth.
Labels
@@ -15,25 +15,25 @@ can contain any number of labels. Each label is a set of key-value pairs,
for example::
{
class: "rect",
id: "Martin",
x: 10,
y: 30,
width: 40,
height: 50,
"class": "rect",
"id": "Martin",
"x": 10,
"y": 30,
"width": 40,
"height": 50,
}
The only required key a label *has* to have is the "class" key. It will be used by the label tool
The only required key a label *has* to have is the "class" key. By convention, the value of "class" is used
to determine the appropriate visualization for this label (in our example it will draw a rectangle).
You will later see, how you can customize the mapping between class and visualization and how to
write your own visualizations.
We will later see, how the mapping between class and visualization can be customized and how custom visualizations
can be added.
Label type conventions
----------------------
Sloth provides support for a range of standard shape labels (for example `rect`, `point`, `polygon` etc.).
In order for the label tool to correctly visualize these labels, the labels have to follow
Sloth provides support for a range of standard shape labels (for example rectangles, points and polygons).
In order for Sloth to correctly visualize these labels, the labels have to follow
a convention, which keys represent the `x`- and `y`-coordinates, `width` and `height` and so on.
The following simple geometric classes are supported out of the box, i.e.
@@ -76,7 +76,7 @@ Polygon
User defined labels
-------------------
In many cases, it will not be sufficient for your labeling needs to stick to those simple classes. Or,
In many cases, labeling requirements extend beyond those simple classes. Or,
you might want to add further information. Since each label is just a set of key-value pairs, this
is easily possible by adding more key-value pairs that carry additional information.
For example you can add a key ``type`` that differentiates point labels to be either the label
@@ -85,12 +85,12 @@ for the left or the right eye of a face::
{
"class": "point",
"type": "left_eye",
x: 50, y: 40,
"x": 50, "y": 40,
},
{
"class": "point",
"type": "right_eye",
x: 70, y: 40,
"x": 70, "y": 40,
}
Of course, you can also create new classes::
@@ -116,7 +116,7 @@ Of course, you can also create new classes::
You see in the second example, that the label does not necessarily have to name
a geometric form of any sort. Neither do the key-value pairs have to denote
only coordinates or attributes. It can be anything you like. However, if you
create your own classes you will need to tell the label tool in the
create your own classes you will need to tell the Sloth in the
configuration how to display this label class. See section :doc:`configuration` on how to do that.
+6 -6
View File
@@ -40,17 +40,17 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = 'Labeltool'
copyright = '2011, cv:hci lab, Institute for Anthropomatics, Karlsruhe Institute of Technology'
project = 'sloth'
copyright = '2011-2014, cv:hci lab, Institute for Anthropomatics, Karlsruhe Institute of Technology'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '0.1'
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -180,7 +180,7 @@ todo_include_todos = True
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Labeltool.tex', 'Labeltool Documentation',
('index', 'sloth.tex', 'Sloth Documentation',
'cv:hci lab, Institute for Anthropomatics, Karlsruhe Institute of Technology', 'manual'),
]
@@ -213,6 +213,6 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'labeltool', 'Labeltool Documentation',
('index', 'sloth', 'Sloth Documentation',
['cv:hci lab'], 1)
]
+13 -22
View File
@@ -59,6 +59,7 @@ different keys of the dictionary in detail:
current state of the button area.
* ``attributes`` has three functions:
1. It defines how a new annotation can be initialized. Fixed
key-value pairs are used directly. If the value is a list of items, the
user can choose interactively which one of the values he wants to use for
@@ -78,7 +79,6 @@ different keys of the dictionary in detail:
* the value for one of keys matches and the other key is not present in
either ``attributes`` or the annotation.
Note that the comma at the end of the first tuple is mandatory. Otherwise the
outer tuple will not be recognized as one (it will be only parentheses around
an object, which will alone not be translated into a tuple object. This
@@ -101,7 +101,7 @@ Default::
)
Defines global keyboard shortcuts. Each hotkey is defined by a tuple with at
least 2 entries, where the first entry is the hotkey (sequence), and the second
least two entries, where the first entry is the hotkey (sequence), and the second
entry is the function that is called. The function should expect a single
parameter, the labeltool object. The optional third entry -- if present -- is
expected to be a string describing the action.
@@ -113,11 +113,13 @@ CONTAINERS
Default::
{
'*.txt': 'annotations.container.SimpleOneLinerTextContainer',
'*.yaml': 'annotations.container.YamlContainer',
'*.pickle': 'annotations.container.PickleContainer',
}
(
('*.json', 'sloth.annotations.container.JsonContainer'),
('*.msgpack', 'sloth.annotations.container.MsgpackContainer'),
('*.yaml', 'sloth.annotations.container.YamlContainer'),
('*.pickle', 'sloth.annotations.container.PickleContainer'),
('*.sloth-init', 'sloth.annotations.container.FileNameListContainer'),
)
Defines a mapping of which container should be used for loading a label file
matching the given filename pattern. This can of course also be a user defined
@@ -132,25 +134,14 @@ container. You can also define the class directly (instead of a module path)::
PLUGINS
-------
Did not think to much about this yet. This is rather for v2.0. Could image to be able to define some kind of
plugin that might do some preprocessing on an image, e.g. detect all faces and convert them into labels.
.. _SCENE_BACKGROUND:
SCENE_BACKGROUND
----------------
A list/tuple of classes implementing the sloth plugin interface. The
classes can either be given directly or their module path be specified as string.
By default, no plugins are active.
Default::
Qt.darkGray
()
Allows to set the scene background to a custom color or pattern. Expects a QBrush. A more
complex background could be a regular box pattern which might simplify the exact resizing of
annotations that extend over image boundaries::
from PyQt4.QtGui import QBrush
from PyQt4.QtCore import Qt
SCENE_BACKGROUND = QBrush(Qt.darkGray, Qt.CrossPattern)
Extending default values
+14 -7
View File
@@ -34,8 +34,8 @@ A container is expected to implement (at least) these five functions:
``frame_number``.
The container base class ``AnnotationContainer`` provides default
implementations for all five function. It however deferes the
parsing and serialization of the labels from/to disk to the to functions
implementations for all five function. It however defers the
parsing and serialization of the labels from/to disk to the two functions
.. py:function:: parseFromFile(self, filename)
@@ -50,9 +50,9 @@ provide implementations for those two functions.
Default Containers
==================
A few containers are included in sloth. They can be found in the module
A few containers are included in Sloth. They can be found in the module
``sloth.annotations.container``. In the default configuration, these
containers are included for their respective default filename patter.
containers are included for their respective default filename pattern.
JsonContainer
-------------
@@ -62,7 +62,6 @@ Default pattern: ``*.json``
Writes and reads annotations in JSON format (needs the python module ``json``
to be installed).
YamlContainer
-------------
@@ -71,6 +70,14 @@ Default pattern: ``*.yaml``
Writes and reads annotations in YAML format (needs the python module ``yaml``
to be installed).
MsgpackContainer
----------------
Default pattern: ``*.msgpack``
Writes and reads annotations in Msgpack format (needs the python module ``msgpack``
to be installed).
PickleContainer
---------------
@@ -87,8 +94,8 @@ Default pattern: ``*.sloth-init``
A simple container that reads one image filename per line. No annotations
are supported. This container can be used for example for initializing
a labeling session. After adding labels, another container should be
used for saving though, otherwise the labels will be lost. (write support
not implemented yet anyway)
used for saving though, otherwise the labels will be lost (write support
is not implemented).
FeretContainer
--------------
+15 -12
View File
@@ -117,7 +117,7 @@ Writing a custom configuration
==============================
We already briefly touch the subject of configuration. Sloth can be easily
tailored to once labeling needs by using different label types, adding own
tailored to ones labeling needs by using different label types, adding own
visualization items and container formats. All of this can be specified in the
configuration file. The configuration file is a python module where the
module-level variables represent the settings. The most important variable is
@@ -165,8 +165,8 @@ different keys of the dictionary in detail:
``sloth.items.RectItem`` class is used, which will draw a rectangle as given by the
coordinates in the annotation. Sloth comes with several predefined
visualization classes, such as ``sloth.items.RectItem`` and
``sloth.items.PointItem`` (see :ref:`items` for a full list). However, it is
also very easy to define your own visualization class (see :ref:`items`).
``sloth.items.PointItem`` (see :doc:`items` for a full list). However, it is
also very easy to define your own visualization class (see :ref:`CUSTOM_ITEMS`).
* ``inserter`` specifies which class is responsible for creating new
annotations based on user input. When the user enters insert-mode with a
@@ -174,8 +174,9 @@ different keys of the dictionary in detail:
user input and takes care of the creation of a new annotation.
* ``attributes`` has three purposes:
1. It defines which key-values pairs are inserted into a new annotation directly.
This can either be a fixed key-value pair. Or, if the value is a list of
This can either be a fixed key-value pair. If the value is a list of
items, the user can choose interactively in the Properties dock which one
of the values he wants to use for a new label. The current state is then
passed to the inserter.
@@ -200,23 +201,25 @@ pass it to Sloth with the ``--config`` command line parameter::
sloth --config myconfig.py examples/example1_labels.json
You can now start labeling head locations and eye positions. You'll see that for each
depending on the chosen annotation, you can either insert a rectangle (this is internally
You can now start labeling head locations and eye positions. You'll see that --
depending on the chosen annotation -- you can either insert a rectangle (this is internally
done by the ``RectItemInserter``) or points (using the ``PointItemInserter``). For
each annotation you can choose an identity between the two supplied options.
each annotation you can choose an identity from the two supplied options.
There are more possibilities to configure the labels, e.g. defining hotkeys, which we
have not touched here. Refer to :ref:`LABELS` for the full documentation.
There are more possibilities to configure the labels, which we have not touched here.
For example, hotkeys can be defined to quick access to often used label types.
Refer to :ref:`LABELS` for the full documentation.
Apart from defining the supported labels in the configuration, other parts of Sloth's
behaviour can be configured there as well, e.g. for supporting own label formats with
custom containers. See :doc:`Configuration <configuration>` for the full
behaviour can be configured there as well, e.g. for supporting custom label formats (using
custom containers). See :doc:`Configuration <configuration>` for the full
reference of all configuration options.
Next steps
==========
You can now continue by reading about :doc:`all available configuration options <configuration>`,
You should quickly familiarize yourself with the :doc:`basic concepts and conventions <concepts>` in Sloth.
Continue then by reading about :doc:`all available configuration options <configuration>`,
how to write your own :doc:`visualization items <items>`, :doc:`custom inserters <inserters>` or
:doc:`custom label containers <containers>`.
+2 -2
View File
@@ -25,7 +25,7 @@ Feedback
*Please* provide feedback to us on this document and Sloth in general! We won't be able
to incorporate your required features if you do not talk to us. Also, use the bug tracker
at https://github.com/cvhciKIT/sloth/issues. Of course, similarly welcome are patches!
at https://github.com/cvhciKIT/sloth/issues. Of course, pull requests are always welcome!
Contents
========
@@ -34,8 +34,8 @@ Contents
:maxdepth: 2
installation
concepts
first_steps
concepts
configuration
items
inserters
+1 -1
View File
@@ -7,4 +7,4 @@ Inserters
Inserters are used for creating new labels interactively. When the users selects a label type in the button area,
the corresponding inserter for the label type (as defined in the :ref:`configuration`).
.. todo:: Write this. It's pretty similar to the items section.
.. todo:: Write this. It's pretty similar to the items section. Pull requests are welcome!
+1 -2
View File
@@ -16,7 +16,7 @@ either `PIL`_ or okapy for image loading.
.. _PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro
.. _PIL: http://www.pythonware.com/products/pil/
To use okapy, make sure to make it's modules known to python, e.g. add
To use okapy, make sure to make its modules known to python, e.g. add
<okapibuild>/python/ to the PYTHONPATH environment variable::
export PYTHONPATH=<okapibuild>/python/:$PYTHONPATH
@@ -26,7 +26,6 @@ For compiling the docs, `Python Sphinx`_ is needed.
.. _Python Sphinx: http://pypi.python.org/pypi/Sphinx
Installing Sloth
================
+13 -3
View File
@@ -23,10 +23,20 @@ The label tool comes with a few predefined visualization items:
- ``items.PolygonItem``
Draws a rectangle. Expects the label to have keys ``xn`` and ``yn``, which are ``;``-separated
Draws a polygon. Expects the label to have keys ``xn`` and ``yn``, which are ``;``-separated
lists of point coordinates.
The predefined items can be used in different ways. If you give the class name in
- ``items.IDRectItem``
Extends ``RectItem``. Displays the value of ``id`` within the rectangle as text.
When an item is selected, the hotkey ``i`` can be used to cycle between numerical id values.
- ``items.OccludablePointItem``
Extends ``PointItems``. Draws the point in a different color (red) when the value of ``occluded`` is ``True``.
The hotkey ``o`` is defined to toggle the ``occluded`` property.
The predefined items can be used in different ways. If you specify the class name in
the configuration, the constructor will be called for initializing the item. However,
you can also create and instance of the item, configure for example the color, and then
use this instance in the configuration. The predefined items have their ``__call__`` operator
@@ -88,7 +98,7 @@ set the graphics items flags to allow interactive modfications of the item::
QGraphicsItem.ItemSendsGeometryChanges | \
QGraphicsItem.ItemSendsScenePositionChanges)
Then we catch the notifications about item changes by overriding ``ìtemChange``. We especially need
By overriding ``ìtemChange`` we get notified about item changes, such as a position change. Especially, we need
to inform the model about the modification::
def itemChange(self, change, value):
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

+26
View File
@@ -0,0 +1,26 @@
from PyQt4.QtGui import QPen
from PyQt4.Qt import Qt
from sloth.items import RectItem
class CustomRectItem(RectItem):
# display values of x and y as text inside the rectangle
defaultAutoTextKeys = ['x', 'y']
def __init__(self, *args, **kwargs):
RectItem.__init__(self, *args, **kwargs)
# set drawing pen to red with width 2
self.setPen(QPen(Qt.red, 2))
LABELS = (
{
'attributes': {
'class': 'rect',
},
'inserter': 'sloth.items.RectItemInserter',
'item': CustomRectItem, # use custom rect item instead of sloth's standard item
'text': 'Rectangle',
},
)
+12 -3
View File
@@ -1,9 +1,18 @@
#!/usr/bin/env python
import sys, os
"""
sloth
-----
sloth is a tool for labeling image and video data for computer vision research.
The documentation can be found at http://sloth.readthedocs.org/ .
"""
import os
from distutils.core import setup
from distutils.command.install import INSTALL_SCHEMES
import sloth
# the following installation setup is based on django's setup.py
def fullsplit(path, result=None):
"""
@@ -49,7 +58,7 @@ setup(name='sloth',
description='The Sloth Labeling Tool',
author='CV:HCI Research Group',
url='http://sloth.readthedocs.org/',
requires=['importlib', 'PyQt4',],
requires=['importlib', 'PyQt4', 'numpy'],
packages=packages,
data_files=data_files,
scripts=['sloth/bin/sloth']
+47 -32
View File
@@ -1,5 +1,4 @@
import os
import sys
import fnmatch
import time
import numpy as np
@@ -11,21 +10,21 @@ LOG = logging.getLogger(__name__)
try:
import cPickle as pickle
except:
except ImportError:
import pickle
try:
import json
except:
except ImportError:
pass
try:
import yaml
except:
except ImportError:
pass
try:
import okapy
import okapy.videoio as okv
_use_pil = False
except:
except ImportError:
try:
from PIL import Image
_use_pil = True
@@ -82,10 +81,11 @@ class AnnotationContainer:
self.clear()
def filename(self):
"""The current filename."""
return self._filename
def clear(self):
self._annotations = [] # TODO Why isn't this used? Annotations are passed as parameters instead. Let's have encapsulation.
self._annotations = [] # TODO Why isn't this used? Annotations are passed as parameters instead. Let's have encapsulation.
self._filename = None
self._video_cache = {}
@@ -204,24 +204,24 @@ class PickleContainer(AnnotationContainer):
"""
Overwritten to write pickle files.
"""
# TODO make all image filenames relative to the label file
f = open(fname, "wb")
pickle.dump(annotations, f)
class OkapiAnnotationContainer(AnnotationContainer):
"""
Converts a AnnotationPropertiesMap to a dict
Simple container which writes the annotations to disk using okapy.AnnotationContainer.
"""
def convertAnnotationPropertiesMapToDict(self, properties):
"""
Converts a AnnotationPropertiesMap to a dict
"""
propdict = {}
for k, v in properties.items():
propdict[k] = v
return propdict
"""
Simple container which writes the annotations to disk using okapy.AnnotationContainer.
"""
def parseFromFile(self, filename):
"""
Overwritten to read Okapi::Annotation files.
@@ -252,10 +252,10 @@ class OkapiAnnotationContainer(AnnotationContainer):
return annotations
"""
Converts a dict to a AnnotationPropertiesMap
"""
def convertDictToAnnotationPropertiesMap(self, annotation, propdict):
"""
Converts a dict to a AnnotationPropertiesMap
"""
for k, v in propdict.items():
if k != 'annotations' or k != 'frames':
annotation.set_str(k, str(v))
@@ -292,7 +292,6 @@ class OkapiAnnotationContainer(AnnotationContainer):
fileitem.frames().push_back(frameitem)
container.files().push_back(fileitem)
# TODO make all image filenames relative to the label file
container.WriteToFile(fname)
@@ -312,9 +311,32 @@ class JsonContainer(AnnotationContainer):
"""
Overwritten to write JSON files.
"""
# TODO make all image filenames relative to the label file
f = open(fname, "w")
json.dump(annotations, f, indent=4, sort_keys=True)
json.dump(annotations, f, indent=4, separators=(',', ': '), sort_keys=True)
f.write("\n")
class MsgpackContainer(AnnotationContainer):
"""
Simple container which writes the annotations to disk in Msgpack format.
"""
def parseFromFile(self, fname):
"""
Overwritten to read Msgpack files.
"""
import msgpack
f = open(fname, "r")
return msgpack.load(f)
def serializeToFile(self, fname, annotations):
"""
Overwritten to write Msgpack files.
"""
# TODO make all image filenames relative to the label file
import msgpack
f = open(fname, "w")
msgpack.dump(annotations, f)
class YamlContainer(AnnotationContainer):
@@ -333,7 +355,6 @@ class YamlContainer(AnnotationContainer):
"""
Overwritten to write YAML files.
"""
# TODO make all image filenames relative to the label file
f = open(fname, "w")
yaml.dump(annotations, f)
@@ -351,8 +372,8 @@ class FileNameListContainer(AnnotationContainer):
for line in f:
line = line.strip()
fileitem = {
'filename': line,
'class': 'image',
'filename': line,
'class': 'image',
'annotations': [],
}
annotations.append(fileitem)
@@ -360,8 +381,7 @@ class FileNameListContainer(AnnotationContainer):
return annotations
def serializeToFile(self, filename, annotations):
raise NotImplemented(
"FileNameListContainer.save() is not implemented yet.")
raise NotImplemented("FileNameListContainer.save() is not implemented yet.")
class FeretContainer(AnnotationContainer):
@@ -381,15 +401,12 @@ class FeretContainer(AnnotationContainer):
fileitem = {
'filename': s[0] + ".bmp",
'class': 'image',
'annotations': [
{'class': 'left_eye', 'x': int(s[1]), 'y': int(s[2])},
{'class': 'right_eye', 'x': int(s[3]), 'y': int(s[4])},
{'class': 'mouth', 'x': int(s[5]), 'y': int(s[6])}
]
}
fileitem['annotations'] = [
{'class': 'left_eye',
'x': int(s[1]), 'y': int(s[2])},
{'class': 'right_eye',
'x': int(s[3]), 'y': int(s[4])},
{'class': 'mouth',
'x': int(s[5]), 'y': int(s[6])}
]
annotations.append(fileitem)
return annotations
@@ -398,8 +415,6 @@ class FeretContainer(AnnotationContainer):
"""
Not implemented yet.
"""
# TODO make sure the image paths are
# relative to the label file's directory
raise NotImplemented(
"FeretContainer.serializeToFile() is not implemented yet."
)
+42 -23
View File
@@ -1,23 +1,25 @@
"""
The annotationmodel module contains the classes for the AnnotationModel.
"""
from PyQt4.QtGui import QTreeView, QItemSelection, QItemSelectionModel, QSortFilterProxyModel, QBrush
from PyQt4.QtCore import QModelIndex, QAbstractItemModel, Qt, pyqtSignal, QVariant
import os.path, sys
import copy
from collections import MutableMapping
import os.path
import time
import logging
import copy
from collections import MutableMapping
from PyQt4.QtGui import QTreeView, QItemSelection, QItemSelectionModel, QSortFilterProxyModel, QBrush
from PyQt4.QtCore import QModelIndex, QAbstractItemModel, Qt, pyqtSignal, QVariant
LOG = logging.getLogger(__name__)
ItemRole, DataRole, ImageRole = [Qt.UserRole + i + 1 for i in range(3)]
ItemRole, DataRole, ImageRole = [Qt.UserRole + ur + 1 for ur in range(3)]
class ModelItem:
def __init__(self):
self._loaded = True
self._model = None
self._parent = None
self._row = -1
self._loaded = True
self._model = None
self._parent = None
self._row = -1
if not hasattr(self, "_children"):
self._children = []
@@ -170,7 +172,7 @@ class ModelItem:
for i, item in enumerate(items):
item._parent = self
item._row = next_row + i
item._row = next_row + i
self._children.append(item)
if self._model is not None:
@@ -219,11 +221,12 @@ class ModelItem:
def getColor(self):
return None
class RootModelItem(ModelItem):
def __init__(self, model, files):
ModelItem.__init__(self)
self._model = model
self._toload = []
self._toload = []
for f in files:
self._toload.append(f)
self._children.append(f)
@@ -249,9 +252,9 @@ class RootModelItem(ModelItem):
else:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
def appendChild(self, item):
def appendChild(self, item, signalModel=True):
if isinstance(item, FileModelItem):
ModelItem.appendChild(self, item)
ModelItem.appendChild(self, item, signalModel=signalModel)
else:
raise TypeError("Only FileModelItems can be attached to RootModelItem")
@@ -283,12 +286,15 @@ class RootModelItem(ModelItem):
return [child.getAnnotations() for child in self.children()
if hasattr(child, 'getAnnotations')]
class KeyValueModelItem(ModelItem, MutableMapping):
def __init__(self, hidden=[], properties=None):
def __init__(self, hidden=None, properties=None):
ModelItem.__init__(self)
self._dict = {}
self._items = {}
self._hidden = hidden + [None, 'class', 'unlabeled', 'unconfirmed']
self._dict = {}
self._items = {}
self._hidden = set(hidden or [])
self._hidden.update({None, 'class', 'unlabeled', 'unconfirmed'})
# dummy key/value so that pyqt does not convert the dict
# into a QVariantMap while communicating with the Views
self._dict[None] = None
@@ -393,8 +399,10 @@ class KeyValueModelItem(ModelItem, MutableMapping):
del self['unconfirmed']
self._emitDataChanged('unconfirmed')
class FileModelItem(KeyValueModelItem):
def __init__(self, fileinfo, hidden=['filename']):
def __init__(self, fileinfo, hidden=None):
if not hidden: hidden = ['filename']
KeyValueModelItem.__init__(self, hidden=hidden, properties=fileinfo)
def data(self, role=Qt.DisplayRole, column=0):
@@ -417,6 +425,7 @@ class FileModelItem(KeyValueModelItem):
elif fileinfo['class'] == 'video':
return VideoFileModelItem(fileinfo)
class ImageModelItem(ModelItem):
def __init__(self, annotations):
ModelItem.__init__(self)
@@ -435,6 +444,7 @@ class ImageModelItem(ModelItem):
for ann in self.annotations():
ann.setUnconfirmed(False)
class ImageFileModelItem(FileModelItem, ImageModelItem):
def __init__(self, fileinfo):
self._annotation_data = fileinfo.get("annotations", [])
@@ -467,6 +477,7 @@ class ImageFileModelItem(FileModelItem, ImageModelItem):
if hasattr(child, 'getAnnotations')]
return fi
class VideoFileModelItem(FileModelItem):
def __init__(self, fileinfo):
frameinfos = fileinfo.get("frames", [])
@@ -483,6 +494,7 @@ class VideoFileModelItem(FileModelItem):
fi['frames'] = [child.getAnnotations() for child in self.children()]
return fi
class FrameModelItem(ImageModelItem, KeyValueModelItem):
def __init__(self, frameinfo):
annotations = frameinfo.get("annotations", [])
@@ -516,6 +528,7 @@ class FrameModelItem(ImageModelItem, KeyValueModelItem):
if hasattr(child, 'getAnnotations')]
return fi
class AnnotationModelItem(KeyValueModelItem):
def __init__(self, annotation):
KeyValueModelItem.__init__(self, properties=annotation)
@@ -542,6 +555,7 @@ class AnnotationModelItem(KeyValueModelItem):
return Qt.red
return None
class KeyValueRowModelItem(ModelItem):
def __init__(self, key, read_only=True):
ModelItem.__init__(self)
@@ -579,18 +593,21 @@ class KeyValueRowModelItem(ModelItem):
return True
return False
class AnnotationModel(QAbstractItemModel):
# signals
dirtyChanged = pyqtSignal(bool, name='dirtyChanged')
def __init__(self, annotations, parent=None):
QAbstractItemModel.__init__(self, parent)
start = time.time()
self._annotations = annotations
self._dirty = False
self._root = RootModelItem(self, annotations)
self._dirty = False
self._root = RootModelItem(self, annotations)
diff = time.time() - start
LOG.info("Created AnnotationModel in %.2fs" % (diff, ))
self.dataChanged.connect(self.onDataChanged)
self.rowsInserted.connect(self.onDataChanged)
self.rowsRemoved.connect(self.onDataChanged)
@@ -660,8 +677,10 @@ class AnnotationModel(QAbstractItemModel):
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
if section == 0: return "File/Type/Key"
elif section == 1: return "Value"
if section == 0:
return "File/Type/Key"
elif section == 1:
return "Value"
return None
# Own methods
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
import sys
from os.path import dirname, realpath
sys.path.insert(1, dirname(dirname(dirname(realpath( __file__ )))))
sys.path.insert(1, dirname(dirname(dirname(realpath(__file__)))))
from PyQt4.QtGui import QApplication
from sloth.core.labeltool import LabelTool
from sloth import APP_NAME, ORGANIZATION_NAME, ORGANIZATION_DOMAIN
+4 -3
View File
@@ -1,7 +1,8 @@
from sloth.conf import default_config
from sloth.utils import importlib
import sys
import os
import sys
import importlib
from sloth.conf import default_config
class Config:
def __init__(self):
+12 -2
View File
@@ -37,7 +37,7 @@ LABELS = (
{
'attributes': {
'class': 'Face',
},
},
'inserter': 'sloth.items.RectItemInserter',
'item': 'sloth.items.RectItem',
'hotkey': 'f',
@@ -46,7 +46,7 @@ LABELS = (
{
'attributes': {
'class': 'rect',
},
},
'inserter': 'sloth.items.RectItemInserter',
'item': 'sloth.items.RectItem',
'hotkey': 'r',
@@ -61,6 +61,15 @@ LABELS = (
'hotkey': 'p',
'text': 'Point',
},
{
'attributes': {
'class': 'polygon',
},
'inserter': 'sloth.items.PolygonItemInserter',
'item': 'sloth.items.PolygonItem',
'hotkey': 'o',
'text': 'Polygon',
},
)
# HOTKEYS
@@ -97,6 +106,7 @@ HOTKEYS = (
# to such a class.
CONTAINERS = (
('*.json', 'sloth.annotations.container.JsonContainer'),
('*.msgpack', 'sloth.annotations.container.MsgpackContainer'),
('*.yaml', 'sloth.annotations.container.YamlContainer'),
('*.pickle', 'sloth.annotations.container.PickleContainer'),
('*.sloth-init', 'sloth.annotations.container.FileNameListContainer'),
+60 -56
View File
@@ -1,14 +1,17 @@
import sys
import os
import sloth
import shutil
from pprint import pprint
from sloth.core.cli import BaseCommand, CommandError
from sloth.annotations.container import *
from optparse import make_option
from operator import itemgetter
import logging
logger = logging.getLogger(__name__)
class ConvertCommand(BaseCommand):
"""
Converts a label file from one file format to another.
@@ -77,8 +80,9 @@ class DumpLabelsCommand(BaseCommand):
class AppendFilesCommand(BaseCommand):
"""
Append image or video files to a label file. Creates the label
file if it does not exist before.
Append image or video files to a label file. Creates the label file if it
does not exist before. If the image or video file is already in the label
file, it will not be appended again.
"""
args = '<labelfile> <file1> [<file2> ...]'
help = __doc__.strip()
@@ -98,6 +102,8 @@ class AppendFilesCommand(BaseCommand):
raise CommandError("Expect at least 2 arguments.")
self.labeltool.loadAnnotations(args[0])
present_filenames = {a["filename"] for a in self.labeltool.annotations()}
for filename in args[1:]:
rel_filename = filename
try:
@@ -106,6 +112,10 @@ class AppendFilesCommand(BaseCommand):
except:
pass
if rel_filename in present_filenames:
logger.info("Not adding file again: %s" % rel_filename)
continue
_, ext = os.path.splitext(rel_filename)
if (not options['image'] and ext.lower() in self.video_extensions) or options['video']:
logger.debug("Adding video file: %s" % rel_filename)
@@ -113,6 +123,7 @@ class AppendFilesCommand(BaseCommand):
else:
logger.debug("Adding image file: %s" % rel_filename)
item = self.labeltool.addImageFile(rel_filename)
present_filenames.add(rel_filename)
if options['unlabeled']:
item.setUnlabeled(True)
@@ -122,10 +133,9 @@ class AppendFilesCommand(BaseCommand):
class MergeFilesCommand(BaseCommand):
"""
Merge annotations of two label files and create a new one from it.
Currently, only video annotation files are supported.
If both input files have annotations for the same frame number, the result
will contain the union of both annotations.
Output format will be determined by the file suffix of output.
"""
args = '<labelfile 1> <labelfile 2> <output>'
@@ -149,59 +159,51 @@ class MergeFilesCommand(BaseCommand):
an3 = self.merge_annotations(an1, an2)
logger.debug("saving annotations to %s" % output)
containerOut = self.labeltool._container_factory.create(output)
containerOut.save(an3, output)
out_container = self.labeltool._container_factory.create(output)
out_container.save(an3, output)
def merge_annotations(self, an1, an2):
# I could also think of an implementation merging an1 and an2, and flattening the lists of lists
# that are obtained
assert(len(an1) == 1 and len(an2) == 1)
d1 = an1[0]
d2 = an2[0]
if(d1['class'] != 'video'):
raise NotImplemented('mergefiles: Currently, only annotation files from video can be merged.')
if(d2['class'] != d1['class']):
raise CommandError("mergefiles: Both annotation files have to be of the same type (%s vs. %s)." % (d1['class'], d2['class']))
if(d1['filename'] != d2['filename']):
raise CommandError('mergefiles: Both annotation files must annotate the same video file.')
def merge_annotations(self, an1, an2, match_key='filename'):
"""This merges all annotations from an2 into an1."""
for item in an2:
matching_items = [it1 for it1 in an1 if
it1['class'] == item['class'] and
it1[match_key] == item[match_key]]
# If we can't find a match, we just append the item to an1.
if len(matching_items) == 0:
an1.append(item)
continue
# We found at least one match, just take the first.
# But put out a warning if there were multiple possible matches.
if len(matching_items) > 1:
logger.warning('Found %d possible matches for %s',
len(matching_items), item['filename'])
match_item = matching_items[0]
# Update the keys first.
for key, value in item.iteritems():
if key == 'annotations':
continue
if match_item['class'] == 'video' and key == 'frames':
continue
if key in match_item and match_item[key] != value:
logger.warning('found matching key %s, but values differ: %s <-> %s',
key, str(value), str(value))
continue
match_item[key] = value
# Merge frames.
if match_item['class'] == 'video':
match_item['frames'] = self.merge_annotations(match_item['frames'], item['frames'], 'num')
match_item['frames'].sort(key=itemgetter('num'))
# Merge annotations.
if 'annotations' in match_item:
match_item['annotations'].extend(item.get('annotations', []))
assert(d1['frames'] != None)
assert(d2['frames'] != None)
frames1 = d1['frames']
frames2 = d2['frames']
# collect list of nums
frameNums1 = set()
for frame in frames1:
frameNums1.add(frame['num'])
# make frames2 accessible by frame number
frameNums2 = dict()
for frame in frames2:
frameNums2[frame['num']] = frame
for frame in frames1:
num = frame['num']
# look for frame with same timestamp in frames2
if num in frameNums2:
# update annotations
frame['annotations'].extend(frameNums2[num]['annotations'])
# append frames with nums only in frames2 to frames1
numsOnlyIn2 = set(frameNums2.keys()) - frameNums1
for key in numsOnlyIn2:
frames1.append(frameNums2[key])
from operator import itemgetter
frames1.sort(key=itemgetter('num'))
return an1
@@ -222,10 +224,12 @@ def _make_writeable(filename):
# command dictionary str -> Command
_commands = {}
def register_command(name, command):
global _commands
_commands[name] = command
def get_commands():
global _commands
return _commands
+4 -1
View File
@@ -1,15 +1,18 @@
"""
Label tool exception classes.
Sloth exception classes.
"""
class ImproperlyConfigured(Exception):
"""There is an error in the configuration."""
pass
class NotImplementedException(Exception):
"""This function/method/class has not been implemented yet."""
pass
class InvalidArgumentException(Exception):
"""The argument is invalid."""
pass
+32 -34
View File
@@ -1,10 +1,8 @@
#!/usr/bin/python
"""
This is the core labeltool module.
"""
import sys, os
import os
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from sloth.annotations.model import *
@@ -16,13 +14,15 @@ from sloth import VERSION
from sloth.core.commands import get_commands
from sloth.gui import MainWindow
import logging
LOG = logging.getLogger(__name__)
try:
import okapy.videoio as okv
except:
except ImportError:
pass
class LabelTool(QObject):
"""
This is the main label tool object. It stores the state of the tool, i.e.
@@ -43,9 +43,9 @@ class LabelTool(QObject):
"Type '%s help <subcommand>' for help on a specific subcommand.\n\n"
# Signals
statusMessage = pyqtSignal(str)
annotationsLoaded = pyqtSignal()
pluginLoaded = pyqtSignal(QAction)
statusMessage = pyqtSignal(str)
annotationsLoaded = pyqtSignal()
pluginLoaded = pyqtSignal(QAction)
# This still emits a QModelIndex, because Qt cannot handle emiting
# a derived class instead of a base class, i.e. ImageFileModelItem
# instead of ModelItem
@@ -105,7 +105,8 @@ class LabelTool(QObject):
# Initialize logging
loglevel = (logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG)[int(options.verbosity)]
logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)-8s %(name)-30s %(message)s') #, datefmt='%H:%M:%S.%m')
logging.basicConfig(level=loglevel,
format='%(asctime)s %(levelname)-8s %(name)-30s %(message)s') #, datefmt='%H:%M:%S.%m')
# Disable PyQt log messages
logging.getLogger("PyQt4").setLevel(logging.WARNING)
@@ -170,7 +171,6 @@ class LabelTool(QObject):
else:
self.clearAnnotations()
def fetch_command(self, subcommand):
"""
Tries to fetch the given subcommand, printing a message with the
@@ -179,8 +179,8 @@ class LabelTool(QObject):
try:
app_name = get_commands()[subcommand]
except KeyError:
sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % \
(subcommand, self.prog_name))
sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" %
(subcommand, self.prog_name))
sys.exit(1)
if isinstance(app_name, BaseCommand):
# If the command is already loaded, use it directly.
@@ -221,13 +221,13 @@ class LabelTool(QObject):
### Annoation file handling
###___________________________________________________________________________________________
def loadAnnotations(self, fname, handleErrors=True):
fname = str(fname) # convert from QString
fname = str(fname) # convert from QString
try:
self._container = self._container_factory.create(fname)
self._model = AnnotationModel(self._container.load(fname))
msg = "Successfully loaded %s (%d files, %d annotations)" % \
(fname, self._model.root().numFiles(), self._model.root().numAnnotations())
(fname, self._model.root().numFiles(), self._model.root().numAnnotations())
except Exception as e:
if handleErrors:
msg = "Error: Loading failed (%s)" % str(e)
@@ -247,7 +247,6 @@ class LabelTool(QObject):
try:
# create new container if the filename is different
if fname != self._container.filename():
# TODO: skip if it is the same class
self._container = self._container_factory.create(fname)
# Get annotations dict
@@ -256,7 +255,7 @@ class LabelTool(QObject):
self._container.save(ann, fname)
#self._model.writeback() # write back changes that are cached in the model itself, e.g. mask updates
msg = "Successfully saved %s (%d files, %d annotations)" % \
(fname, self._model.root().numFiles(), self._model.root().numAnnotations())
(fname, self._model.root().numFiles(), self._model.root().numAnnotations())
success = True
self._model.setDirty(False)
except Exception as e:
@@ -300,7 +299,7 @@ class LabelTool(QObject):
else:
next_image = next(self._model.iterator(ImageModelItem))
if next_image is not None:
next_image = next_image.getNextSibling(step-1)
next_image = next_image.getNextSibling(step - 1)
if next_image is not None:
self.setCurrentImage(next_image)
@@ -349,18 +348,18 @@ class LabelTool(QObject):
def addImageFile(self, fname):
fileitem = {
'filename': fname,
'class': 'image',
'annotations': [ ],
}
'filename': fname,
'class': 'image',
'annotations': [],
}
return self._model._root.appendFileItem(fileitem)
def addVideoFile(self, fname):
fileitem = {
'filename': fname,
'class': 'video',
'frames': [ ],
}
'filename': fname,
'class': 'video',
'frames': [],
}
# FIXME: OKAPI should provide a method to get all timestamps at once
# FIXME: Some dialog should be displayed, telling the user that the
@@ -376,17 +375,17 @@ class LabelTool(QObject):
LOG.debug("Adding %d frames" % len(timestamps))
fileitem['frames'] = [{'annotations': [], 'num': i,
'timestamp': ts, 'class': 'frame'}
for i, ts in enumerate(timestamps)]
for i, ts in enumerate(timestamps)]
else:
i = 0
while video.getNextFrame():
LOG.debug("Adding frame %d" % i)
ts = video.getTimestamp()
frame = { 'annotations': [],
'num': i,
'timestamp': ts,
'class': 'frame'
}
frame = {'annotations': [],
'num': i,
'timestamp': ts,
'class': 'frame'
}
fileitem['frames'].append(frame)
i += 1
@@ -429,7 +428,7 @@ class LabelTool(QObject):
def selectPreviousAnnotation(self):
if self._mainwindow is not None:
return self._mainwindow.scene.selectNextItem(reverse=True)
def selectAllAnnotations(self):
if self._mainwindow is not None:
return self._mainwindow.scene.selectAllItems()
@@ -449,5 +448,4 @@ class LabelTool(QObject):
if self._mainwindow is None:
return None
else:
return self._mainwindow.treeview
return self._mainwindow.treeview
+2 -1
View File
@@ -1,5 +1,6 @@
import importlib
from sloth.core import exceptions
from sloth.utils import importlib
def import_callable(module_path_name):
"""
+24 -8
View File
@@ -1,6 +1,4 @@
"""This is the AnnotationScene module"""
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from sloth.items import *
from sloth.core.exceptions import InvalidArgumentException
from sloth.annotations.model import AnnotationModelItem
@@ -9,19 +7,20 @@ from sloth.conf import config
import logging
LOG = logging.getLogger(__name__)
class AnnotationScene(QGraphicsScene):
mousePositionChanged = pyqtSignal(float, float)
def __init__(self, labeltool, items=None, inserters=None, parent=None):
super(AnnotationScene, self).__init__(parent)
self._model = None
self._model = None
self._image_item = None
self._inserter = None
self._inserter = None
self._scene_item = None
self._message = ""
self._labeltool = labeltool
self._message = ""
self._labeltool = labeltool
self._itemfactory = Factory(items)
self._itemfactory = Factory(items)
self._inserterfactory = Factory(inserters)
try:
@@ -143,10 +142,14 @@ class AnnotationScene(QGraphicsScene):
self._labeltool.currentImageChanged.connect(inserter.imageChange)
self._inserter = inserter
LOG.debug("Created inserter for class '%s' with default properties '%s'" % (label_class, default_properties))
# Change cursor to cross
self.views()[0].viewport().setCursor(Qt.CrossCursor)
def onInsertionModeEnded(self):
if self._inserter is not None:
self._inserter.abort()
self.views()[0].viewport().setCursor(Qt.ArrowCursor)
#
# common methods
@@ -185,6 +188,19 @@ class AnnotationScene(QGraphicsScene):
# selection mode
QGraphicsScene.mousePressEvent(self, event)
def mouseDoubleClickEvent(self, event):
LOG.debug("mouseDoubleClickEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos()))
if self._inserter is not None:
if not self.sceneRect().contains(event.scenePos()) and \
not self._inserter.allowOutOfSceneEvents():
# ignore events outside the scene rect
return
# insert mode
self._inserter.mouseDoubleClickEvent(event, self._image_item)
else:
# selection mode
QGraphicsScene.mouseDoubleClickEvent(self, event)
def mouseReleaseEvent(self, event):
LOG.debug("mouseReleaseEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos()))
if self._inserter is not None:
@@ -255,7 +271,7 @@ class AnnotationScene(QGraphicsScene):
found = False
items = [item for item in self.items()
if item.flags() & QGraphicsItem.ItemIsSelectable] * 2
if item.flags() & QGraphicsItem.ItemIsSelectable] * 2
if reverse:
items.reverse()
+11 -4
View File
@@ -3,17 +3,21 @@ from PyQt4.QtGui import *
from PyQt4.QtCore import *
from sloth.gui.floatinglayout import FloatingLayout
import logging
LOG = logging.getLogger(__name__)
def unique_list(seq):
seen = {}
result = []
for item in seq:
if item in seen: continue
if item in seen:
continue
seen[item] = 1
result.append(item)
return result
class ButtonListWidget(QGroupBox):
selectionChanged = pyqtSignal(object)
@@ -141,9 +145,9 @@ class ButtonArea(QWidget):
else:
button_list.hide()
def add_label(self, label_name, properties = {}):
def add_label(self, label_name, properties=None):
self.label_names.append(label_name)
self.label_properties[label_name] = properties
self.label_properties[label_name] = properties or {}
for key, value in properties.items():
if key in self.properties:
self.properties[key] = unique_list(self.properties[key] + value)
@@ -183,7 +187,7 @@ class ButtonArea(QWidget):
def clickedLabelButton(self, label_name):
#button = self.get_checked_label_button()
#print button
if label_name != None:
if label_name is not None:
LOG.debug("ButtonArea: %s" % label_name)
self.show_only_label_properties(label_name)
else:
@@ -196,8 +200,10 @@ class ButtonArea(QWidget):
if button is not None:
self.label_button_list.toggleChecked(button)
def main():
from conf import config
config.update("example_config")
app = QApplication(sys.argv)
@@ -206,6 +212,7 @@ def main():
return app.exec_()
if __name__ == '__main__':
sys.exit(main())
+2 -1
View File
@@ -1,7 +1,7 @@
import sys, os
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class Label(QLabel):
def __init__(self, *args, **kwargs):
@@ -12,6 +12,7 @@ class Label(QLabel):
menu.addActions(self.actions())
menu.exec_(ev.globalPos())
class ControlButtonWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
+1
View File
@@ -1,6 +1,7 @@
from PyQt4.QtCore import Qt, QRect, QSize, QPoint
from PyQt4.QtGui import QLayout, QSizePolicy, QWidgetItem
class FloatingLayout(QLayout):
def __init__(self, parent=None):
QLayout.__init__(self, parent)
+16 -12
View File
@@ -1,32 +1,37 @@
#!/usr/bin/python
import math
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtGui import *
try:
import okapy.videoio
except:
except ImportError:
pass
videos = []
scenes = []
class GraphicsView(QGraphicsView):
# Signals
scaleChanged = pyqtSignal(float)
focusIn = pyqtSignal()
focusIn = pyqtSignal()
def __init__(self, parent=None):
QGraphicsView.__init__(self, parent)
self.setDragMode(QGraphicsView.RubberBandDrag)
#self.setDragMode(QGraphicsView.ScrollHandDrag)
self.setMouseTracking(True)
self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing);
self.setStyleSheet("QFrame { border: 3px solid black }");
self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing)
self.setStyleSheet("QFrame { border: 3px solid black }")
self._active = False
self._pan = False
self._panStartX = -1
self._panStartY = -1
# Windows photo viewer like controls
self.setRenderHints(QPainter.Antialiasing|QPainter.SmoothPixmapTransform)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setDragMode(QGraphicsView.ScrollHandDrag)
def fitInView(self):
if self.scene() is None:
return
@@ -58,14 +63,14 @@ class GraphicsView(QGraphicsView):
if not self._active:
self._active = True
self.setFocus(Qt.OtherFocusReason)
self.setStyleSheet("QFrame { border: 3px solid red }");
self.setStyleSheet("QFrame { border: 3px solid red }")
self.update()
def deactivate(self):
if self._active:
self._active = False
self.clearFocus()
self.setStyleSheet("QFrame { border: 3px solid black }");
self.setStyleSheet("QFrame { border: 3px solid black }")
self.update()
def getMinScale(self):
@@ -125,7 +130,7 @@ class GraphicsView(QGraphicsView):
def mouseMoveEvent(self, event):
if self._pan:
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - (event.x() - self._panStartX))
self.verticalScrollBar() .setValue(self.verticalScrollBar().value() - (event.y() - self._panStartY));
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - (event.y() - self._panStartY))
self._panStartX = event.x()
self._panStartY = event.y()
event.accept()
@@ -138,7 +143,7 @@ class FrameViewer(QWidget):
activeSceneViewChanged = pyqtSignal(GraphicsView)
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
QWidget.__init__(self, parent)
def getActiveSceneView(self):
pass
@@ -188,7 +193,7 @@ class MultiFrameEqualViewer(FrameViewer):
n_cols = math.ceil(len(self.scenes) / n_rows)
self.layout = QGridLayout(self)
for i, scene_view in enumerate(self.scene_views):
self.layout.addWidget(scene_view, i/n_cols, i%n_cols)
self.layout.addWidget(scene_view, i/n_cols, i % n_cols)
self.setLayout(self.layout)
self.activateSceneView(0)
@@ -208,4 +213,3 @@ class MultiFrameEqualViewer(FrameViewer):
def getActiveSceneView(self):
return self.scene_views[self.active_scene_view]
+16 -8
View File
@@ -377,18 +377,26 @@ class MainWindow(QMainWindow):
image_types = [ '*.jpg', '*.bmp', '*.png', '*.pgm', '*.ppm', '*.ppm', '*.tif', '*.gif' ]
video_types = [ '*.mp4', '*.mpg', '*.mpeg', '*.avi', '*.mov', '*.vob' ]
format_str = ' '.join(image_types + video_types)
fname = QFileDialog.getOpenFileName(self, "%s - Add Media File" % APP_NAME, path, "Media files (%s)" % (format_str, ))
fnames = QFileDialog.getOpenFileNames(self, "%s - Add Media File" % APP_NAME, path, "Media files (%s)" % (format_str, ))
if len(str(fname)) == 0:
return
item = None
for fname in fnames:
if len(str(fname)) == 0:
continue
fname = str(fname)
fname = str(fname)
for pattern in image_types:
if fnmatch.fnmatch(fname, pattern):
return self.labeltool.addImageFile(fname)
if os.path.isabs(fname):
fname = os.path.relpath(fname, str(path))
return self.labeltool.addVideoFile(fname)
for pattern in image_types:
if fnmatch.fnmatch(fname, pattern):
item = self.labeltool.addImageFile(fname)
if item is None:
return self.labeltool.addVideoFile(fname)
return item
def onViewsLockedChanged(self, checked):
features = QDockWidget.AllDockWidgetFeatures
+24 -12
View File
@@ -1,24 +1,31 @@
import time
import logging
from PyQt4.QtCore import pyqtSignal, QSize, Qt
from PyQt4.QtGui import QWidget, QGroupBox, QVBoxLayout, QPushButton, QScrollArea, QLineEdit, QDoubleValidator, QIntValidator, QShortcut, QKeySequence
from sloth.core.exceptions import ImproperlyConfigured
from sloth.annotations.model import AnnotationModelItem
from sloth.gui.floatinglayout import FloatingLayout
from sloth.gui.utils import MyVBoxLayout
from sloth.utils.bind import bind
from PyQt4.QtCore import pyqtSignal, QSize, Qt
from PyQt4.QtGui import QWidget, QGroupBox, QVBoxLayout, QPushButton, QScrollArea, QLineEdit, QDoubleValidator, QIntValidator, QShortcut, QKeySequence
import time
import logging
LOG = logging.getLogger(__name__)
class AbstractAttributeHandler:
def defaults(self):
return {}
def updateValues(self, values):
pass
def setItems(self, items, showItemClasses=False):
pass
def autoAddEnabled(self):
return False
class AttributeHandlerFactory:
def create(self, attribute, values):
# Class attribute cannot be changed
@@ -36,17 +43,18 @@ class AttributeHandlerFactory:
# Else, we create our own default handler
return DefaultAttributeHandler(attribute, values)
class DefaultAttributeHandler(QGroupBox, AbstractAttributeHandler):
def __init__(self, attribute, values, parent=None):
QGroupBox.__init__(self, attribute, parent)
self._attribute = attribute
self._current_items = []
self._defaults = {}
self._inputField = None
self._attribute = attribute
self._current_items = []
self._defaults = {}
self._inputField = None
self._inputFieldType = None
self._insertIndex = -1
self._insertAtEnd = False
self._shortcuts = {}
self._insertIndex = -1
self._insertAtEnd = False
self._shortcuts = {}
# Setup GUI
self._layout = FloatingLayout()
@@ -251,11 +259,14 @@ class LabelEditor(QScrollArea):
for lc in self._label_classes:
attributes |= set(self._editor.getLabelClassAttributes(lc))
attributes = list(attributes)
attributes.sort()
for attr in attributes:
handler = self._editor.getHandler(attr)
if handler is not None:
if len(items) > 1:
valid_items = [item for item in items if attr in self._editor.getLabelClassAttributes(item['class'])]
valid_items = [item for item in items
if attr in self._editor.getLabelClassAttributes(item['class'])]
handler.setItems(valid_items, True)
else:
handler.setItems(items)
@@ -283,6 +294,7 @@ class LabelEditor(QScrollArea):
def insertionMode(self):
return self._insertion_mode
class PropertyEditor(QWidget):
# Signals
insertionModeStarted = pyqtSignal(str)
+2 -1
View File
@@ -1,5 +1,6 @@
from PyQt4.QtCore import QSize
from PyQt4.QtGui import QVBoxLayout
from PyQt4.QtGui import QVBoxLayout
# This is really really ugly, but the QDockWidget for some reason does not notice when
# its child widget becomes smaller...
+5 -5
View File
@@ -1,5 +1,6 @@
from sloth.core.utils import import_callable
class Factory:
"""
A generic factory for both items and inserters.
@@ -34,8 +35,8 @@ class Factory:
"""
_type = str(_type)
if _type in self._items and not replace:
raise Exception("Type %s already has an item: %s" % \
(_type, str(self._items[_type])))
raise Exception("Type %s already has an item: %s" %
(_type, str(self._items[_type])))
else:
if type(item) == str:
item = import_callable(item)
@@ -51,10 +52,10 @@ class Factory:
Type for which the mapping should be removed. If None, all
mappings will be removed.
"""
_type = str(_type)
if _type is None:
self._items = {}
self._items = {}
else:
_type = str(_type)
if _type in self._items:
del self._items[_type]
@@ -82,4 +83,3 @@ class Factory:
if item is None:
return None
return item(*args, **kwargs)
+118 -27
View File
@@ -1,6 +1,6 @@
import math
from PyQt4.QtGui import *
from PyQt4.Qt import *
import math
class ItemInserter(QObject):
@@ -11,14 +11,12 @@ class ItemInserter(QObject):
annotationFinished = pyqtSignal()
inserterFinished = pyqtSignal()
def __init__(self, labeltool, scene, default_properties={},
def __init__(self, labeltool, scene, default_properties=None,
prefix="", commit=True):
QObject.__init__(self)
self._labeltool = labeltool
self._scene = scene
self._default_properties = default_properties
if self._default_properties is None:
self._default_properties = {}
self._default_properties = default_properties or {}
self._prefix = prefix
self._ann = {}
self._commit = commit
@@ -40,6 +38,9 @@ class ItemInserter(QObject):
def mousePressEvent(self, event, image_item):
event.accept()
def mouseDoubleClickEvent(self, event, image_item):
event.accept()
def mouseReleaseEvent(self, event, image_item):
event.accept()
@@ -79,13 +80,21 @@ class PointItemInserter(ItemInserter):
class RectItemInserter(ItemInserter):
def __init__(self, labeltool, scene, default_properties={},
def __init__(self, labeltool, scene, default_properties=None,
prefix="", commit=True):
ItemInserter.__init__(self, labeltool, scene, default_properties,
prefix, commit)
self._aiming = True
self._helpLines = None
self._helpLinesPen = QPen(Qt.green, 2, Qt.DashLine)
self._init_pos = None
def mousePressEvent(self, event, image_item):
self._aiming = False
if self._helpLines is not None:
self._scene.removeItem(self._helpLines)
self._helpLines = None
pos = event.scenePos()
self._init_pos = pos
self._item = QGraphicsRectItem(QRectF(pos.x(), pos.y(), 0, 0))
@@ -94,10 +103,28 @@ class RectItemInserter(ItemInserter):
event.accept()
def mouseMoveEvent(self, event, image_item):
if self._item is not None:
assert self._init_pos is not None
rect = QRectF(self._init_pos, event.scenePos()).normalized()
self._item.setRect(rect)
if self._aiming:
if self._helpLines is not None:
self._scene.removeItem(self._helpLines)
self._helpLines = QGraphicsItemGroup()
group = self._helpLines
verticalHelpLine = QGraphicsLineItem(event.scenePos().x(), 0, event.scenePos().x(), self._scene.height())
horizontalHelpLine = QGraphicsLineItem(0, event.scenePos().y(), self._scene.width(), event.scenePos().y())
horizontalHelpLine.setPen(self._helpLinesPen)
verticalHelpLine.setPen(self._helpLinesPen)
group.addToGroup(verticalHelpLine);
group.addToGroup(horizontalHelpLine);
self._scene.addItem(self._helpLines)
else:
if self._item is not None:
assert self._init_pos is not None
rect = QRectF(self._init_pos, event.scenePos()).normalized()
self._item.setRect(rect)
event.accept()
@@ -118,12 +145,18 @@ class RectItemInserter(ItemInserter):
self._init_pos = None
self._item = None
self._aiming = True
self._scene.views()[0].viewport().setCursor(Qt.CrossCursor)
event.accept()
def allowOutOfSceneEvents(self):
return True
def abort(self):
if self._helpLines is not None:
self._scene.removeItem(self._helpLines)
self._helpLines = None
if self._item is not None:
self._scene.removeItem(self._item)
self._item = None
@@ -132,7 +165,7 @@ class RectItemInserter(ItemInserter):
class FixedRatioRectItemInserter(RectItemInserter):
def __init__(self, labeltool, scene, default_properties={},
def __init__(self, labeltool, scene, default_properties=None,
prefix="", commit=True):
RectItemInserter.__init__(self, labeltool, scene, default_properties,
prefix, commit)
@@ -159,7 +192,7 @@ class FixedRatioRectItemInserter(RectItemInserter):
class SequenceItemInserter(ItemInserter):
inserters = []
def __init__(self, labeltool, scene, default_properties={},
def __init__(self, labeltool, scene, default_properties=None,
prefix="", commit=True):
ItemInserter.__init__(self, labeltool, scene, default_properties,
prefix, commit)
@@ -260,8 +293,7 @@ class BBoxFaceInserter(SequenceItemInserter):
if event.key() == Qt.Key_O and self._state > 0:
self.toggleOccludedForCurrentInserter()
return
elif Qt.Key_0 <= event.key() <= Qt.Key_9 or \
Qt.Key_A <= event.key() <= Qt.Key_Z:
elif Qt.Key_0 <= event.key() <= Qt.Key_9 or Qt.Key_A <= event.key() <= Qt.Key_Z:
if Qt.Key_0 <= event.key() <= Qt.Key_9:
self._ann['id'] = int(str(event.text()))
else:
@@ -324,31 +356,90 @@ class NPointFaceInserter(SequenceItemInserter):
"Now at: " + self.inserters[self._state][2])
# TODO
class PolygonItemInserter(ItemInserter):
def __init__(self, scene, mode=None):
ItemInserter.__init__(self, scene, mode)
self._current_item = None
def __init__(self, labeltool, scene, default_properties=None,
prefix="", commit=True):
ItemInserter.__init__(self, labeltool, scene, default_properties,
prefix, commit)
self._item = None
def _removeLastPointAndFinish(self, image_item):
polygon = self._item.polygon()
polygon.remove(polygon.size()-1)
assert polygon.size() > 0
self._item.setPolygon(polygon)
self._updateAnnotation()
if self._commit:
image_item.addAnnotation(self._ann)
self._scene.removeItem(self._item)
self.annotationFinished.emit()
self._item = None
self._scene.clearMessage()
self.inserterFinished.emit()
def mousePressEvent(self, event, image_item):
pos = event.scenePos()
if self._current_item is None:
if self._item is None:
item = QGraphicsPolygonItem(QPolygonF([pos]))
self._current_item = item
self._item = item
self._item.setPen(self.pen())
self._scene.addItem(item)
else:
polygon = self._current_item.polygon()
polygon.append(pos)
self._current_item.setPolygon(polygon)
self._scene.setMessage("Press Enter to finish the polygon.")
polygon = self._item.polygon()
polygon.append(pos)
self._item.setPolygon(polygon)
event.accept()
def mouseDoubleClickEvent(self, event, image_item):
"""Finish the polygon when the user double clicks."""
# No need to add the position of the click, as a single mouse
# press event added the point already.
# Even then, the last point of the polygon is duplicate as it would be
# shortly after a single mouse press. At this point, we want to throw it
# away.
self._removeLastPointAndFinish(image_item)
event.accept()
def mouseMoveEvent(self, event, image_item):
if self._current_item is not None:
if self._item is not None:
pos = event.scenePos()
polygon = self._current_item.polygon()
polygon = self._item.polygon()
assert polygon.size() > 0
polygon[-1] = pos
self._current_item.setPolygon(polygon)
self._item.setPolygon(polygon)
event.accept()
def keyPressEvent(self, event, image_item):
"""
When the user presses Enter, the polygon is finished.
"""
if event.key() == Qt.Key_Return and self._item is not None:
# The last point of the polygon is the point the user would add
# to the polygon when pressing the mouse button. At this point,
# we want to throw it away.
self._removeLastPointAndFinish(image_item)
def abort(self):
if self._item is not None:
self._scene.removeItem(self._item)
self._item = None
self._scene.clearMessage()
ItemInserter.abort(self)
def _updateAnnotation(self):
polygon = self._item.polygon()
self._ann.update({self._prefix + 'xn':
";".join([str(p.x()) for p in polygon]),
self._prefix + 'yn':
";".join([str(p.y()) for p in polygon])})
self._ann.update(self._default_properties)
+149 -31
View File
@@ -1,9 +1,10 @@
from PyQt4.QtGui import *
import logging
from PyQt4.Qt import *
import logging
LOG = logging.getLogger(__name__)
# convenience functions for creating hotkey functions
class cycleValue:
def __init__(self, itemkey, valuelist):
@@ -34,9 +35,11 @@ class cycleValue:
item._model_item[key] = self.valuelist[nextindex]
item.dataChanged()
def setValue(itemkey, newvalue):
return lambda self: _setValue(self, itemkey, newvalue)
def _setValue(self, itemkey, newvalue):
if isinstance(itemkey, IgnorePrefix):
itemkey = itemkey.value
@@ -58,6 +61,7 @@ class IgnorePrefix:
def __str__(self):
return self.value
class BaseItem(QAbstractGraphicsShapeItem):
"""
Base class for visualization items.
@@ -70,12 +74,6 @@ class BaseItem(QAbstractGraphicsShapeItem):
def __init__(self, model_item=None, prefix="", parent=None):
"""
Creates a visualization item.
Parameters
==========
index :
data :
parent :
"""
QAbstractGraphicsShapeItem.__init__(self, parent)
self.setFlags(QGraphicsItem.ItemIsSelectable |
@@ -100,7 +98,8 @@ class BaseItem(QAbstractGraphicsShapeItem):
self._valid = True
if len(self.cycleValuesOnKeypress) > 0:
logging.warning("cycleValueOnKeypress is deprecated and will be removed in the future. Set BaseItem.hotkeys instead with cycleValue()")
logging.warning("cycleValueOnKeypress is deprecated and will be removed in the future. " +
"Set BaseItem.hotkeys instead with cycleValue()")
self.changeColor()
@@ -139,8 +138,9 @@ class BaseItem(QAbstractGraphicsShapeItem):
return self._prefix
def setPen(self, pen):
pen = QPen(pen) # convert to pen if argument is a QColor
QAbstractGraphicsShapeItem.setPen(self, pen)
self._text_item.setDefaultTextColor(pen)
self._text_item.setDefaultTextColor(pen.color())
def setText(self, text=""):
"""
@@ -166,12 +166,12 @@ class BaseItem(QAbstractGraphicsShapeItem):
"""
return self._text_bg_brush
def setAutoTextKeys(self, keys=[]):
def setAutoTextKeys(self, keys=None):
"""
Sets the keys for which the values from the annotations
are displayed automatically as text.
"""
self._auto_text_keys = keys
self._auto_text_keys = keys or []
self._text_item.setHtml(self._compile_text())
def autoTextKeys(self):
@@ -520,9 +520,6 @@ class MultiPointItem(BaseItem):
self._points = points
self.setPos(QPointF(0, 0))
def updateModel(self):
pass
def boundingRect(self):
xmin = min(self._points[::2])
xmax = max(self._points[::2])
@@ -651,21 +648,73 @@ class NPointFacePointItem(QGraphicsEllipseItem):
class NPointFaceItem(GroupItem):
items = [
# Eyebrows
(OccludablePointItem, "lboc"), # left eyebrow outer center
(OccludablePointItem, "lbu75"), # left eyebrow upper contour 75%
(OccludablePointItem, "lbu50"), # left eyebrow upper contour 50%
(OccludablePointItem, "lbu25"), # left eyebrow upper contour 25%
(OccludablePointItem, "lbic"), # left eyebrow inner center
(OccludablePointItem, "rbic"), # right eyebrow inner center
(OccludablePointItem, "rbu25"), # right eyebrow upper contour 25%
(OccludablePointItem, "rbu50"), # right eyebrow upper contour 50%
(OccludablePointItem, "rbu75"), # right eyebrow upper contour 75%
(OccludablePointItem, "rboc"), # right eyebrow outer center
# Eyes
(OccludablePointItem, "leoc"), # left eye outer center
(OccludablePointItem, "lec"), # left eye center
(OccludablePointItem, "leic"), # left eye inner center
(OccludablePointItem, "reic"), # right eye inner center
(OccludablePointItem, "rec"), # right eye center
(OccludablePointItem, "reoc"), # right eye outer center
(OccludablePointItem, "leoc"), # left eye outer center
(OccludablePointItem, "leu67"), # left eye upper countour 67%
(OccludablePointItem, "leu33"), # left eye upper countour 33%
(OccludablePointItem, "leic"), # left eye inner center
(OccludablePointItem, "lel33"), # left eye lower countour 33%
(OccludablePointItem, "lel67"), # left eye lower countour 67%
(OccludablePointItem, "reic"), # right eye inner center
(OccludablePointItem, "reu33"), # left eye upper countour 33%
(OccludablePointItem, "reu67"), # left eye upper countour 67%
(OccludablePointItem, "reoc"), # right eye outer center
(OccludablePointItem, "rel67"), # left eye lower countour 67%
(OccludablePointItem, "rel33"), # left eye lower countour 33%
(OccludablePointItem, "lec"), # left eye center
(OccludablePointItem, "rec"), # right eye center
# Nose
(OccludablePointItem, "nt"), # nose tip
(OccludablePointItem, "nl"), # nose left
(OccludablePointItem, "nr"), # nose right
(OccludablePointItem, "nc"), # nose center
(OccludablePointItem, "nr100"), # nose ridge 100%
(OccludablePointItem, "nr67"), # nose ridge 67%
(OccludablePointItem, "nr33"), # nose ridge 33%
(OccludablePointItem, "nt"), # nose tip
(OccludablePointItem, "nl"), # nose left
(OccludablePointItem, "nbl50"), # nose base left 50%
(OccludablePointItem, "nc"), # nose center
(OccludablePointItem, "nbr50"), # nose base right 50%
(OccludablePointItem, "nr"), # nose right
# Mouth
(OccludablePointItem, "mollc"),
(OccludablePointItem, "moltl67"),
(OccludablePointItem, "moltl33"),
(OccludablePointItem, "moltc"),
(OccludablePointItem, "moltr33"),
(OccludablePointItem, "moltr67"),
(OccludablePointItem, "molrc"),
(OccludablePointItem, "molbr67"),
(OccludablePointItem, "molbr33"),
(OccludablePointItem, "molbc"),
(OccludablePointItem, "molbl33"),
(OccludablePointItem, "molbl67"),
(OccludablePointItem, "millc"),
(OccludablePointItem, "miltl50"),
(OccludablePointItem, "miltc"),
(OccludablePointItem, "miltr50"),
(OccludablePointItem, "milrc"),
(OccludablePointItem, "milbr50"),
(OccludablePointItem, "milbc"),
(OccludablePointItem, "milbl50"),
# Mouth (legacy)
(OccludablePointItem, "ulc"), # upper lip center
(OccludablePointItem, "llc"), # lower lip center
(OccludablePointItem, "mc"), # mouth center
@@ -678,12 +727,6 @@ class NPointFaceItem(GroupItem):
# Chin
(OccludablePointItem, "cc"), # chin center
# Eyebrows
(OccludablePointItem, "lboc"), # left eyebrow outer center
(OccludablePointItem, "lbic"), # left eyebrow inner center
(OccludablePointItem, "rbic"), # right eyebrow inner center
(OccludablePointItem, "rboc"), # right eyebrow outer center
]
def __init__(self, model_item=None, prefix="", parent=None):
@@ -694,6 +737,8 @@ class NPointFaceItem(GroupItem):
if prefix + 'x' in self._model_item and \
prefix + 'y' in self._model_item:
child = callable_(self._model_item, prefix, self)
if hasattr(child, 'setToolTip'):
child.setToolTip(prefix)
self._children.append(child)
def boundingRect(self):
@@ -712,3 +757,76 @@ class NPointFaceItem(GroupItem):
pen.setStyle(Qt.DashLine)
painter.setPen(pen)
painter.drawRect(self.boundingRect())
class PolygonItem(BaseItem):
def __init__(self, model_item=None, prefix="", parent=None):
BaseItem.__init__(self, model_item, prefix, parent)
# Make it non-movable for now
self.setFlags(QGraphicsItem.ItemIsSelectable |
QGraphicsItem.ItemSendsGeometryChanges |
QGraphicsItem.ItemSendsScenePositionChanges)
self._polygon = None
self._updatePolygon(self._dataToPolygon(self._model_item))
LOG.debug("Constructed polygon %s for model item %s" %
(self._polygon, model_item))
def __call__(self, model_item=None, parent=None):
item = PolygonItem(model_item, parent)
item.setPen(self.pen())
item.setBrush(self.brush())
return item
def _dataToPolygon(self, model_item):
if model_item is None:
return QPolygonF()
try:
polygon = QPolygonF()
xn = [float(x) for x in model_item["xn"].split(";")]
yn = [float(y) for y in model_item["yn"].split(";")]
for x, y in zip(xn, yn):
polygon.append(QPointF(x, y))
return polygon
except KeyError as e:
LOG.debug("PolygonItem: Could not find expected key in item: "
+ str(e) + ". Check your config!")
self.setValid(False)
return QPolygonF()
def _updatePolygon(self, polygon):
if polygon == self._polygon:
return
self.prepareGeometryChange()
self._polygon = polygon
self.setPos(QPointF(0, 0))
def boundingRect(self):
xn = [p.x() for p in self._polygon]
yn = [p.y() for p in self._polygon]
xmin = min(xn)
xmax = max(xn)
ymin = min(yn)
ymax = max(yn)
return QRectF(xmin, ymin, xmax - xmin, ymax - ymin)
def paint(self, painter, option, widget=None):
BaseItem.paint(self, painter, option, widget)
pen = self.pen()
if self.isSelected():
pen.setStyle(Qt.DashLine)
painter.setPen(pen)
for k in range(-1, len(self._polygon)-1):
p1 = self._polygon[k]
p2 = self._polygon[k+1]
painter.drawLine(p1, p2)
def dataChange(self):
polygon = self._dataToPolygon(self._model_item)
self._updatePolygon(polygon)
+30 -6
View File
@@ -1,8 +1,11 @@
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import logging
LOG = logging.getLogger(__name__)
class CopyAnnotationsPlugin(QObject):
def __init__(self, labeltool, class_filter=None, frame_range=1, overlap_threshold=None, prefix=''):
QObject.__init__(self)
@@ -14,7 +17,7 @@ class CopyAnnotationsPlugin(QObject):
self._labeltool = labeltool
self._wnd = labeltool.mainWindow()
self._sc = QAction("Copy labels from previous image/frame", self._wnd)
self._sc = QAction("Copy labels from previous image/frame", self._wnd)
self._sc.triggered.connect(self.copy)
def copy(self):
@@ -53,11 +56,8 @@ class CopyAnnotationsPlugin(QObject):
for annotation in image_item.getAnnotations()['annotations']:
# check class filter
if self._class_filter is not None:
if 'class' not in annotation:
continue # do not copy
if annotation['class'] not in self._class_filter:
log
continue # do not copy
if annotation.get('class', None) not in self._class_filter:
continue # do not copy
annotations.append(annotation)
return annotations
@@ -85,3 +85,27 @@ class CopyAnnotationsPlugin(QObject):
def action(self):
return self._sc
class PolygonEnumeratorPlugin(QObject):
"""Enumerate the corners of polygons."""
def __init__(self, labeltool):
QObject.__init__(self)
# Decorate the paint() method with our enumerating paint:
from sloth.items import PolygonItem
oldpaint = PolygonItem.paint
def paint(self, painter, option, widget=None):
oldpaint(self, painter, option, widget)
for i, p in enumerate(self._polygon):
painter.drawText(p, str(i))
import functools
functools.update_wrapper(paint, oldpaint)
PolygonItem.paint = paint
def action(self):
return None
+10 -7
View File
@@ -1,10 +1,13 @@
from sloth.core.exceptions import NotImplementedException
from PyQt4.QtGui import QImage, qRgb
import numpy as np
import random
import colorsys
from PyQt4.QtGui import QImage, qRgb
from sloth.core.exceptions import NotImplementedException
gray_color_table = [qRgb(i, i, i) for i in range(256)]
def toQImage(im, copy=False):
if im is None:
return QImage()
@@ -17,12 +20,13 @@ def toQImage(im, copy=False):
elif len(im.shape) == 3:
if im.shape[2] == 3:
qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_RGB888);
qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_RGB888)
return qim.copy() if copy else qim
elif im.shape[2] == 4:
qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_ARGB32);
qim = QImage(im.data, im.shape[1], im.shape[0], im.strides[0], QImage.Format_ARGB32)
return qim.copy() if copy else qim
raise NotImplementedException('no conversion to QImage implemented for given image type (depth: %s, shape: %s)' % (im.dtype, im.shape))
raise NotImplementedException('no conversion to QImage implemented for given image type (depth: %s, shape: %s)' %
(im.dtype, im.shape))
def gen_colors(s=0.99, v=0.99, h=None, color_space='rgb', _golden_ratio_conjugate=0.618033988749895):
@@ -57,5 +61,4 @@ def gen_colors(s=0.99, v=0.99, h=None, color_space='rgb', _golden_ratio_conjugat
while True:
h += _golden_ratio_conjugate
h %= 1
yield cs_convert(h, s, v)
yield cs_convert(h, s, v)
+2
View File
@@ -1,12 +1,14 @@
def bind(fun, *args):
return lambda: fun(*args)
def compose_noargs(funs):
def tmp():
for f in funs:
f()
return tmp
def compose(funs):
def tmp(*args, **kwargs):
for f in funs:
-38
View File
@@ -1,38 +0,0 @@
# taken from django...
# Taken from Python 2.7 with permission from/by the original author.
import sys
def _resolve_name(name, package, level):
"""Return the absolute name of the module to be imported."""
if not hasattr(package, 'rindex'):
raise ValueError("'package' not set to a string")
dot = len(package)
for x in range(level, 1, -1):
try:
dot = package.rindex('.', 0, dot)
except ValueError:
raise ValueError("attempted relative import beyond top-level "
"package")
return "%s.%s" % (package[:dot], name)
def import_module(name, package=None):
"""Import a module.
The 'package' argument is required when performing a relative import. It
specifies the package to use as the anchor point from which to resolve the
relative import to an absolute import.
"""
if name.startswith('.'):
if not package:
raise TypeError("relative imports require the 'package' argument")
level = 0
for character in name:
if character != '.':
break
level += 1
name = _resolve_name(name[level:], package, level)
__import__(name)
return sys.modules[name]
+114
View File
@@ -0,0 +1,114 @@
from sloth.core.commands import *
def test_merge_command_same_images():
ann1 = [{'class': 'image', 'filename': 'abc.jpg',
'annotations': []},
{'class': 'image', 'filename': 'def.jpg',
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}]
ann2 = [{'class': 'image', 'filename': 'abc.jpg',
'annotations': []},
{'class': 'image', 'filename': 'def.jpg',
'custom': 1,
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}]
mc = MergeFilesCommand()
ann3 = mc.merge_annotations(ann1, ann2)
assert len(ann3) == 2
assert ann3[1].get('custom') == 1
def test_merge_command_different_images():
ann1 = [{'class': 'image', 'filename': 'abc.jpg',
'annotations': []},
{'class': 'image', 'filename': 'def.jpg',
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}]
ann2 = [{'class': 'image', 'filename': 'abc1.jpg',
'annotations': []},
{'class': 'image', 'filename': 'def2.jpg',
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}]
mc = MergeFilesCommand()
ann3 = mc.merge_annotations(ann1, ann2)
assert len(ann3) == 4
def test_merge_command_empty():
ann1 = []
ann2 = [{'class': 'image', 'filename': 'abc1.jpg',
'annotations': []},
{'class': 'image', 'filename': 'def2.jpg',
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}]
mc = MergeFilesCommand()
ann3 = mc.merge_annotations(ann1, ann2)
assert len(ann3) == 2
def test_merge_command_different_same_videos():
ann1 = [{'class': 'image', 'filename': 'abc.jpg',
'annotations': []},
{'class': 'video', 'filename': 'def.avi',
'frames': [
{'class': 'frame', 'num': 10, 'timestamp': 100.0,
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]},
{'class': 'frame', 'num': 12, 'timestamp': 102.0,
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}
]}]
ann2 = [{'class': 'video', 'filename': 'def.avi',
'frames': [
{'class': 'frame', 'num': 10, 'timestamp': 100.0,
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]},
{'class': 'frame', 'num': 11, 'timestamp': 101.0,
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}]
},
{'class': 'image', 'filename': 'def2.jpg',
'annotations': [
{'class': 'point', 'x': 10, 'y': 100}
]}]
mc = MergeFilesCommand()
ann3 = mc.merge_annotations(ann1, ann2)
assert len(ann3) == 3
item = [it for it in ann3 if it['filename'] == 'def.avi'][0]
assert len(item['frames']) == 3
assert item['frames'][0]['num'] == 10
assert item['frames'][1]['num'] == 11
assert item['frames'][2]['num'] == 12
assert len(item['frames'][0]['annotations']) == 2
def test_merge_command_same_file(tmpdir):
class LabelToolMockup:
container_config = (('*', 'sloth.annotations.container.JsonContainer'),)
_container_factory = AnnotationContainerFactory(container_config)
mc = MergeFilesCommand()
mc.labeltool = LabelToolMockup()
output_fname = str(tmpdir.join('output.json'))
mc.handle('tests/data/example1_labels.json', 'tests/data/example1_labels.json', output_fname)
import json
merged_annotations = json.load(open(output_fname))
assert len(merged_annotations) == 2
assert len(merged_annotations[0]['annotations']) == 4
assert len(merged_annotations[1]['annotations']) == 2
+13 -19
View File
@@ -1,54 +1,45 @@
from sloth.annotations.container import *
class MockupContainer:
pass
def someFileAnnotations(i):
annotations = []
annotations.append({'type': 'rect',
'x': 10 * i,
'y': '20',
'w': '40',
'h': '60'})
annotations.append({'type': 'rect',
'x': '80',
'y': 20 * i,
'w': '40',
'h': '60'})
annotations = [{'type': 'rect', 'x': 10 * i, 'y': '20', 'w': '40', 'h': '60'},
{'type': 'rect', 'x': '80', 'y': 20 * i, 'w': '40', 'h': '60'}]
for k in range(i):
annotations.append({'type': 'point',
'x': 30 * k,
'y': 30 * k})
return annotations
def someAnnotations():
annotations = []
for i in range(5):
file = {
ann = {
'filename': 'file%d.png' % i,
'type': 'image',
'annotations': someFileAnnotations(i)
}
annotations.append(file)
annotations.append(ann)
return annotations
def common_container_test(filename, container):
original_anns = someAnnotations()
container.setAnnotations(original_anns)
assert len(container.annotations()) == len(original_anns)
container.save(filename)
container.save(original_anns, filename)
assert container.filename() == filename
assert os.path.exists(filename)
container.clear()
assert container.filename() is None
assert len(container.annotations()) == 0
container.load(filename)
assert container.filename() == filename
assert len(container.annotations()) == len(original_anns)
def test_import_callable():
containers = (('*', 'container_test.MockupContainer'),)
@@ -56,16 +47,19 @@ def test_import_callable():
item = factory.create('test')
assert isinstance(item, MockupContainer)
def test_PickleContainer(tmpdir):
filename = os.path.join(str(tmpdir), "test_PickleContainer.pickle")
container = PickleContainer()
common_container_test(filename, container)
def test_JsonContainer(tmpdir):
filename = os.path.join(str(tmpdir), "test_JsonContainer.json")
container = JsonContainer()
common_container_test(filename, container)
def test_YamlContainer(tmpdir):
filename = os.path.join(str(tmpdir), "test_YamlContainer.yaml")
container = YamlContainer()
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

+5 -5
View File
@@ -1,31 +1,31 @@
[
{
"type": "image",
"class": "image",
"annotations": [
{
"height": 60.0,
"width": 46.0,
"y": 105.0,
"x": 346.0,
"type": "rect"
"class": "rect"
},
{
"height": 58.0,
"width": 56.0,
"y": 119.0,
"x": 636.0,
"type": "rect"
"class": "rect"
}
],
"filename": "image1.jpg"
},
{
"type": "image",
"class": "image",
"annotations": [
{
"y": 155.0,
"x": 409.0,
"type": "point"
"class": "point"
}
],
"filename": "image2.jpg"
+17 -5
View File
@@ -1,9 +1,18 @@
import pytest
from sloth.items import Factory
class MockupRectItem: pass
class MockupPointItem: pass
class MockupPolygonItem: pass
class MockupRectItem:
pass
class MockupPointItem:
pass
class MockupPolygonItem:
pass
def _create_factory():
itemfactory = Factory({'point': MockupPointItem,
@@ -12,6 +21,7 @@ def _create_factory():
return itemfactory
def test_register():
itemfactory = _create_factory()
@@ -24,11 +34,13 @@ def test_register():
item = itemfactory.create('polygon2')
assert item is None
def test_register_fail():
itemfactory = _create_factory()
with pytest.raises(Exception):
itemfactory.register('rect', MockupRectItem)
def test_register_replace():
itemfactory = _create_factory()
@@ -36,6 +48,7 @@ def test_register_replace():
item = itemfactory.create('rect')
assert isinstance(item, MockupPolygonItem)
def test_clear():
itemfactory = _create_factory()
@@ -51,5 +64,4 @@ def test_clear():
assert isinstance(item, MockupPolygonItem)
itemfactory.clear()
assert itemfactory.create('point') is None
assert itemfactory.create('polygon') is None
assert itemfactory.create('polygon') is None