mirror of
https://github.com/wassname/sloth.git
synced 2026-09-11 12:41:38 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8696de1385 | ||
|
|
e8f7284bbb | ||
|
|
06feb9bfeb | ||
|
|
2b49500f64 | ||
|
|
4c39b89ca7 | ||
|
|
d47cd43948 | ||
|
|
1b0a6eaa24 | ||
|
|
46e80011c3 | ||
|
|
3bf65b9b26 | ||
|
|
af0829baf5 | ||
|
|
e6927cbc04 | ||
|
|
8d9a6af7bf | ||
|
|
8c711fa447 | ||
|
|
138c9abaa1 | ||
|
|
013a2a4bc3 | ||
|
|
bde36785b6 | ||
|
|
ebc40deb0a | ||
|
|
bad6e9ccda | ||
|
|
184791a795 | ||
|
|
ad05875f0b | ||
|
|
d6a66eb0e6 | ||
|
|
0dc837dc66 | ||
|
|
63e841ec12 | ||
|
|
26543b7a6d | ||
|
|
4feb87c72b | ||
|
|
e810cd09af | ||
|
|
e2d61febc3 | ||
|
|
8fbb44218d | ||
|
|
32a4bbce76 | ||
|
|
2579c4f7b0 | ||
|
|
e0fa0534d5 | ||
|
|
3358d6d579 | ||
|
|
0d63d1ea34 | ||
|
|
53a971b9ce | ||
|
|
5ef339d7a5 | ||
|
|
2afa69a2e4 | ||
|
|
19caf08ae1 | ||
|
|
6b5dc54188 | ||
|
|
cf0b79afed | ||
|
|
fa578410dc | ||
|
|
9eb3e888c5 | ||
|
|
86ba971a9c | ||
|
|
bcd5415b0a | ||
|
|
9b3ba3cb9f | ||
|
|
db1dc22036 | ||
|
|
957daf4c84 | ||
|
|
5d207711de | ||
|
|
1c0387a192 | ||
|
|
a811b9cccf | ||
|
|
9bb08470cc | ||
|
|
638c3db45d | ||
|
|
841ffee22d | ||
|
|
5e1e1ffd38 | ||
|
|
402ccb556e | ||
|
|
a78591a9cd | ||
|
|
2ca1f84156 | ||
|
|
16b5e9ca4a | ||
|
|
015b6f3afb | ||
|
|
5df2ff66e0 |
+1
-1
@@ -6,7 +6,7 @@ python:
|
|||||||
|
|
||||||
# install dependencies and sloth
|
# install dependencies and sloth
|
||||||
install:
|
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 Pillow
|
||||||
- pip install .
|
- pip install .
|
||||||
|
|
||||||
|
|||||||
@@ -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/
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
sloth
|
||||||
|
=====
|
||||||
|
|
||||||
|
[](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
@@ -4,7 +4,7 @@
|
|||||||
Concepts
|
Concepts
|
||||||
========
|
========
|
||||||
|
|
||||||
In this section, we will introduce some high-level concepts of Sloth.
|
We start by introducing some high-level concepts of Sloth.
|
||||||
|
|
||||||
|
|
||||||
Labels
|
Labels
|
||||||
@@ -15,25 +15,25 @@ can contain any number of labels. Each label is a set of key-value pairs,
|
|||||||
for example::
|
for example::
|
||||||
|
|
||||||
{
|
{
|
||||||
class: "rect",
|
"class": "rect",
|
||||||
id: "Martin",
|
"id": "Martin",
|
||||||
x: 10,
|
"x": 10,
|
||||||
y: 30,
|
"y": 30,
|
||||||
width: 40,
|
"width": 40,
|
||||||
height: 50,
|
"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).
|
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
|
We will later see, how the mapping between class and visualization can be customized and how custom visualizations
|
||||||
write your own visualizations.
|
can be added.
|
||||||
|
|
||||||
|
|
||||||
Label type conventions
|
Label type conventions
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
Sloth provides support for a range of standard shape labels (for example `rect`, `point`, `polygon` etc.).
|
Sloth provides support for a range of standard shape labels (for example rectangles, points and polygons).
|
||||||
In order for the label tool to correctly visualize these labels, the labels have to follow
|
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.
|
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.
|
The following simple geometric classes are supported out of the box, i.e.
|
||||||
@@ -76,7 +76,7 @@ Polygon
|
|||||||
User defined labels
|
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
|
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.
|
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
|
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",
|
"class": "point",
|
||||||
"type": "left_eye",
|
"type": "left_eye",
|
||||||
x: 50, y: 40,
|
"x": 50, "y": 40,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"class": "point",
|
"class": "point",
|
||||||
"type": "right_eye",
|
"type": "right_eye",
|
||||||
x: 70, y: 40,
|
"x": 70, "y": 40,
|
||||||
}
|
}
|
||||||
|
|
||||||
Of course, you can also create new classes::
|
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
|
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
|
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
|
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.
|
configuration how to display this label class. See section :doc:`configuration` on how to do that.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -40,17 +40,17 @@ source_suffix = '.rst'
|
|||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Labeltool'
|
project = 'sloth'
|
||||||
copyright = '2011, cv:hci lab, Institute for Anthropomatics, Karlsruhe Institute of Technology'
|
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
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.1'
|
version = '1.0'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.1'
|
release = '1.0'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
@@ -180,7 +180,7 @@ todo_include_todos = True
|
|||||||
# Grouping the document tree into LaTeX files. List of tuples
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
('index', 'Labeltool.tex', 'Labeltool Documentation',
|
('index', 'sloth.tex', 'Sloth Documentation',
|
||||||
'cv:hci lab, Institute for Anthropomatics, Karlsruhe Institute of Technology', 'manual'),
|
'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
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
('index', 'labeltool', 'Labeltool Documentation',
|
('index', 'sloth', 'Sloth Documentation',
|
||||||
['cv:hci lab'], 1)
|
['cv:hci lab'], 1)
|
||||||
]
|
]
|
||||||
|
|||||||
+13
-22
@@ -59,6 +59,7 @@ different keys of the dictionary in detail:
|
|||||||
current state of the button area.
|
current state of the button area.
|
||||||
|
|
||||||
* ``attributes`` has three functions:
|
* ``attributes`` has three functions:
|
||||||
|
|
||||||
1. It defines how a new annotation can be initialized. Fixed
|
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
|
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
|
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
|
* the value for one of keys matches and the other key is not present in
|
||||||
either ``attributes`` or the annotation.
|
either ``attributes`` or the annotation.
|
||||||
|
|
||||||
|
|
||||||
Note that the comma at the end of the first tuple is mandatory. Otherwise the
|
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
|
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
|
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
|
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
|
entry is the function that is called. The function should expect a single
|
||||||
parameter, the labeltool object. The optional third entry -- if present -- is
|
parameter, the labeltool object. The optional third entry -- if present -- is
|
||||||
expected to be a string describing the action.
|
expected to be a string describing the action.
|
||||||
@@ -113,11 +113,13 @@ CONTAINERS
|
|||||||
|
|
||||||
Default::
|
Default::
|
||||||
|
|
||||||
{
|
(
|
||||||
'*.txt': 'annotations.container.SimpleOneLinerTextContainer',
|
('*.json', 'sloth.annotations.container.JsonContainer'),
|
||||||
'*.yaml': 'annotations.container.YamlContainer',
|
('*.msgpack', 'sloth.annotations.container.MsgpackContainer'),
|
||||||
'*.pickle': 'annotations.container.PickleContainer',
|
('*.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
|
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
|
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
|
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
|
A list/tuple of classes implementing the sloth plugin interface. The
|
||||||
plugin that might do some preprocessing on an image, e.g. detect all faces and convert them into labels.
|
classes can either be given directly or their module path be specified as string.
|
||||||
|
By default, no plugins are active.
|
||||||
.. _SCENE_BACKGROUND:
|
|
||||||
|
|
||||||
SCENE_BACKGROUND
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Default::
|
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
|
Extending default values
|
||||||
|
|||||||
+14
-7
@@ -34,8 +34,8 @@ A container is expected to implement (at least) these five functions:
|
|||||||
``frame_number``.
|
``frame_number``.
|
||||||
|
|
||||||
The container base class ``AnnotationContainer`` provides default
|
The container base class ``AnnotationContainer`` provides default
|
||||||
implementations for all five function. It however deferes the
|
implementations for all five function. It however defers the
|
||||||
parsing and serialization of the labels from/to disk to the to functions
|
parsing and serialization of the labels from/to disk to the two functions
|
||||||
|
|
||||||
.. py:function:: parseFromFile(self, filename)
|
.. py:function:: parseFromFile(self, filename)
|
||||||
|
|
||||||
@@ -50,9 +50,9 @@ provide implementations for those two functions.
|
|||||||
Default Containers
|
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
|
``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
|
JsonContainer
|
||||||
-------------
|
-------------
|
||||||
@@ -62,7 +62,6 @@ Default pattern: ``*.json``
|
|||||||
Writes and reads annotations in JSON format (needs the python module ``json``
|
Writes and reads annotations in JSON format (needs the python module ``json``
|
||||||
to be installed).
|
to be installed).
|
||||||
|
|
||||||
|
|
||||||
YamlContainer
|
YamlContainer
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -71,6 +70,14 @@ Default pattern: ``*.yaml``
|
|||||||
Writes and reads annotations in YAML format (needs the python module ``yaml``
|
Writes and reads annotations in YAML format (needs the python module ``yaml``
|
||||||
to be installed).
|
to be installed).
|
||||||
|
|
||||||
|
MsgpackContainer
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Default pattern: ``*.msgpack``
|
||||||
|
|
||||||
|
Writes and reads annotations in Msgpack format (needs the python module ``msgpack``
|
||||||
|
to be installed).
|
||||||
|
|
||||||
PickleContainer
|
PickleContainer
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
@@ -87,8 +94,8 @@ Default pattern: ``*.sloth-init``
|
|||||||
A simple container that reads one image filename per line. No annotations
|
A simple container that reads one image filename per line. No annotations
|
||||||
are supported. This container can be used for example for initializing
|
are supported. This container can be used for example for initializing
|
||||||
a labeling session. After adding labels, another container should be
|
a labeling session. After adding labels, another container should be
|
||||||
used for saving though, otherwise the labels will be lost. (write support
|
used for saving though, otherwise the labels will be lost (write support
|
||||||
not implemented yet anyway)
|
is not implemented).
|
||||||
|
|
||||||
FeretContainer
|
FeretContainer
|
||||||
--------------
|
--------------
|
||||||
|
|||||||
+15
-12
@@ -117,7 +117,7 @@ Writing a custom configuration
|
|||||||
==============================
|
==============================
|
||||||
|
|
||||||
We already briefly touch the subject of configuration. Sloth can be easily
|
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
|
visualization items and container formats. All of this can be specified in the
|
||||||
configuration file. The configuration file is a python module where the
|
configuration file. The configuration file is a python module where the
|
||||||
module-level variables represent the settings. The most important variable is
|
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
|
``sloth.items.RectItem`` class is used, which will draw a rectangle as given by the
|
||||||
coordinates in the annotation. Sloth comes with several predefined
|
coordinates in the annotation. Sloth comes with several predefined
|
||||||
visualization classes, such as ``sloth.items.RectItem`` and
|
visualization classes, such as ``sloth.items.RectItem`` and
|
||||||
``sloth.items.PointItem`` (see :ref:`items` for a full list). However, it is
|
``sloth.items.PointItem`` (see :doc:`items` for a full list). However, it is
|
||||||
also very easy to define your own visualization class (see :ref:`items`).
|
also very easy to define your own visualization class (see :ref:`CUSTOM_ITEMS`).
|
||||||
|
|
||||||
* ``inserter`` specifies which class is responsible for creating new
|
* ``inserter`` specifies which class is responsible for creating new
|
||||||
annotations based on user input. When the user enters insert-mode with a
|
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.
|
user input and takes care of the creation of a new annotation.
|
||||||
|
|
||||||
* ``attributes`` has three purposes:
|
* ``attributes`` has three purposes:
|
||||||
|
|
||||||
1. It defines which key-values pairs are inserted into a new annotation directly.
|
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
|
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
|
of the values he wants to use for a new label. The current state is then
|
||||||
passed to the inserter.
|
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
|
sloth --config myconfig.py examples/example1_labels.json
|
||||||
|
|
||||||
You can now start labeling head locations and eye positions. You'll see that for each
|
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
|
depending on the chosen annotation -- you can either insert a rectangle (this is internally
|
||||||
done by the ``RectItemInserter``) or points (using the ``PointItemInserter``). For
|
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
|
There are more possibilities to configure the labels, which we have not touched here.
|
||||||
have not touched here. Refer to :ref:`LABELS` for the full documentation.
|
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
|
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
|
behaviour can be configured there as well, e.g. for supporting custom label formats (using
|
||||||
custom containers. See :doc:`Configuration <configuration>` for the full
|
custom containers). See :doc:`Configuration <configuration>` for the full
|
||||||
reference of all configuration options.
|
reference of all configuration options.
|
||||||
|
|
||||||
Next steps
|
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
|
how to write your own :doc:`visualization items <items>`, :doc:`custom inserters <inserters>` or
|
||||||
:doc:`custom label containers <containers>`.
|
:doc:`custom label containers <containers>`.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ Feedback
|
|||||||
|
|
||||||
*Please* provide feedback to us on this document and Sloth in general! We won't be able
|
*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
|
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
|
Contents
|
||||||
========
|
========
|
||||||
@@ -34,8 +34,8 @@ Contents
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
installation
|
installation
|
||||||
concepts
|
|
||||||
first_steps
|
first_steps
|
||||||
|
concepts
|
||||||
configuration
|
configuration
|
||||||
items
|
items
|
||||||
inserters
|
inserters
|
||||||
|
|||||||
+1
-1
@@ -7,4 +7,4 @@ Inserters
|
|||||||
Inserters are used for creating new labels interactively. When the users selects a label type in the button area,
|
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`).
|
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!
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ either `PIL`_ or okapy for image loading.
|
|||||||
.. _PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro
|
.. _PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro
|
||||||
.. _PIL: http://www.pythonware.com/products/pil/
|
.. _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::
|
<okapibuild>/python/ to the PYTHONPATH environment variable::
|
||||||
|
|
||||||
export PYTHONPATH=<okapibuild>/python/:$PYTHONPATH
|
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
|
.. _Python Sphinx: http://pypi.python.org/pypi/Sphinx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installing Sloth
|
Installing Sloth
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|||||||
+13
-3
@@ -23,10 +23,20 @@ The label tool comes with a few predefined visualization items:
|
|||||||
|
|
||||||
- ``items.PolygonItem``
|
- ``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.
|
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,
|
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
|
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
|
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.ItemSendsGeometryChanges | \
|
||||||
QGraphicsItem.ItemSendsScenePositionChanges)
|
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::
|
to inform the model about the modification::
|
||||||
|
|
||||||
def itemChange(self, change, value):
|
def itemChange(self, change, value):
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@@ -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.core import setup
|
||||||
from distutils.command.install import INSTALL_SCHEMES
|
from distutils.command.install import INSTALL_SCHEMES
|
||||||
import sloth
|
import sloth
|
||||||
|
|
||||||
|
|
||||||
# the following installation setup is based on django's setup.py
|
# the following installation setup is based on django's setup.py
|
||||||
def fullsplit(path, result=None):
|
def fullsplit(path, result=None):
|
||||||
"""
|
"""
|
||||||
@@ -49,7 +58,7 @@ setup(name='sloth',
|
|||||||
description='The Sloth Labeling Tool',
|
description='The Sloth Labeling Tool',
|
||||||
author='CV:HCI Research Group',
|
author='CV:HCI Research Group',
|
||||||
url='http://sloth.readthedocs.org/',
|
url='http://sloth.readthedocs.org/',
|
||||||
requires=['importlib', 'PyQt4',],
|
requires=['importlib', 'PyQt4', 'numpy'],
|
||||||
packages=packages,
|
packages=packages,
|
||||||
data_files=data_files,
|
data_files=data_files,
|
||||||
scripts=['sloth/bin/sloth']
|
scripts=['sloth/bin/sloth']
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -11,21 +10,21 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
except:
|
except ImportError:
|
||||||
import pickle
|
import pickle
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
except:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
except:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
import okapy
|
import okapy
|
||||||
import okapy.videoio as okv
|
import okapy.videoio as okv
|
||||||
_use_pil = False
|
_use_pil = False
|
||||||
except:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
_use_pil = True
|
_use_pil = True
|
||||||
@@ -82,10 +81,11 @@ class AnnotationContainer:
|
|||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
def filename(self):
|
def filename(self):
|
||||||
|
"""The current filename."""
|
||||||
return self._filename
|
return self._filename
|
||||||
|
|
||||||
def clear(self):
|
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._filename = None
|
||||||
self._video_cache = {}
|
self._video_cache = {}
|
||||||
|
|
||||||
@@ -204,24 +204,24 @@ class PickleContainer(AnnotationContainer):
|
|||||||
"""
|
"""
|
||||||
Overwritten to write pickle files.
|
Overwritten to write pickle files.
|
||||||
"""
|
"""
|
||||||
# TODO make all image filenames relative to the label file
|
|
||||||
f = open(fname, "wb")
|
f = open(fname, "wb")
|
||||||
pickle.dump(annotations, f)
|
pickle.dump(annotations, f)
|
||||||
|
|
||||||
|
|
||||||
class OkapiAnnotationContainer(AnnotationContainer):
|
class OkapiAnnotationContainer(AnnotationContainer):
|
||||||
"""
|
"""
|
||||||
Converts a AnnotationPropertiesMap to a dict
|
Simple container which writes the annotations to disk using okapy.AnnotationContainer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def convertAnnotationPropertiesMapToDict(self, properties):
|
def convertAnnotationPropertiesMapToDict(self, properties):
|
||||||
|
"""
|
||||||
|
Converts a AnnotationPropertiesMap to a dict
|
||||||
|
"""
|
||||||
propdict = {}
|
propdict = {}
|
||||||
for k, v in properties.items():
|
for k, v in properties.items():
|
||||||
propdict[k] = v
|
propdict[k] = v
|
||||||
return propdict
|
return propdict
|
||||||
|
|
||||||
"""
|
|
||||||
Simple container which writes the annotations to disk using okapy.AnnotationContainer.
|
|
||||||
"""
|
|
||||||
def parseFromFile(self, filename):
|
def parseFromFile(self, filename):
|
||||||
"""
|
"""
|
||||||
Overwritten to read Okapi::Annotation files.
|
Overwritten to read Okapi::Annotation files.
|
||||||
@@ -252,10 +252,10 @@ class OkapiAnnotationContainer(AnnotationContainer):
|
|||||||
|
|
||||||
return annotations
|
return annotations
|
||||||
|
|
||||||
"""
|
|
||||||
Converts a dict to a AnnotationPropertiesMap
|
|
||||||
"""
|
|
||||||
def convertDictToAnnotationPropertiesMap(self, annotation, propdict):
|
def convertDictToAnnotationPropertiesMap(self, annotation, propdict):
|
||||||
|
"""
|
||||||
|
Converts a dict to a AnnotationPropertiesMap
|
||||||
|
"""
|
||||||
for k, v in propdict.items():
|
for k, v in propdict.items():
|
||||||
if k != 'annotations' or k != 'frames':
|
if k != 'annotations' or k != 'frames':
|
||||||
annotation.set_str(k, str(v))
|
annotation.set_str(k, str(v))
|
||||||
@@ -292,7 +292,6 @@ class OkapiAnnotationContainer(AnnotationContainer):
|
|||||||
fileitem.frames().push_back(frameitem)
|
fileitem.frames().push_back(frameitem)
|
||||||
container.files().push_back(fileitem)
|
container.files().push_back(fileitem)
|
||||||
|
|
||||||
# TODO make all image filenames relative to the label file
|
|
||||||
container.WriteToFile(fname)
|
container.WriteToFile(fname)
|
||||||
|
|
||||||
|
|
||||||
@@ -312,9 +311,32 @@ class JsonContainer(AnnotationContainer):
|
|||||||
"""
|
"""
|
||||||
Overwritten to write JSON files.
|
Overwritten to write JSON files.
|
||||||
"""
|
"""
|
||||||
# TODO make all image filenames relative to the label file
|
|
||||||
f = open(fname, "w")
|
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):
|
class YamlContainer(AnnotationContainer):
|
||||||
@@ -333,7 +355,6 @@ class YamlContainer(AnnotationContainer):
|
|||||||
"""
|
"""
|
||||||
Overwritten to write YAML files.
|
Overwritten to write YAML files.
|
||||||
"""
|
"""
|
||||||
# TODO make all image filenames relative to the label file
|
|
||||||
f = open(fname, "w")
|
f = open(fname, "w")
|
||||||
yaml.dump(annotations, f)
|
yaml.dump(annotations, f)
|
||||||
|
|
||||||
@@ -351,8 +372,8 @@ class FileNameListContainer(AnnotationContainer):
|
|||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
fileitem = {
|
fileitem = {
|
||||||
'filename': line,
|
'filename': line,
|
||||||
'class': 'image',
|
'class': 'image',
|
||||||
'annotations': [],
|
'annotations': [],
|
||||||
}
|
}
|
||||||
annotations.append(fileitem)
|
annotations.append(fileitem)
|
||||||
@@ -360,8 +381,7 @@ class FileNameListContainer(AnnotationContainer):
|
|||||||
return annotations
|
return annotations
|
||||||
|
|
||||||
def serializeToFile(self, filename, annotations):
|
def serializeToFile(self, filename, annotations):
|
||||||
raise NotImplemented(
|
raise NotImplemented("FileNameListContainer.save() is not implemented yet.")
|
||||||
"FileNameListContainer.save() is not implemented yet.")
|
|
||||||
|
|
||||||
|
|
||||||
class FeretContainer(AnnotationContainer):
|
class FeretContainer(AnnotationContainer):
|
||||||
@@ -381,15 +401,12 @@ class FeretContainer(AnnotationContainer):
|
|||||||
fileitem = {
|
fileitem = {
|
||||||
'filename': s[0] + ".bmp",
|
'filename': s[0] + ".bmp",
|
||||||
'class': 'image',
|
'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)
|
annotations.append(fileitem)
|
||||||
|
|
||||||
return annotations
|
return annotations
|
||||||
@@ -398,8 +415,6 @@ class FeretContainer(AnnotationContainer):
|
|||||||
"""
|
"""
|
||||||
Not implemented yet.
|
Not implemented yet.
|
||||||
"""
|
"""
|
||||||
# TODO make sure the image paths are
|
|
||||||
# relative to the label file's directory
|
|
||||||
raise NotImplemented(
|
raise NotImplemented(
|
||||||
"FeretContainer.serializeToFile() is not implemented yet."
|
"FeretContainer.serializeToFile() is not implemented yet."
|
||||||
)
|
)
|
||||||
|
|||||||
+42
-23
@@ -1,23 +1,25 @@
|
|||||||
"""
|
"""
|
||||||
The annotationmodel module contains the classes for the AnnotationModel.
|
The annotationmodel module contains the classes for the AnnotationModel.
|
||||||
"""
|
"""
|
||||||
from PyQt4.QtGui import QTreeView, QItemSelection, QItemSelectionModel, QSortFilterProxyModel, QBrush
|
import os.path
|
||||||
from PyQt4.QtCore import QModelIndex, QAbstractItemModel, Qt, pyqtSignal, QVariant
|
|
||||||
import os.path, sys
|
|
||||||
import copy
|
|
||||||
from collections import MutableMapping
|
|
||||||
import time
|
import time
|
||||||
import logging
|
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__)
|
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:
|
class ModelItem:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._loaded = True
|
self._loaded = True
|
||||||
self._model = None
|
self._model = None
|
||||||
self._parent = None
|
self._parent = None
|
||||||
self._row = -1
|
self._row = -1
|
||||||
if not hasattr(self, "_children"):
|
if not hasattr(self, "_children"):
|
||||||
self._children = []
|
self._children = []
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ class ModelItem:
|
|||||||
|
|
||||||
for i, item in enumerate(items):
|
for i, item in enumerate(items):
|
||||||
item._parent = self
|
item._parent = self
|
||||||
item._row = next_row + i
|
item._row = next_row + i
|
||||||
self._children.append(item)
|
self._children.append(item)
|
||||||
|
|
||||||
if self._model is not None:
|
if self._model is not None:
|
||||||
@@ -219,11 +221,12 @@ class ModelItem:
|
|||||||
def getColor(self):
|
def getColor(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class RootModelItem(ModelItem):
|
class RootModelItem(ModelItem):
|
||||||
def __init__(self, model, files):
|
def __init__(self, model, files):
|
||||||
ModelItem.__init__(self)
|
ModelItem.__init__(self)
|
||||||
self._model = model
|
self._model = model
|
||||||
self._toload = []
|
self._toload = []
|
||||||
for f in files:
|
for f in files:
|
||||||
self._toload.append(f)
|
self._toload.append(f)
|
||||||
self._children.append(f)
|
self._children.append(f)
|
||||||
@@ -249,9 +252,9 @@ class RootModelItem(ModelItem):
|
|||||||
else:
|
else:
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||||
|
|
||||||
def appendChild(self, item):
|
def appendChild(self, item, signalModel=True):
|
||||||
if isinstance(item, FileModelItem):
|
if isinstance(item, FileModelItem):
|
||||||
ModelItem.appendChild(self, item)
|
ModelItem.appendChild(self, item, signalModel=signalModel)
|
||||||
else:
|
else:
|
||||||
raise TypeError("Only FileModelItems can be attached to RootModelItem")
|
raise TypeError("Only FileModelItems can be attached to RootModelItem")
|
||||||
|
|
||||||
@@ -283,12 +286,15 @@ class RootModelItem(ModelItem):
|
|||||||
return [child.getAnnotations() for child in self.children()
|
return [child.getAnnotations() for child in self.children()
|
||||||
if hasattr(child, 'getAnnotations')]
|
if hasattr(child, 'getAnnotations')]
|
||||||
|
|
||||||
|
|
||||||
class KeyValueModelItem(ModelItem, MutableMapping):
|
class KeyValueModelItem(ModelItem, MutableMapping):
|
||||||
def __init__(self, hidden=[], properties=None):
|
def __init__(self, hidden=None, properties=None):
|
||||||
ModelItem.__init__(self)
|
ModelItem.__init__(self)
|
||||||
self._dict = {}
|
self._dict = {}
|
||||||
self._items = {}
|
self._items = {}
|
||||||
self._hidden = hidden + [None, 'class', 'unlabeled', 'unconfirmed']
|
self._hidden = set(hidden or [])
|
||||||
|
self._hidden.update({None, 'class', 'unlabeled', 'unconfirmed'})
|
||||||
|
|
||||||
# dummy key/value so that pyqt does not convert the dict
|
# dummy key/value so that pyqt does not convert the dict
|
||||||
# into a QVariantMap while communicating with the Views
|
# into a QVariantMap while communicating with the Views
|
||||||
self._dict[None] = None
|
self._dict[None] = None
|
||||||
@@ -393,8 +399,10 @@ class KeyValueModelItem(ModelItem, MutableMapping):
|
|||||||
del self['unconfirmed']
|
del self['unconfirmed']
|
||||||
self._emitDataChanged('unconfirmed')
|
self._emitDataChanged('unconfirmed')
|
||||||
|
|
||||||
|
|
||||||
class FileModelItem(KeyValueModelItem):
|
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)
|
KeyValueModelItem.__init__(self, hidden=hidden, properties=fileinfo)
|
||||||
|
|
||||||
def data(self, role=Qt.DisplayRole, column=0):
|
def data(self, role=Qt.DisplayRole, column=0):
|
||||||
@@ -417,6 +425,7 @@ class FileModelItem(KeyValueModelItem):
|
|||||||
elif fileinfo['class'] == 'video':
|
elif fileinfo['class'] == 'video':
|
||||||
return VideoFileModelItem(fileinfo)
|
return VideoFileModelItem(fileinfo)
|
||||||
|
|
||||||
|
|
||||||
class ImageModelItem(ModelItem):
|
class ImageModelItem(ModelItem):
|
||||||
def __init__(self, annotations):
|
def __init__(self, annotations):
|
||||||
ModelItem.__init__(self)
|
ModelItem.__init__(self)
|
||||||
@@ -435,6 +444,7 @@ class ImageModelItem(ModelItem):
|
|||||||
for ann in self.annotations():
|
for ann in self.annotations():
|
||||||
ann.setUnconfirmed(False)
|
ann.setUnconfirmed(False)
|
||||||
|
|
||||||
|
|
||||||
class ImageFileModelItem(FileModelItem, ImageModelItem):
|
class ImageFileModelItem(FileModelItem, ImageModelItem):
|
||||||
def __init__(self, fileinfo):
|
def __init__(self, fileinfo):
|
||||||
self._annotation_data = fileinfo.get("annotations", [])
|
self._annotation_data = fileinfo.get("annotations", [])
|
||||||
@@ -467,6 +477,7 @@ class ImageFileModelItem(FileModelItem, ImageModelItem):
|
|||||||
if hasattr(child, 'getAnnotations')]
|
if hasattr(child, 'getAnnotations')]
|
||||||
return fi
|
return fi
|
||||||
|
|
||||||
|
|
||||||
class VideoFileModelItem(FileModelItem):
|
class VideoFileModelItem(FileModelItem):
|
||||||
def __init__(self, fileinfo):
|
def __init__(self, fileinfo):
|
||||||
frameinfos = fileinfo.get("frames", [])
|
frameinfos = fileinfo.get("frames", [])
|
||||||
@@ -483,6 +494,7 @@ class VideoFileModelItem(FileModelItem):
|
|||||||
fi['frames'] = [child.getAnnotations() for child in self.children()]
|
fi['frames'] = [child.getAnnotations() for child in self.children()]
|
||||||
return fi
|
return fi
|
||||||
|
|
||||||
|
|
||||||
class FrameModelItem(ImageModelItem, KeyValueModelItem):
|
class FrameModelItem(ImageModelItem, KeyValueModelItem):
|
||||||
def __init__(self, frameinfo):
|
def __init__(self, frameinfo):
|
||||||
annotations = frameinfo.get("annotations", [])
|
annotations = frameinfo.get("annotations", [])
|
||||||
@@ -516,6 +528,7 @@ class FrameModelItem(ImageModelItem, KeyValueModelItem):
|
|||||||
if hasattr(child, 'getAnnotations')]
|
if hasattr(child, 'getAnnotations')]
|
||||||
return fi
|
return fi
|
||||||
|
|
||||||
|
|
||||||
class AnnotationModelItem(KeyValueModelItem):
|
class AnnotationModelItem(KeyValueModelItem):
|
||||||
def __init__(self, annotation):
|
def __init__(self, annotation):
|
||||||
KeyValueModelItem.__init__(self, properties=annotation)
|
KeyValueModelItem.__init__(self, properties=annotation)
|
||||||
@@ -542,6 +555,7 @@ class AnnotationModelItem(KeyValueModelItem):
|
|||||||
return Qt.red
|
return Qt.red
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class KeyValueRowModelItem(ModelItem):
|
class KeyValueRowModelItem(ModelItem):
|
||||||
def __init__(self, key, read_only=True):
|
def __init__(self, key, read_only=True):
|
||||||
ModelItem.__init__(self)
|
ModelItem.__init__(self)
|
||||||
@@ -579,18 +593,21 @@ class KeyValueRowModelItem(ModelItem):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class AnnotationModel(QAbstractItemModel):
|
class AnnotationModel(QAbstractItemModel):
|
||||||
# signals
|
# signals
|
||||||
dirtyChanged = pyqtSignal(bool, name='dirtyChanged')
|
dirtyChanged = pyqtSignal(bool, name='dirtyChanged')
|
||||||
|
|
||||||
def __init__(self, annotations, parent=None):
|
def __init__(self, annotations, parent=None):
|
||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
self._annotations = annotations
|
self._annotations = annotations
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
self._root = RootModelItem(self, annotations)
|
self._root = RootModelItem(self, annotations)
|
||||||
diff = time.time() - start
|
diff = time.time() - start
|
||||||
LOG.info("Created AnnotationModel in %.2fs" % (diff, ))
|
LOG.info("Created AnnotationModel in %.2fs" % (diff, ))
|
||||||
|
|
||||||
self.dataChanged.connect(self.onDataChanged)
|
self.dataChanged.connect(self.onDataChanged)
|
||||||
self.rowsInserted.connect(self.onDataChanged)
|
self.rowsInserted.connect(self.onDataChanged)
|
||||||
self.rowsRemoved.connect(self.onDataChanged)
|
self.rowsRemoved.connect(self.onDataChanged)
|
||||||
@@ -660,8 +677,10 @@ class AnnotationModel(QAbstractItemModel):
|
|||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||||
if section == 0: return "File/Type/Key"
|
if section == 0:
|
||||||
elif section == 1: return "Value"
|
return "File/Type/Key"
|
||||||
|
elif section == 1:
|
||||||
|
return "Value"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Own methods
|
# Own methods
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import sys
|
import sys
|
||||||
from os.path import dirname, realpath
|
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 PyQt4.QtGui import QApplication
|
||||||
from sloth.core.labeltool import LabelTool
|
from sloth.core.labeltool import LabelTool
|
||||||
from sloth import APP_NAME, ORGANIZATION_NAME, ORGANIZATION_DOMAIN
|
from sloth import APP_NAME, ORGANIZATION_NAME, ORGANIZATION_DOMAIN
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from sloth.conf import default_config
|
|
||||||
from sloth.utils import importlib
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
from sloth.conf import default_config
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ LABELS = (
|
|||||||
{
|
{
|
||||||
'attributes': {
|
'attributes': {
|
||||||
'class': 'Face',
|
'class': 'Face',
|
||||||
},
|
},
|
||||||
'inserter': 'sloth.items.RectItemInserter',
|
'inserter': 'sloth.items.RectItemInserter',
|
||||||
'item': 'sloth.items.RectItem',
|
'item': 'sloth.items.RectItem',
|
||||||
'hotkey': 'f',
|
'hotkey': 'f',
|
||||||
@@ -46,7 +46,7 @@ LABELS = (
|
|||||||
{
|
{
|
||||||
'attributes': {
|
'attributes': {
|
||||||
'class': 'rect',
|
'class': 'rect',
|
||||||
},
|
},
|
||||||
'inserter': 'sloth.items.RectItemInserter',
|
'inserter': 'sloth.items.RectItemInserter',
|
||||||
'item': 'sloth.items.RectItem',
|
'item': 'sloth.items.RectItem',
|
||||||
'hotkey': 'r',
|
'hotkey': 'r',
|
||||||
@@ -61,6 +61,15 @@ LABELS = (
|
|||||||
'hotkey': 'p',
|
'hotkey': 'p',
|
||||||
'text': 'Point',
|
'text': 'Point',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'attributes': {
|
||||||
|
'class': 'polygon',
|
||||||
|
},
|
||||||
|
'inserter': 'sloth.items.PolygonItemInserter',
|
||||||
|
'item': 'sloth.items.PolygonItem',
|
||||||
|
'hotkey': 'o',
|
||||||
|
'text': 'Polygon',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# HOTKEYS
|
# HOTKEYS
|
||||||
@@ -97,6 +106,7 @@ HOTKEYS = (
|
|||||||
# to such a class.
|
# to such a class.
|
||||||
CONTAINERS = (
|
CONTAINERS = (
|
||||||
('*.json', 'sloth.annotations.container.JsonContainer'),
|
('*.json', 'sloth.annotations.container.JsonContainer'),
|
||||||
|
('*.msgpack', 'sloth.annotations.container.MsgpackContainer'),
|
||||||
('*.yaml', 'sloth.annotations.container.YamlContainer'),
|
('*.yaml', 'sloth.annotations.container.YamlContainer'),
|
||||||
('*.pickle', 'sloth.annotations.container.PickleContainer'),
|
('*.pickle', 'sloth.annotations.container.PickleContainer'),
|
||||||
('*.sloth-init', 'sloth.annotations.container.FileNameListContainer'),
|
('*.sloth-init', 'sloth.annotations.container.FileNameListContainer'),
|
||||||
|
|||||||
+52
-48
@@ -1,14 +1,17 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
import sloth
|
import sloth
|
||||||
import shutil
|
import shutil
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from sloth.core.cli import BaseCommand, CommandError
|
from sloth.core.cli import BaseCommand, CommandError
|
||||||
from sloth.annotations.container import *
|
from sloth.annotations.container import *
|
||||||
from optparse import make_option
|
from optparse import make_option
|
||||||
|
from operator import itemgetter
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ConvertCommand(BaseCommand):
|
class ConvertCommand(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Converts a label file from one file format to another.
|
Converts a label file from one file format to another.
|
||||||
@@ -77,8 +80,9 @@ class DumpLabelsCommand(BaseCommand):
|
|||||||
|
|
||||||
class AppendFilesCommand(BaseCommand):
|
class AppendFilesCommand(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Append image or video files to a label file. Creates the label
|
Append image or video files to a label file. Creates the label file if it
|
||||||
file if it does not exist before.
|
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> ...]'
|
args = '<labelfile> <file1> [<file2> ...]'
|
||||||
help = __doc__.strip()
|
help = __doc__.strip()
|
||||||
@@ -98,6 +102,8 @@ class AppendFilesCommand(BaseCommand):
|
|||||||
raise CommandError("Expect at least 2 arguments.")
|
raise CommandError("Expect at least 2 arguments.")
|
||||||
|
|
||||||
self.labeltool.loadAnnotations(args[0])
|
self.labeltool.loadAnnotations(args[0])
|
||||||
|
present_filenames = {a["filename"] for a in self.labeltool.annotations()}
|
||||||
|
|
||||||
for filename in args[1:]:
|
for filename in args[1:]:
|
||||||
rel_filename = filename
|
rel_filename = filename
|
||||||
try:
|
try:
|
||||||
@@ -106,6 +112,10 @@ class AppendFilesCommand(BaseCommand):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if rel_filename in present_filenames:
|
||||||
|
logger.info("Not adding file again: %s" % rel_filename)
|
||||||
|
continue
|
||||||
|
|
||||||
_, ext = os.path.splitext(rel_filename)
|
_, ext = os.path.splitext(rel_filename)
|
||||||
if (not options['image'] and ext.lower() in self.video_extensions) or options['video']:
|
if (not options['image'] and ext.lower() in self.video_extensions) or options['video']:
|
||||||
logger.debug("Adding video file: %s" % rel_filename)
|
logger.debug("Adding video file: %s" % rel_filename)
|
||||||
@@ -113,6 +123,7 @@ class AppendFilesCommand(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
logger.debug("Adding image file: %s" % rel_filename)
|
logger.debug("Adding image file: %s" % rel_filename)
|
||||||
item = self.labeltool.addImageFile(rel_filename)
|
item = self.labeltool.addImageFile(rel_filename)
|
||||||
|
present_filenames.add(rel_filename)
|
||||||
|
|
||||||
if options['unlabeled']:
|
if options['unlabeled']:
|
||||||
item.setUnlabeled(True)
|
item.setUnlabeled(True)
|
||||||
@@ -122,7 +133,6 @@ class AppendFilesCommand(BaseCommand):
|
|||||||
class MergeFilesCommand(BaseCommand):
|
class MergeFilesCommand(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Merge annotations of two label files and create a new one from it.
|
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
|
If both input files have annotations for the same frame number, the result
|
||||||
will contain the union of both annotations.
|
will contain the union of both annotations.
|
||||||
|
|
||||||
@@ -149,58 +159,50 @@ class MergeFilesCommand(BaseCommand):
|
|||||||
an3 = self.merge_annotations(an1, an2)
|
an3 = self.merge_annotations(an1, an2)
|
||||||
|
|
||||||
logger.debug("saving annotations to %s" % output)
|
logger.debug("saving annotations to %s" % output)
|
||||||
containerOut = self.labeltool._container_factory.create(output)
|
out_container = self.labeltool._container_factory.create(output)
|
||||||
containerOut.save(an3, output)
|
out_container.save(an3, output)
|
||||||
|
|
||||||
|
def merge_annotations(self, an1, an2, match_key='filename'):
|
||||||
|
"""This merges all annotations from an2 into an1."""
|
||||||
|
|
||||||
def merge_annotations(self, an1, an2):
|
for item in an2:
|
||||||
|
matching_items = [it1 for it1 in an1 if
|
||||||
|
it1['class'] == item['class'] and
|
||||||
|
it1[match_key] == item[match_key]]
|
||||||
|
|
||||||
# I could also think of an implementation merging an1 and an2, and flattening the lists of lists
|
# If we can't find a match, we just append the item to an1.
|
||||||
# that are obtained
|
if len(matching_items) == 0:
|
||||||
assert(len(an1) == 1 and len(an2) == 1)
|
an1.append(item)
|
||||||
d1 = an1[0]
|
continue
|
||||||
d2 = an2[0]
|
|
||||||
|
|
||||||
if(d1['class'] != 'video'):
|
# We found at least one match, just take the first.
|
||||||
raise NotImplemented('mergefiles: Currently, only annotation files from video can be merged.')
|
# 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]
|
||||||
|
|
||||||
if(d2['class'] != d1['class']):
|
# Update the keys first.
|
||||||
raise CommandError("mergefiles: Both annotation files have to be of the same type (%s vs. %s)." % (d1['class'], d2['class']))
|
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
|
||||||
|
|
||||||
if(d1['filename'] != d2['filename']):
|
match_item[key] = value
|
||||||
raise CommandError('mergefiles: Both annotation files must annotate the same video file.')
|
|
||||||
|
|
||||||
assert(d1['frames'] != None)
|
# Merge frames.
|
||||||
assert(d2['frames'] != None)
|
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.
|
||||||
frames1 = d1['frames']
|
if 'annotations' in match_item:
|
||||||
frames2 = d2['frames']
|
match_item['annotations'].extend(item.get('annotations', []))
|
||||||
|
|
||||||
# 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
|
return an1
|
||||||
|
|
||||||
@@ -222,10 +224,12 @@ def _make_writeable(filename):
|
|||||||
# command dictionary str -> Command
|
# command dictionary str -> Command
|
||||||
_commands = {}
|
_commands = {}
|
||||||
|
|
||||||
|
|
||||||
def register_command(name, command):
|
def register_command(name, command):
|
||||||
global _commands
|
global _commands
|
||||||
_commands[name] = command
|
_commands[name] = command
|
||||||
|
|
||||||
|
|
||||||
def get_commands():
|
def get_commands():
|
||||||
global _commands
|
global _commands
|
||||||
return _commands
|
return _commands
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
Label tool exception classes.
|
Sloth exception classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ImproperlyConfigured(Exception):
|
class ImproperlyConfigured(Exception):
|
||||||
"""There is an error in the configuration."""
|
"""There is an error in the configuration."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotImplementedException(Exception):
|
class NotImplementedException(Exception):
|
||||||
"""This function/method/class has not been implemented yet."""
|
"""This function/method/class has not been implemented yet."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidArgumentException(Exception):
|
class InvalidArgumentException(Exception):
|
||||||
"""The argument is invalid."""
|
"""The argument is invalid."""
|
||||||
pass
|
pass
|
||||||
|
|||||||
+30
-32
@@ -1,10 +1,8 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
"""
|
"""
|
||||||
This is the core labeltool module.
|
This is the core labeltool module.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import sys, os
|
import os
|
||||||
|
import sys
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
from sloth.annotations.model import *
|
from sloth.annotations.model import *
|
||||||
@@ -16,13 +14,15 @@ from sloth import VERSION
|
|||||||
from sloth.core.commands import get_commands
|
from sloth.core.commands import get_commands
|
||||||
from sloth.gui import MainWindow
|
from sloth.gui import MainWindow
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import okapy.videoio as okv
|
import okapy.videoio as okv
|
||||||
except:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LabelTool(QObject):
|
class LabelTool(QObject):
|
||||||
"""
|
"""
|
||||||
This is the main label tool object. It stores the state of the tool, i.e.
|
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"
|
"Type '%s help <subcommand>' for help on a specific subcommand.\n\n"
|
||||||
|
|
||||||
# Signals
|
# Signals
|
||||||
statusMessage = pyqtSignal(str)
|
statusMessage = pyqtSignal(str)
|
||||||
annotationsLoaded = pyqtSignal()
|
annotationsLoaded = pyqtSignal()
|
||||||
pluginLoaded = pyqtSignal(QAction)
|
pluginLoaded = pyqtSignal(QAction)
|
||||||
# This still emits a QModelIndex, because Qt cannot handle emiting
|
# This still emits a QModelIndex, because Qt cannot handle emiting
|
||||||
# a derived class instead of a base class, i.e. ImageFileModelItem
|
# a derived class instead of a base class, i.e. ImageFileModelItem
|
||||||
# instead of ModelItem
|
# instead of ModelItem
|
||||||
@@ -105,7 +105,8 @@ class LabelTool(QObject):
|
|||||||
|
|
||||||
# Initialize logging
|
# Initialize logging
|
||||||
loglevel = (logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG)[int(options.verbosity)]
|
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
|
# Disable PyQt log messages
|
||||||
logging.getLogger("PyQt4").setLevel(logging.WARNING)
|
logging.getLogger("PyQt4").setLevel(logging.WARNING)
|
||||||
@@ -170,7 +171,6 @@ class LabelTool(QObject):
|
|||||||
else:
|
else:
|
||||||
self.clearAnnotations()
|
self.clearAnnotations()
|
||||||
|
|
||||||
|
|
||||||
def fetch_command(self, subcommand):
|
def fetch_command(self, subcommand):
|
||||||
"""
|
"""
|
||||||
Tries to fetch the given subcommand, printing a message with the
|
Tries to fetch the given subcommand, printing a message with the
|
||||||
@@ -179,8 +179,8 @@ class LabelTool(QObject):
|
|||||||
try:
|
try:
|
||||||
app_name = get_commands()[subcommand]
|
app_name = get_commands()[subcommand]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % \
|
sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" %
|
||||||
(subcommand, self.prog_name))
|
(subcommand, self.prog_name))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if isinstance(app_name, BaseCommand):
|
if isinstance(app_name, BaseCommand):
|
||||||
# If the command is already loaded, use it directly.
|
# If the command is already loaded, use it directly.
|
||||||
@@ -221,13 +221,13 @@ class LabelTool(QObject):
|
|||||||
### Annoation file handling
|
### Annoation file handling
|
||||||
###___________________________________________________________________________________________
|
###___________________________________________________________________________________________
|
||||||
def loadAnnotations(self, fname, handleErrors=True):
|
def loadAnnotations(self, fname, handleErrors=True):
|
||||||
fname = str(fname) # convert from QString
|
fname = str(fname) # convert from QString
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._container = self._container_factory.create(fname)
|
self._container = self._container_factory.create(fname)
|
||||||
self._model = AnnotationModel(self._container.load(fname))
|
self._model = AnnotationModel(self._container.load(fname))
|
||||||
msg = "Successfully loaded %s (%d files, %d annotations)" % \
|
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:
|
except Exception as e:
|
||||||
if handleErrors:
|
if handleErrors:
|
||||||
msg = "Error: Loading failed (%s)" % str(e)
|
msg = "Error: Loading failed (%s)" % str(e)
|
||||||
@@ -247,7 +247,6 @@ class LabelTool(QObject):
|
|||||||
try:
|
try:
|
||||||
# create new container if the filename is different
|
# create new container if the filename is different
|
||||||
if fname != self._container.filename():
|
if fname != self._container.filename():
|
||||||
# TODO: skip if it is the same class
|
|
||||||
self._container = self._container_factory.create(fname)
|
self._container = self._container_factory.create(fname)
|
||||||
|
|
||||||
# Get annotations dict
|
# Get annotations dict
|
||||||
@@ -256,7 +255,7 @@ class LabelTool(QObject):
|
|||||||
self._container.save(ann, fname)
|
self._container.save(ann, fname)
|
||||||
#self._model.writeback() # write back changes that are cached in the model itself, e.g. mask updates
|
#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)" % \
|
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
|
success = True
|
||||||
self._model.setDirty(False)
|
self._model.setDirty(False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -300,7 +299,7 @@ class LabelTool(QObject):
|
|||||||
else:
|
else:
|
||||||
next_image = next(self._model.iterator(ImageModelItem))
|
next_image = next(self._model.iterator(ImageModelItem))
|
||||||
if next_image is not None:
|
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:
|
if next_image is not None:
|
||||||
self.setCurrentImage(next_image)
|
self.setCurrentImage(next_image)
|
||||||
@@ -349,18 +348,18 @@ class LabelTool(QObject):
|
|||||||
|
|
||||||
def addImageFile(self, fname):
|
def addImageFile(self, fname):
|
||||||
fileitem = {
|
fileitem = {
|
||||||
'filename': fname,
|
'filename': fname,
|
||||||
'class': 'image',
|
'class': 'image',
|
||||||
'annotations': [ ],
|
'annotations': [],
|
||||||
}
|
}
|
||||||
return self._model._root.appendFileItem(fileitem)
|
return self._model._root.appendFileItem(fileitem)
|
||||||
|
|
||||||
def addVideoFile(self, fname):
|
def addVideoFile(self, fname):
|
||||||
fileitem = {
|
fileitem = {
|
||||||
'filename': fname,
|
'filename': fname,
|
||||||
'class': 'video',
|
'class': 'video',
|
||||||
'frames': [ ],
|
'frames': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# FIXME: OKAPI should provide a method to get all timestamps at once
|
# FIXME: OKAPI should provide a method to get all timestamps at once
|
||||||
# FIXME: Some dialog should be displayed, telling the user that the
|
# 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))
|
LOG.debug("Adding %d frames" % len(timestamps))
|
||||||
fileitem['frames'] = [{'annotations': [], 'num': i,
|
fileitem['frames'] = [{'annotations': [], 'num': i,
|
||||||
'timestamp': ts, 'class': 'frame'}
|
'timestamp': ts, 'class': 'frame'}
|
||||||
for i, ts in enumerate(timestamps)]
|
for i, ts in enumerate(timestamps)]
|
||||||
else:
|
else:
|
||||||
i = 0
|
i = 0
|
||||||
while video.getNextFrame():
|
while video.getNextFrame():
|
||||||
LOG.debug("Adding frame %d" % i)
|
LOG.debug("Adding frame %d" % i)
|
||||||
ts = video.getTimestamp()
|
ts = video.getTimestamp()
|
||||||
frame = { 'annotations': [],
|
frame = {'annotations': [],
|
||||||
'num': i,
|
'num': i,
|
||||||
'timestamp': ts,
|
'timestamp': ts,
|
||||||
'class': 'frame'
|
'class': 'frame'
|
||||||
}
|
}
|
||||||
fileitem['frames'].append(frame)
|
fileitem['frames'].append(frame)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
@@ -450,4 +449,3 @@ class LabelTool(QObject):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self._mainwindow.treeview
|
return self._mainwindow.treeview
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
|
import importlib
|
||||||
from sloth.core import exceptions
|
from sloth.core import exceptions
|
||||||
from sloth.utils import importlib
|
|
||||||
|
|
||||||
def import_callable(module_path_name):
|
def import_callable(module_path_name):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
"""This is the AnnotationScene module"""
|
"""This is the AnnotationScene module"""
|
||||||
from PyQt4.QtGui import *
|
|
||||||
from PyQt4.QtCore import *
|
|
||||||
from sloth.items import *
|
from sloth.items import *
|
||||||
from sloth.core.exceptions import InvalidArgumentException
|
from sloth.core.exceptions import InvalidArgumentException
|
||||||
from sloth.annotations.model import AnnotationModelItem
|
from sloth.annotations.model import AnnotationModelItem
|
||||||
@@ -9,19 +7,20 @@ from sloth.conf import config
|
|||||||
import logging
|
import logging
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AnnotationScene(QGraphicsScene):
|
class AnnotationScene(QGraphicsScene):
|
||||||
mousePositionChanged = pyqtSignal(float, float)
|
mousePositionChanged = pyqtSignal(float, float)
|
||||||
def __init__(self, labeltool, items=None, inserters=None, parent=None):
|
def __init__(self, labeltool, items=None, inserters=None, parent=None):
|
||||||
super(AnnotationScene, self).__init__(parent)
|
super(AnnotationScene, self).__init__(parent)
|
||||||
|
|
||||||
self._model = None
|
self._model = None
|
||||||
self._image_item = None
|
self._image_item = None
|
||||||
self._inserter = None
|
self._inserter = None
|
||||||
self._scene_item = None
|
self._scene_item = None
|
||||||
self._message = ""
|
self._message = ""
|
||||||
self._labeltool = labeltool
|
self._labeltool = labeltool
|
||||||
|
|
||||||
self._itemfactory = Factory(items)
|
self._itemfactory = Factory(items)
|
||||||
self._inserterfactory = Factory(inserters)
|
self._inserterfactory = Factory(inserters)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -143,10 +142,14 @@ class AnnotationScene(QGraphicsScene):
|
|||||||
self._labeltool.currentImageChanged.connect(inserter.imageChange)
|
self._labeltool.currentImageChanged.connect(inserter.imageChange)
|
||||||
self._inserter = inserter
|
self._inserter = inserter
|
||||||
LOG.debug("Created inserter for class '%s' with default properties '%s'" % (label_class, default_properties))
|
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):
|
def onInsertionModeEnded(self):
|
||||||
if self._inserter is not None:
|
if self._inserter is not None:
|
||||||
self._inserter.abort()
|
self._inserter.abort()
|
||||||
|
self.views()[0].viewport().setCursor(Qt.ArrowCursor)
|
||||||
|
|
||||||
#
|
#
|
||||||
# common methods
|
# common methods
|
||||||
@@ -185,6 +188,19 @@ class AnnotationScene(QGraphicsScene):
|
|||||||
# selection mode
|
# selection mode
|
||||||
QGraphicsScene.mousePressEvent(self, event)
|
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):
|
def mouseReleaseEvent(self, event):
|
||||||
LOG.debug("mouseReleaseEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos()))
|
LOG.debug("mouseReleaseEvent %s %s" % (self.sceneRect().contains(event.scenePos()), event.scenePos()))
|
||||||
if self._inserter is not None:
|
if self._inserter is not None:
|
||||||
@@ -255,7 +271,7 @@ class AnnotationScene(QGraphicsScene):
|
|||||||
found = False
|
found = False
|
||||||
|
|
||||||
items = [item for item in self.items()
|
items = [item for item in self.items()
|
||||||
if item.flags() & QGraphicsItem.ItemIsSelectable] * 2
|
if item.flags() & QGraphicsItem.ItemIsSelectable] * 2
|
||||||
if reverse:
|
if reverse:
|
||||||
items.reverse()
|
items.reverse()
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -3,17 +3,21 @@ from PyQt4.QtGui import *
|
|||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
from sloth.gui.floatinglayout import FloatingLayout
|
from sloth.gui.floatinglayout import FloatingLayout
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def unique_list(seq):
|
def unique_list(seq):
|
||||||
seen = {}
|
seen = {}
|
||||||
result = []
|
result = []
|
||||||
for item in seq:
|
for item in seq:
|
||||||
if item in seen: continue
|
if item in seen:
|
||||||
|
continue
|
||||||
seen[item] = 1
|
seen[item] = 1
|
||||||
result.append(item)
|
result.append(item)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ButtonListWidget(QGroupBox):
|
class ButtonListWidget(QGroupBox):
|
||||||
selectionChanged = pyqtSignal(object)
|
selectionChanged = pyqtSignal(object)
|
||||||
|
|
||||||
@@ -141,9 +145,9 @@ class ButtonArea(QWidget):
|
|||||||
else:
|
else:
|
||||||
button_list.hide()
|
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_names.append(label_name)
|
||||||
self.label_properties[label_name] = properties
|
self.label_properties[label_name] = properties or {}
|
||||||
for key, value in properties.items():
|
for key, value in properties.items():
|
||||||
if key in self.properties:
|
if key in self.properties:
|
||||||
self.properties[key] = unique_list(self.properties[key] + value)
|
self.properties[key] = unique_list(self.properties[key] + value)
|
||||||
@@ -183,7 +187,7 @@ class ButtonArea(QWidget):
|
|||||||
def clickedLabelButton(self, label_name):
|
def clickedLabelButton(self, label_name):
|
||||||
#button = self.get_checked_label_button()
|
#button = self.get_checked_label_button()
|
||||||
#print button
|
#print button
|
||||||
if label_name != None:
|
if label_name is not None:
|
||||||
LOG.debug("ButtonArea: %s" % label_name)
|
LOG.debug("ButtonArea: %s" % label_name)
|
||||||
self.show_only_label_properties(label_name)
|
self.show_only_label_properties(label_name)
|
||||||
else:
|
else:
|
||||||
@@ -196,8 +200,10 @@ class ButtonArea(QWidget):
|
|||||||
if button is not None:
|
if button is not None:
|
||||||
self.label_button_list.toggleChecked(button)
|
self.label_button_list.toggleChecked(button)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
from conf import config
|
from conf import config
|
||||||
|
|
||||||
config.update("example_config")
|
config.update("example_config")
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
@@ -206,6 +212,7 @@ def main():
|
|||||||
|
|
||||||
return app.exec_()
|
return app.exec_()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import sys, os
|
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
|
|
||||||
|
|
||||||
class Label(QLabel):
|
class Label(QLabel):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -12,6 +12,7 @@ class Label(QLabel):
|
|||||||
menu.addActions(self.actions())
|
menu.addActions(self.actions())
|
||||||
menu.exec_(ev.globalPos())
|
menu.exec_(ev.globalPos())
|
||||||
|
|
||||||
|
|
||||||
class ControlButtonWidget(QWidget):
|
class ControlButtonWidget(QWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from PyQt4.QtCore import Qt, QRect, QSize, QPoint
|
from PyQt4.QtCore import Qt, QRect, QSize, QPoint
|
||||||
from PyQt4.QtGui import QLayout, QSizePolicy, QWidgetItem
|
from PyQt4.QtGui import QLayout, QSizePolicy, QWidgetItem
|
||||||
|
|
||||||
|
|
||||||
class FloatingLayout(QLayout):
|
class FloatingLayout(QLayout):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QLayout.__init__(self, parent)
|
QLayout.__init__(self, parent)
|
||||||
|
|||||||
+16
-12
@@ -1,32 +1,37 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
import math
|
import math
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
try:
|
try:
|
||||||
import okapy.videoio
|
import okapy.videoio
|
||||||
except:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
videos = []
|
videos = []
|
||||||
scenes = []
|
scenes = []
|
||||||
|
|
||||||
|
|
||||||
class GraphicsView(QGraphicsView):
|
class GraphicsView(QGraphicsView):
|
||||||
# Signals
|
# Signals
|
||||||
scaleChanged = pyqtSignal(float)
|
scaleChanged = pyqtSignal(float)
|
||||||
focusIn = pyqtSignal()
|
focusIn = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QGraphicsView.__init__(self, parent)
|
QGraphicsView.__init__(self, parent)
|
||||||
self.setDragMode(QGraphicsView.RubberBandDrag)
|
self.setDragMode(QGraphicsView.RubberBandDrag)
|
||||||
#self.setDragMode(QGraphicsView.ScrollHandDrag)
|
#self.setDragMode(QGraphicsView.ScrollHandDrag)
|
||||||
self.setMouseTracking(True)
|
self.setMouseTracking(True)
|
||||||
self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing);
|
self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform | QPainter.TextAntialiasing)
|
||||||
self.setStyleSheet("QFrame { border: 3px solid black }");
|
self.setStyleSheet("QFrame { border: 3px solid black }")
|
||||||
self._active = False
|
self._active = False
|
||||||
self._pan = False
|
self._pan = False
|
||||||
self._panStartX = -1
|
self._panStartX = -1
|
||||||
self._panStartY = -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):
|
def fitInView(self):
|
||||||
if self.scene() is None:
|
if self.scene() is None:
|
||||||
return
|
return
|
||||||
@@ -58,14 +63,14 @@ class GraphicsView(QGraphicsView):
|
|||||||
if not self._active:
|
if not self._active:
|
||||||
self._active = True
|
self._active = True
|
||||||
self.setFocus(Qt.OtherFocusReason)
|
self.setFocus(Qt.OtherFocusReason)
|
||||||
self.setStyleSheet("QFrame { border: 3px solid red }");
|
self.setStyleSheet("QFrame { border: 3px solid red }")
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def deactivate(self):
|
def deactivate(self):
|
||||||
if self._active:
|
if self._active:
|
||||||
self._active = False
|
self._active = False
|
||||||
self.clearFocus()
|
self.clearFocus()
|
||||||
self.setStyleSheet("QFrame { border: 3px solid black }");
|
self.setStyleSheet("QFrame { border: 3px solid black }")
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def getMinScale(self):
|
def getMinScale(self):
|
||||||
@@ -125,7 +130,7 @@ class GraphicsView(QGraphicsView):
|
|||||||
def mouseMoveEvent(self, event):
|
def mouseMoveEvent(self, event):
|
||||||
if self._pan:
|
if self._pan:
|
||||||
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - (event.x() - self._panStartX))
|
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._panStartX = event.x()
|
||||||
self._panStartY = event.y()
|
self._panStartY = event.y()
|
||||||
event.accept()
|
event.accept()
|
||||||
@@ -138,7 +143,7 @@ class FrameViewer(QWidget):
|
|||||||
activeSceneViewChanged = pyqtSignal(GraphicsView)
|
activeSceneViewChanged = pyqtSignal(GraphicsView)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QMainWindow.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
|
|
||||||
def getActiveSceneView(self):
|
def getActiveSceneView(self):
|
||||||
pass
|
pass
|
||||||
@@ -188,7 +193,7 @@ class MultiFrameEqualViewer(FrameViewer):
|
|||||||
n_cols = math.ceil(len(self.scenes) / n_rows)
|
n_cols = math.ceil(len(self.scenes) / n_rows)
|
||||||
self.layout = QGridLayout(self)
|
self.layout = QGridLayout(self)
|
||||||
for i, scene_view in enumerate(self.scene_views):
|
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.setLayout(self.layout)
|
||||||
self.activateSceneView(0)
|
self.activateSceneView(0)
|
||||||
|
|
||||||
@@ -208,4 +213,3 @@ class MultiFrameEqualViewer(FrameViewer):
|
|||||||
|
|
||||||
def getActiveSceneView(self):
|
def getActiveSceneView(self):
|
||||||
return self.scene_views[self.active_scene_view]
|
return self.scene_views[self.active_scene_view]
|
||||||
|
|
||||||
|
|||||||
+18
-9
@@ -305,7 +305,8 @@ class MainWindow(QMainWindow):
|
|||||||
if isinstance(locked, QVariant): locked = locked.toBool()
|
if isinstance(locked, QVariant): locked = locked.toBool()
|
||||||
self.resize(size)
|
self.resize(size)
|
||||||
self.move(pos)
|
self.move(pos)
|
||||||
self.restoreState(state)
|
if state is not None:
|
||||||
|
self.restoreState(state)
|
||||||
self.ui.actionLocked.setChecked(bool(locked))
|
self.ui.actionLocked.setChecked(bool(locked))
|
||||||
|
|
||||||
def saveApplicationSettings(self):
|
def saveApplicationSettings(self):
|
||||||
@@ -377,18 +378,26 @@ class MainWindow(QMainWindow):
|
|||||||
image_types = [ '*.jpg', '*.bmp', '*.png', '*.pgm', '*.ppm', '*.ppm', '*.tif', '*.gif' ]
|
image_types = [ '*.jpg', '*.bmp', '*.png', '*.pgm', '*.ppm', '*.ppm', '*.tif', '*.gif' ]
|
||||||
video_types = [ '*.mp4', '*.mpg', '*.mpeg', '*.avi', '*.mov', '*.vob' ]
|
video_types = [ '*.mp4', '*.mpg', '*.mpeg', '*.avi', '*.mov', '*.vob' ]
|
||||||
format_str = ' '.join(image_types + video_types)
|
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:
|
item = None
|
||||||
return
|
for fname in fnames:
|
||||||
|
if len(str(fname)) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
fname = str(fname)
|
fname = str(fname)
|
||||||
|
|
||||||
for pattern in image_types:
|
if os.path.isabs(fname):
|
||||||
if fnmatch.fnmatch(fname, pattern):
|
fname = os.path.relpath(fname, str(path))
|
||||||
return self.labeltool.addImageFile(fname)
|
|
||||||
|
|
||||||
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):
|
def onViewsLockedChanged(self, checked):
|
||||||
features = QDockWidget.AllDockWidgetFeatures
|
features = QDockWidget.AllDockWidgetFeatures
|
||||||
|
|||||||
+24
-12
@@ -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.core.exceptions import ImproperlyConfigured
|
||||||
from sloth.annotations.model import AnnotationModelItem
|
from sloth.annotations.model import AnnotationModelItem
|
||||||
from sloth.gui.floatinglayout import FloatingLayout
|
from sloth.gui.floatinglayout import FloatingLayout
|
||||||
from sloth.gui.utils import MyVBoxLayout
|
from sloth.gui.utils import MyVBoxLayout
|
||||||
from sloth.utils.bind import bind
|
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__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AbstractAttributeHandler:
|
class AbstractAttributeHandler:
|
||||||
def defaults(self):
|
def defaults(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def updateValues(self, values):
|
def updateValues(self, values):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setItems(self, items, showItemClasses=False):
|
def setItems(self, items, showItemClasses=False):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def autoAddEnabled(self):
|
def autoAddEnabled(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class AttributeHandlerFactory:
|
class AttributeHandlerFactory:
|
||||||
def create(self, attribute, values):
|
def create(self, attribute, values):
|
||||||
# Class attribute cannot be changed
|
# Class attribute cannot be changed
|
||||||
@@ -36,17 +43,18 @@ class AttributeHandlerFactory:
|
|||||||
# Else, we create our own default handler
|
# Else, we create our own default handler
|
||||||
return DefaultAttributeHandler(attribute, values)
|
return DefaultAttributeHandler(attribute, values)
|
||||||
|
|
||||||
|
|
||||||
class DefaultAttributeHandler(QGroupBox, AbstractAttributeHandler):
|
class DefaultAttributeHandler(QGroupBox, AbstractAttributeHandler):
|
||||||
def __init__(self, attribute, values, parent=None):
|
def __init__(self, attribute, values, parent=None):
|
||||||
QGroupBox.__init__(self, attribute, parent)
|
QGroupBox.__init__(self, attribute, parent)
|
||||||
self._attribute = attribute
|
self._attribute = attribute
|
||||||
self._current_items = []
|
self._current_items = []
|
||||||
self._defaults = {}
|
self._defaults = {}
|
||||||
self._inputField = None
|
self._inputField = None
|
||||||
self._inputFieldType = None
|
self._inputFieldType = None
|
||||||
self._insertIndex = -1
|
self._insertIndex = -1
|
||||||
self._insertAtEnd = False
|
self._insertAtEnd = False
|
||||||
self._shortcuts = {}
|
self._shortcuts = {}
|
||||||
|
|
||||||
# Setup GUI
|
# Setup GUI
|
||||||
self._layout = FloatingLayout()
|
self._layout = FloatingLayout()
|
||||||
@@ -251,11 +259,14 @@ class LabelEditor(QScrollArea):
|
|||||||
for lc in self._label_classes:
|
for lc in self._label_classes:
|
||||||
attributes |= set(self._editor.getLabelClassAttributes(lc))
|
attributes |= set(self._editor.getLabelClassAttributes(lc))
|
||||||
|
|
||||||
|
attributes = list(attributes)
|
||||||
|
attributes.sort()
|
||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
handler = self._editor.getHandler(attr)
|
handler = self._editor.getHandler(attr)
|
||||||
if handler is not None:
|
if handler is not None:
|
||||||
if len(items) > 1:
|
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)
|
handler.setItems(valid_items, True)
|
||||||
else:
|
else:
|
||||||
handler.setItems(items)
|
handler.setItems(items)
|
||||||
@@ -283,6 +294,7 @@ class LabelEditor(QScrollArea):
|
|||||||
def insertionMode(self):
|
def insertionMode(self):
|
||||||
return self._insertion_mode
|
return self._insertion_mode
|
||||||
|
|
||||||
|
|
||||||
class PropertyEditor(QWidget):
|
class PropertyEditor(QWidget):
|
||||||
# Signals
|
# Signals
|
||||||
insertionModeStarted = pyqtSignal(str)
|
insertionModeStarted = pyqtSignal(str)
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
from PyQt4.QtCore import QSize
|
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
|
# This is really really ugly, but the QDockWidget for some reason does not notice when
|
||||||
# its child widget becomes smaller...
|
# its child widget becomes smaller...
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from sloth.core.utils import import_callable
|
from sloth.core.utils import import_callable
|
||||||
|
|
||||||
|
|
||||||
class Factory:
|
class Factory:
|
||||||
"""
|
"""
|
||||||
A generic factory for both items and inserters.
|
A generic factory for both items and inserters.
|
||||||
@@ -34,8 +35,8 @@ class Factory:
|
|||||||
"""
|
"""
|
||||||
_type = str(_type)
|
_type = str(_type)
|
||||||
if _type in self._items and not replace:
|
if _type in self._items and not replace:
|
||||||
raise Exception("Type %s already has an item: %s" % \
|
raise Exception("Type %s already has an item: %s" %
|
||||||
(_type, str(self._items[_type])))
|
(_type, str(self._items[_type])))
|
||||||
else:
|
else:
|
||||||
if type(item) == str:
|
if type(item) == str:
|
||||||
item = import_callable(item)
|
item = import_callable(item)
|
||||||
@@ -51,10 +52,10 @@ class Factory:
|
|||||||
Type for which the mapping should be removed. If None, all
|
Type for which the mapping should be removed. If None, all
|
||||||
mappings will be removed.
|
mappings will be removed.
|
||||||
"""
|
"""
|
||||||
_type = str(_type)
|
|
||||||
if _type is None:
|
if _type is None:
|
||||||
self._items = {}
|
self._items = {}
|
||||||
else:
|
else:
|
||||||
|
_type = str(_type)
|
||||||
if _type in self._items:
|
if _type in self._items:
|
||||||
del self._items[_type]
|
del self._items[_type]
|
||||||
|
|
||||||
@@ -82,4 +83,3 @@ class Factory:
|
|||||||
if item is None:
|
if item is None:
|
||||||
return None
|
return None
|
||||||
return item(*args, **kwargs)
|
return item(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
+118
-27
@@ -1,6 +1,6 @@
|
|||||||
|
import math
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.Qt import *
|
from PyQt4.Qt import *
|
||||||
import math
|
|
||||||
|
|
||||||
|
|
||||||
class ItemInserter(QObject):
|
class ItemInserter(QObject):
|
||||||
@@ -11,14 +11,12 @@ class ItemInserter(QObject):
|
|||||||
annotationFinished = pyqtSignal()
|
annotationFinished = pyqtSignal()
|
||||||
inserterFinished = pyqtSignal()
|
inserterFinished = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, labeltool, scene, default_properties={},
|
def __init__(self, labeltool, scene, default_properties=None,
|
||||||
prefix="", commit=True):
|
prefix="", commit=True):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
self._labeltool = labeltool
|
self._labeltool = labeltool
|
||||||
self._scene = scene
|
self._scene = scene
|
||||||
self._default_properties = default_properties
|
self._default_properties = default_properties or {}
|
||||||
if self._default_properties is None:
|
|
||||||
self._default_properties = {}
|
|
||||||
self._prefix = prefix
|
self._prefix = prefix
|
||||||
self._ann = {}
|
self._ann = {}
|
||||||
self._commit = commit
|
self._commit = commit
|
||||||
@@ -40,6 +38,9 @@ class ItemInserter(QObject):
|
|||||||
def mousePressEvent(self, event, image_item):
|
def mousePressEvent(self, event, image_item):
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, event, image_item):
|
||||||
|
event.accept()
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event, image_item):
|
def mouseReleaseEvent(self, event, image_item):
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
@@ -79,13 +80,21 @@ class PointItemInserter(ItemInserter):
|
|||||||
|
|
||||||
|
|
||||||
class RectItemInserter(ItemInserter):
|
class RectItemInserter(ItemInserter):
|
||||||
def __init__(self, labeltool, scene, default_properties={},
|
def __init__(self, labeltool, scene, default_properties=None,
|
||||||
prefix="", commit=True):
|
prefix="", commit=True):
|
||||||
ItemInserter.__init__(self, labeltool, scene, default_properties,
|
ItemInserter.__init__(self, labeltool, scene, default_properties,
|
||||||
prefix, commit)
|
prefix, commit)
|
||||||
|
self._aiming = True
|
||||||
|
self._helpLines = None
|
||||||
|
self._helpLinesPen = QPen(Qt.green, 2, Qt.DashLine)
|
||||||
self._init_pos = None
|
self._init_pos = None
|
||||||
|
|
||||||
def mousePressEvent(self, event, image_item):
|
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()
|
pos = event.scenePos()
|
||||||
self._init_pos = pos
|
self._init_pos = pos
|
||||||
self._item = QGraphicsRectItem(QRectF(pos.x(), pos.y(), 0, 0))
|
self._item = QGraphicsRectItem(QRectF(pos.x(), pos.y(), 0, 0))
|
||||||
@@ -94,10 +103,28 @@ class RectItemInserter(ItemInserter):
|
|||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
def mouseMoveEvent(self, event, image_item):
|
def mouseMoveEvent(self, event, image_item):
|
||||||
if self._item is not None:
|
if self._aiming:
|
||||||
assert self._init_pos is not None
|
if self._helpLines is not None:
|
||||||
rect = QRectF(self._init_pos, event.scenePos()).normalized()
|
self._scene.removeItem(self._helpLines)
|
||||||
self._item.setRect(rect)
|
|
||||||
|
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()
|
event.accept()
|
||||||
|
|
||||||
@@ -118,12 +145,18 @@ class RectItemInserter(ItemInserter):
|
|||||||
self._init_pos = None
|
self._init_pos = None
|
||||||
self._item = None
|
self._item = None
|
||||||
|
|
||||||
|
self._aiming = True
|
||||||
|
self._scene.views()[0].viewport().setCursor(Qt.CrossCursor)
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
def allowOutOfSceneEvents(self):
|
def allowOutOfSceneEvents(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
|
if self._helpLines is not None:
|
||||||
|
self._scene.removeItem(self._helpLines)
|
||||||
|
self._helpLines = None
|
||||||
|
|
||||||
if self._item is not None:
|
if self._item is not None:
|
||||||
self._scene.removeItem(self._item)
|
self._scene.removeItem(self._item)
|
||||||
self._item = None
|
self._item = None
|
||||||
@@ -132,7 +165,7 @@ class RectItemInserter(ItemInserter):
|
|||||||
|
|
||||||
|
|
||||||
class FixedRatioRectItemInserter(RectItemInserter):
|
class FixedRatioRectItemInserter(RectItemInserter):
|
||||||
def __init__(self, labeltool, scene, default_properties={},
|
def __init__(self, labeltool, scene, default_properties=None,
|
||||||
prefix="", commit=True):
|
prefix="", commit=True):
|
||||||
RectItemInserter.__init__(self, labeltool, scene, default_properties,
|
RectItemInserter.__init__(self, labeltool, scene, default_properties,
|
||||||
prefix, commit)
|
prefix, commit)
|
||||||
@@ -159,7 +192,7 @@ class FixedRatioRectItemInserter(RectItemInserter):
|
|||||||
class SequenceItemInserter(ItemInserter):
|
class SequenceItemInserter(ItemInserter):
|
||||||
inserters = []
|
inserters = []
|
||||||
|
|
||||||
def __init__(self, labeltool, scene, default_properties={},
|
def __init__(self, labeltool, scene, default_properties=None,
|
||||||
prefix="", commit=True):
|
prefix="", commit=True):
|
||||||
ItemInserter.__init__(self, labeltool, scene, default_properties,
|
ItemInserter.__init__(self, labeltool, scene, default_properties,
|
||||||
prefix, commit)
|
prefix, commit)
|
||||||
@@ -260,8 +293,7 @@ class BBoxFaceInserter(SequenceItemInserter):
|
|||||||
if event.key() == Qt.Key_O and self._state > 0:
|
if event.key() == Qt.Key_O and self._state > 0:
|
||||||
self.toggleOccludedForCurrentInserter()
|
self.toggleOccludedForCurrentInserter()
|
||||||
return
|
return
|
||||||
elif Qt.Key_0 <= event.key() <= Qt.Key_9 or \
|
elif Qt.Key_0 <= event.key() <= Qt.Key_9 or Qt.Key_A <= event.key() <= Qt.Key_Z:
|
||||||
Qt.Key_A <= event.key() <= Qt.Key_Z:
|
|
||||||
if Qt.Key_0 <= event.key() <= Qt.Key_9:
|
if Qt.Key_0 <= event.key() <= Qt.Key_9:
|
||||||
self._ann['id'] = int(str(event.text()))
|
self._ann['id'] = int(str(event.text()))
|
||||||
else:
|
else:
|
||||||
@@ -324,31 +356,90 @@ class NPointFaceInserter(SequenceItemInserter):
|
|||||||
"Now at: " + self.inserters[self._state][2])
|
"Now at: " + self.inserters[self._state][2])
|
||||||
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
class PolygonItemInserter(ItemInserter):
|
class PolygonItemInserter(ItemInserter):
|
||||||
def __init__(self, scene, mode=None):
|
def __init__(self, labeltool, scene, default_properties=None,
|
||||||
ItemInserter.__init__(self, scene, mode)
|
prefix="", commit=True):
|
||||||
self._current_item = None
|
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):
|
def mousePressEvent(self, event, image_item):
|
||||||
pos = event.scenePos()
|
pos = event.scenePos()
|
||||||
if self._current_item is None:
|
|
||||||
|
if self._item is None:
|
||||||
item = QGraphicsPolygonItem(QPolygonF([pos]))
|
item = QGraphicsPolygonItem(QPolygonF([pos]))
|
||||||
self._current_item = item
|
self._item = item
|
||||||
|
self._item.setPen(self.pen())
|
||||||
self._scene.addItem(item)
|
self._scene.addItem(item)
|
||||||
else:
|
|
||||||
polygon = self._current_item.polygon()
|
self._scene.setMessage("Press Enter to finish the polygon.")
|
||||||
polygon.append(pos)
|
|
||||||
self._current_item.setPolygon(polygon)
|
polygon = self._item.polygon()
|
||||||
|
polygon.append(pos)
|
||||||
|
self._item.setPolygon(polygon)
|
||||||
|
|
||||||
event.accept()
|
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):
|
def mouseMoveEvent(self, event, image_item):
|
||||||
if self._current_item is not None:
|
if self._item is not None:
|
||||||
pos = event.scenePos()
|
pos = event.scenePos()
|
||||||
polygon = self._current_item.polygon()
|
polygon = self._item.polygon()
|
||||||
assert polygon.size() > 0
|
assert polygon.size() > 0
|
||||||
polygon[-1] = pos
|
polygon[-1] = pos
|
||||||
self._current_item.setPolygon(polygon)
|
self._item.setPolygon(polygon)
|
||||||
|
|
||||||
event.accept()
|
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
@@ -1,9 +1,10 @@
|
|||||||
from PyQt4.QtGui import *
|
import logging
|
||||||
from PyQt4.Qt import *
|
from PyQt4.Qt import *
|
||||||
|
|
||||||
import logging
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# convenience functions for creating hotkey functions
|
# convenience functions for creating hotkey functions
|
||||||
class cycleValue:
|
class cycleValue:
|
||||||
def __init__(self, itemkey, valuelist):
|
def __init__(self, itemkey, valuelist):
|
||||||
@@ -34,9 +35,11 @@ class cycleValue:
|
|||||||
item._model_item[key] = self.valuelist[nextindex]
|
item._model_item[key] = self.valuelist[nextindex]
|
||||||
item.dataChanged()
|
item.dataChanged()
|
||||||
|
|
||||||
|
|
||||||
def setValue(itemkey, newvalue):
|
def setValue(itemkey, newvalue):
|
||||||
return lambda self: _setValue(self, itemkey, newvalue)
|
return lambda self: _setValue(self, itemkey, newvalue)
|
||||||
|
|
||||||
|
|
||||||
def _setValue(self, itemkey, newvalue):
|
def _setValue(self, itemkey, newvalue):
|
||||||
if isinstance(itemkey, IgnorePrefix):
|
if isinstance(itemkey, IgnorePrefix):
|
||||||
itemkey = itemkey.value
|
itemkey = itemkey.value
|
||||||
@@ -58,6 +61,7 @@ class IgnorePrefix:
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class BaseItem(QAbstractGraphicsShapeItem):
|
class BaseItem(QAbstractGraphicsShapeItem):
|
||||||
"""
|
"""
|
||||||
Base class for visualization items.
|
Base class for visualization items.
|
||||||
@@ -70,12 +74,6 @@ class BaseItem(QAbstractGraphicsShapeItem):
|
|||||||
def __init__(self, model_item=None, prefix="", parent=None):
|
def __init__(self, model_item=None, prefix="", parent=None):
|
||||||
"""
|
"""
|
||||||
Creates a visualization item.
|
Creates a visualization item.
|
||||||
|
|
||||||
Parameters
|
|
||||||
==========
|
|
||||||
index :
|
|
||||||
data :
|
|
||||||
parent :
|
|
||||||
"""
|
"""
|
||||||
QAbstractGraphicsShapeItem.__init__(self, parent)
|
QAbstractGraphicsShapeItem.__init__(self, parent)
|
||||||
self.setFlags(QGraphicsItem.ItemIsSelectable |
|
self.setFlags(QGraphicsItem.ItemIsSelectable |
|
||||||
@@ -100,7 +98,8 @@ class BaseItem(QAbstractGraphicsShapeItem):
|
|||||||
self._valid = True
|
self._valid = True
|
||||||
|
|
||||||
if len(self.cycleValuesOnKeypress) > 0:
|
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()
|
self.changeColor()
|
||||||
|
|
||||||
@@ -139,8 +138,9 @@ class BaseItem(QAbstractGraphicsShapeItem):
|
|||||||
return self._prefix
|
return self._prefix
|
||||||
|
|
||||||
def setPen(self, pen):
|
def setPen(self, pen):
|
||||||
|
pen = QPen(pen) # convert to pen if argument is a QColor
|
||||||
QAbstractGraphicsShapeItem.setPen(self, pen)
|
QAbstractGraphicsShapeItem.setPen(self, pen)
|
||||||
self._text_item.setDefaultTextColor(pen)
|
self._text_item.setDefaultTextColor(pen.color())
|
||||||
|
|
||||||
def setText(self, text=""):
|
def setText(self, text=""):
|
||||||
"""
|
"""
|
||||||
@@ -166,12 +166,12 @@ class BaseItem(QAbstractGraphicsShapeItem):
|
|||||||
"""
|
"""
|
||||||
return self._text_bg_brush
|
return self._text_bg_brush
|
||||||
|
|
||||||
def setAutoTextKeys(self, keys=[]):
|
def setAutoTextKeys(self, keys=None):
|
||||||
"""
|
"""
|
||||||
Sets the keys for which the values from the annotations
|
Sets the keys for which the values from the annotations
|
||||||
are displayed automatically as text.
|
are displayed automatically as text.
|
||||||
"""
|
"""
|
||||||
self._auto_text_keys = keys
|
self._auto_text_keys = keys or []
|
||||||
self._text_item.setHtml(self._compile_text())
|
self._text_item.setHtml(self._compile_text())
|
||||||
|
|
||||||
def autoTextKeys(self):
|
def autoTextKeys(self):
|
||||||
@@ -520,9 +520,6 @@ class MultiPointItem(BaseItem):
|
|||||||
self._points = points
|
self._points = points
|
||||||
self.setPos(QPointF(0, 0))
|
self.setPos(QPointF(0, 0))
|
||||||
|
|
||||||
def updateModel(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
xmin = min(self._points[::2])
|
xmin = min(self._points[::2])
|
||||||
xmax = max(self._points[::2])
|
xmax = max(self._points[::2])
|
||||||
@@ -651,21 +648,73 @@ class NPointFacePointItem(QGraphicsEllipseItem):
|
|||||||
|
|
||||||
class NPointFaceItem(GroupItem):
|
class NPointFaceItem(GroupItem):
|
||||||
items = [
|
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
|
# Eyes
|
||||||
(OccludablePointItem, "leoc"), # left eye outer center
|
(OccludablePointItem, "leoc"), # left eye outer center
|
||||||
(OccludablePointItem, "lec"), # left eye center
|
(OccludablePointItem, "leu67"), # left eye upper countour 67%
|
||||||
(OccludablePointItem, "leic"), # left eye inner center
|
(OccludablePointItem, "leu33"), # left eye upper countour 33%
|
||||||
(OccludablePointItem, "reic"), # right eye inner center
|
(OccludablePointItem, "leic"), # left eye inner center
|
||||||
(OccludablePointItem, "rec"), # right eye center
|
(OccludablePointItem, "lel33"), # left eye lower countour 33%
|
||||||
(OccludablePointItem, "reoc"), # right eye outer center
|
(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
|
# Nose
|
||||||
(OccludablePointItem, "nt"), # nose tip
|
(OccludablePointItem, "nr100"), # nose ridge 100%
|
||||||
(OccludablePointItem, "nl"), # nose left
|
(OccludablePointItem, "nr67"), # nose ridge 67%
|
||||||
(OccludablePointItem, "nr"), # nose right
|
(OccludablePointItem, "nr33"), # nose ridge 33%
|
||||||
(OccludablePointItem, "nc"), # nose center
|
(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
|
# 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, "ulc"), # upper lip center
|
||||||
(OccludablePointItem, "llc"), # lower lip center
|
(OccludablePointItem, "llc"), # lower lip center
|
||||||
(OccludablePointItem, "mc"), # mouth center
|
(OccludablePointItem, "mc"), # mouth center
|
||||||
@@ -678,12 +727,6 @@ class NPointFaceItem(GroupItem):
|
|||||||
|
|
||||||
# Chin
|
# Chin
|
||||||
(OccludablePointItem, "cc"), # chin center
|
(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):
|
def __init__(self, model_item=None, prefix="", parent=None):
|
||||||
@@ -694,6 +737,8 @@ class NPointFaceItem(GroupItem):
|
|||||||
if prefix + 'x' in self._model_item and \
|
if prefix + 'x' in self._model_item and \
|
||||||
prefix + 'y' in self._model_item:
|
prefix + 'y' in self._model_item:
|
||||||
child = callable_(self._model_item, prefix, self)
|
child = callable_(self._model_item, prefix, self)
|
||||||
|
if hasattr(child, 'setToolTip'):
|
||||||
|
child.setToolTip(prefix)
|
||||||
self._children.append(child)
|
self._children.append(child)
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
@@ -712,3 +757,76 @@ class NPointFaceItem(GroupItem):
|
|||||||
pen.setStyle(Qt.DashLine)
|
pen.setStyle(Qt.DashLine)
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
painter.drawRect(self.boundingRect())
|
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)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CopyAnnotationsPlugin(QObject):
|
class CopyAnnotationsPlugin(QObject):
|
||||||
def __init__(self, labeltool, class_filter=None, frame_range=1, overlap_threshold=None, prefix=''):
|
def __init__(self, labeltool, class_filter=None, frame_range=1, overlap_threshold=None, prefix=''):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
@@ -14,7 +17,7 @@ class CopyAnnotationsPlugin(QObject):
|
|||||||
|
|
||||||
self._labeltool = labeltool
|
self._labeltool = labeltool
|
||||||
self._wnd = labeltool.mainWindow()
|
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)
|
self._sc.triggered.connect(self.copy)
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
@@ -53,11 +56,8 @@ class CopyAnnotationsPlugin(QObject):
|
|||||||
for annotation in image_item.getAnnotations()['annotations']:
|
for annotation in image_item.getAnnotations()['annotations']:
|
||||||
# check class filter
|
# check class filter
|
||||||
if self._class_filter is not None:
|
if self._class_filter is not None:
|
||||||
if 'class' not in annotation:
|
if annotation.get('class', None) not in self._class_filter:
|
||||||
continue # do not copy
|
continue # do not copy
|
||||||
if annotation['class'] not in self._class_filter:
|
|
||||||
log
|
|
||||||
continue # do not copy
|
|
||||||
annotations.append(annotation)
|
annotations.append(annotation)
|
||||||
return annotations
|
return annotations
|
||||||
|
|
||||||
@@ -85,3 +85,27 @@ class CopyAnnotationsPlugin(QObject):
|
|||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
return self._sc
|
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
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from sloth.core.exceptions import NotImplementedException
|
|
||||||
from PyQt4.QtGui import QImage, qRgb
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import random
|
import random
|
||||||
import colorsys
|
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)]
|
gray_color_table = [qRgb(i, i, i) for i in range(256)]
|
||||||
|
|
||||||
|
|
||||||
def toQImage(im, copy=False):
|
def toQImage(im, copy=False):
|
||||||
if im is None:
|
if im is None:
|
||||||
return QImage()
|
return QImage()
|
||||||
@@ -17,12 +20,13 @@ def toQImage(im, copy=False):
|
|||||||
|
|
||||||
elif len(im.shape) == 3:
|
elif len(im.shape) == 3:
|
||||||
if im.shape[2] == 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
|
return qim.copy() if copy else qim
|
||||||
elif im.shape[2] == 4:
|
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
|
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):
|
def gen_colors(s=0.99, v=0.99, h=None, color_space='rgb', _golden_ratio_conjugate=0.618033988749895):
|
||||||
@@ -58,4 +62,3 @@ def gen_colors(s=0.99, v=0.99, h=None, color_space='rgb', _golden_ratio_conjugat
|
|||||||
h += _golden_ratio_conjugate
|
h += _golden_ratio_conjugate
|
||||||
h %= 1
|
h %= 1
|
||||||
yield cs_convert(h, s, v)
|
yield cs_convert(h, s, v)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
def bind(fun, *args):
|
def bind(fun, *args):
|
||||||
return lambda: fun(*args)
|
return lambda: fun(*args)
|
||||||
|
|
||||||
|
|
||||||
def compose_noargs(funs):
|
def compose_noargs(funs):
|
||||||
def tmp():
|
def tmp():
|
||||||
for f in funs:
|
for f in funs:
|
||||||
f()
|
f()
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
|
|
||||||
def compose(funs):
|
def compose(funs):
|
||||||
def tmp(*args, **kwargs):
|
def tmp(*args, **kwargs):
|
||||||
for f in funs:
|
for f in funs:
|
||||||
|
|||||||
@@ -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]
|
|
||||||
@@ -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
@@ -1,54 +1,45 @@
|
|||||||
from sloth.annotations.container import *
|
from sloth.annotations.container import *
|
||||||
|
|
||||||
|
|
||||||
class MockupContainer:
|
class MockupContainer:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def someFileAnnotations(i):
|
def someFileAnnotations(i):
|
||||||
annotations = []
|
annotations = [{'type': 'rect', 'x': 10 * i, 'y': '20', 'w': '40', 'h': '60'},
|
||||||
annotations.append({'type': 'rect',
|
{'type': 'rect', 'x': '80', 'y': 20 * i, 'w': '40', 'h': '60'}]
|
||||||
'x': 10 * i,
|
|
||||||
'y': '20',
|
|
||||||
'w': '40',
|
|
||||||
'h': '60'})
|
|
||||||
annotations.append({'type': 'rect',
|
|
||||||
'x': '80',
|
|
||||||
'y': 20 * i,
|
|
||||||
'w': '40',
|
|
||||||
'h': '60'})
|
|
||||||
for k in range(i):
|
for k in range(i):
|
||||||
annotations.append({'type': 'point',
|
annotations.append({'type': 'point',
|
||||||
'x': 30 * k,
|
'x': 30 * k,
|
||||||
'y': 30 * k})
|
'y': 30 * k})
|
||||||
return annotations
|
return annotations
|
||||||
|
|
||||||
|
|
||||||
def someAnnotations():
|
def someAnnotations():
|
||||||
annotations = []
|
annotations = []
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
file = {
|
ann = {
|
||||||
'filename': 'file%d.png' % i,
|
'filename': 'file%d.png' % i,
|
||||||
'type': 'image',
|
'type': 'image',
|
||||||
'annotations': someFileAnnotations(i)
|
'annotations': someFileAnnotations(i)
|
||||||
}
|
}
|
||||||
annotations.append(file)
|
annotations.append(ann)
|
||||||
return annotations
|
return annotations
|
||||||
|
|
||||||
|
|
||||||
def common_container_test(filename, container):
|
def common_container_test(filename, container):
|
||||||
original_anns = someAnnotations()
|
original_anns = someAnnotations()
|
||||||
|
|
||||||
container.setAnnotations(original_anns)
|
container.save(original_anns, filename)
|
||||||
assert len(container.annotations()) == len(original_anns)
|
|
||||||
|
|
||||||
container.save(filename)
|
|
||||||
assert container.filename() == filename
|
assert container.filename() == filename
|
||||||
assert os.path.exists(filename)
|
assert os.path.exists(filename)
|
||||||
|
|
||||||
container.clear()
|
container.clear()
|
||||||
assert container.filename() is None
|
assert container.filename() is None
|
||||||
assert len(container.annotations()) == 0
|
|
||||||
|
|
||||||
container.load(filename)
|
container.load(filename)
|
||||||
assert container.filename() == filename
|
assert container.filename() == filename
|
||||||
assert len(container.annotations()) == len(original_anns)
|
|
||||||
|
|
||||||
def test_import_callable():
|
def test_import_callable():
|
||||||
containers = (('*', 'container_test.MockupContainer'),)
|
containers = (('*', 'container_test.MockupContainer'),)
|
||||||
@@ -56,16 +47,19 @@ def test_import_callable():
|
|||||||
item = factory.create('test')
|
item = factory.create('test')
|
||||||
assert isinstance(item, MockupContainer)
|
assert isinstance(item, MockupContainer)
|
||||||
|
|
||||||
|
|
||||||
def test_PickleContainer(tmpdir):
|
def test_PickleContainer(tmpdir):
|
||||||
filename = os.path.join(str(tmpdir), "test_PickleContainer.pickle")
|
filename = os.path.join(str(tmpdir), "test_PickleContainer.pickle")
|
||||||
container = PickleContainer()
|
container = PickleContainer()
|
||||||
common_container_test(filename, container)
|
common_container_test(filename, container)
|
||||||
|
|
||||||
|
|
||||||
def test_JsonContainer(tmpdir):
|
def test_JsonContainer(tmpdir):
|
||||||
filename = os.path.join(str(tmpdir), "test_JsonContainer.json")
|
filename = os.path.join(str(tmpdir), "test_JsonContainer.json")
|
||||||
container = JsonContainer()
|
container = JsonContainer()
|
||||||
common_container_test(filename, container)
|
common_container_test(filename, container)
|
||||||
|
|
||||||
|
|
||||||
def test_YamlContainer(tmpdir):
|
def test_YamlContainer(tmpdir):
|
||||||
filename = os.path.join(str(tmpdir), "test_YamlContainer.yaml")
|
filename = os.path.join(str(tmpdir), "test_YamlContainer.yaml")
|
||||||
container = YamlContainer()
|
container = YamlContainer()
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 136 KiB |
@@ -1,31 +1,31 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"type": "image",
|
"class": "image",
|
||||||
"annotations": [
|
"annotations": [
|
||||||
{
|
{
|
||||||
"height": 60.0,
|
"height": 60.0,
|
||||||
"width": 46.0,
|
"width": 46.0,
|
||||||
"y": 105.0,
|
"y": 105.0,
|
||||||
"x": 346.0,
|
"x": 346.0,
|
||||||
"type": "rect"
|
"class": "rect"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"height": 58.0,
|
"height": 58.0,
|
||||||
"width": 56.0,
|
"width": 56.0,
|
||||||
"y": 119.0,
|
"y": 119.0,
|
||||||
"x": 636.0,
|
"x": 636.0,
|
||||||
"type": "rect"
|
"class": "rect"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"filename": "image1.jpg"
|
"filename": "image1.jpg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "image",
|
"class": "image",
|
||||||
"annotations": [
|
"annotations": [
|
||||||
{
|
{
|
||||||
"y": 155.0,
|
"y": 155.0,
|
||||||
"x": 409.0,
|
"x": 409.0,
|
||||||
"type": "point"
|
"class": "point"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"filename": "image2.jpg"
|
"filename": "image2.jpg"
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from sloth.items import Factory
|
from sloth.items import Factory
|
||||||
|
|
||||||
class MockupRectItem: pass
|
|
||||||
class MockupPointItem: pass
|
class MockupRectItem:
|
||||||
class MockupPolygonItem: pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MockupPointItem:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MockupPolygonItem:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _create_factory():
|
def _create_factory():
|
||||||
itemfactory = Factory({'point': MockupPointItem,
|
itemfactory = Factory({'point': MockupPointItem,
|
||||||
@@ -12,6 +21,7 @@ def _create_factory():
|
|||||||
|
|
||||||
return itemfactory
|
return itemfactory
|
||||||
|
|
||||||
|
|
||||||
def test_register():
|
def test_register():
|
||||||
itemfactory = _create_factory()
|
itemfactory = _create_factory()
|
||||||
|
|
||||||
@@ -24,11 +34,13 @@ def test_register():
|
|||||||
item = itemfactory.create('polygon2')
|
item = itemfactory.create('polygon2')
|
||||||
assert item is None
|
assert item is None
|
||||||
|
|
||||||
|
|
||||||
def test_register_fail():
|
def test_register_fail():
|
||||||
itemfactory = _create_factory()
|
itemfactory = _create_factory()
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
itemfactory.register('rect', MockupRectItem)
|
itemfactory.register('rect', MockupRectItem)
|
||||||
|
|
||||||
|
|
||||||
def test_register_replace():
|
def test_register_replace():
|
||||||
itemfactory = _create_factory()
|
itemfactory = _create_factory()
|
||||||
|
|
||||||
@@ -36,6 +48,7 @@ def test_register_replace():
|
|||||||
item = itemfactory.create('rect')
|
item = itemfactory.create('rect')
|
||||||
assert isinstance(item, MockupPolygonItem)
|
assert isinstance(item, MockupPolygonItem)
|
||||||
|
|
||||||
|
|
||||||
def test_clear():
|
def test_clear():
|
||||||
itemfactory = _create_factory()
|
itemfactory = _create_factory()
|
||||||
|
|
||||||
@@ -52,4 +65,3 @@ def test_clear():
|
|||||||
itemfactory.clear()
|
itemfactory.clear()
|
||||||
assert itemfactory.create('point') is None
|
assert itemfactory.create('point') is None
|
||||||
assert itemfactory.create('polygon') is None
|
assert itemfactory.create('polygon') is None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user