├── .gitignore
├── .travis.yml
├── LICENSE.md
├── Makefile
├── README.md
├── _init_path.py
├── build-tools
├── .gitignore
├── build-ubuntu-binary.sh
├── build-windows-binary.sh
└── envsetup.sh
├── contributors.txt
├── data
└── predefined_classes.txt
├── demo
├── demo.png
└── demo2.png
├── icons
├── cancel.png
├── close.png
├── color.png
├── color_line.png
├── copy.png
├── delete.png
├── done.png
├── done.svg
├── edit.png
├── expert1.png
├── expert2.png
├── eye.png
├── feBlend-icon.png
├── file.png
├── fit-width.png
├── fit-window.png
├── fit.png
├── help.png
├── labels.png
├── labels.svg
├── new.png
├── next.png
├── objects.png
├── open.png
├── open.svg
├── prev.png
├── quit.png
├── save-as.png
├── save-as.svg
├── save.png
├── save.svg
├── undo-cross.png
├── undo.png
├── zoom-in.png
├── zoom-out.png
└── zoom.png
├── labelImg.py
├── libs
├── canvas.py
├── colorDialog.py
├── labelDialog.py
├── labelFile.py
├── lib.py
├── pascal_voc_io.py
├── shape.py
├── toolBar.py
└── zoomWidget.py
├── read_xml.py
├── read_xml_correct_rotation.py
├── read_xml_correct_rotation_left.py
├── resources.qrc
├── rotation_opencv_fasterRCNN.py
├── rotation_opencv_fasterRCNN_correct_rotation.py
└── tests
├── test.bmp
├── test.py
└── 臉書.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | icons/.DS_Store
2 |
3 | resources.py
4 |
5 | *.pyc
6 | .*.swp
7 |
8 | build/
9 | dist/
10 |
11 | tags
12 | cscope*
13 | .ycm_extra_conf.py
14 | .subvimrc
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # vim: set ts=2 et:
2 |
3 | # run xvfb with 32-bit color
4 | # xvfb-run -s '-screen 0 1600x1200x24+32' command_goes_here
5 |
6 | matrix:
7 | include:
8 |
9 | # Python 2.7 + QT4
10 | - os: linux
11 | dist: trusty
12 | sudo: required
13 | language: generic
14 | python: "2.7"
15 | env:
16 | - QT=4
17 | addons:
18 | apt:
19 | packages:
20 | - cmake
21 | - python-qt4
22 | - pyqt4-dev-tools
23 | - xvfb
24 | before_install:
25 | - sudo pip install lxml
26 | - make qt4py2
27 | - xvfb-run make testpy2
28 |
29 | # Python 2.7 + QT4
30 | - os: linux
31 | dist: trusty
32 | sudo: required
33 | language: generic
34 | python: "2.7"
35 | env:
36 | - QT=4
37 | - CONDA=4.2.0
38 | addons:
39 | apt:
40 | packages:
41 | - cmake
42 | #- python-qt4
43 | #- pyqt4-dev-tools
44 | - xvfb
45 | before_install:
46 | # ref: https://www.continuum.io/downloads
47 | - curl -O https://repo.continuum.io/archive/Anaconda2-4.2.0-Linux-x86_64.sh
48 | # ref: http://conda.pydata.org/docs/help/silent.html
49 | - /bin/bash Anaconda2-4.2.0-Linux-x86_64.sh -b -p $HOME/anaconda2
50 | - export PATH="$HOME/anaconda2/bin:$PATH"
51 | # ref: http://stackoverflow.com/questions/21637922/how-to-install-pyqt4-in-anaconda
52 | - conda create -y -n labelImg-py2qt4 python=2.7
53 | - source activate labelImg-py2qt4
54 | - conda install -y pyqt=4
55 | - conda install -y lxml
56 | - make qt4py2
57 | - xvfb-run make testpy2
58 |
59 | # Python 2 + QT5
60 | # disabled; can't get it to work
61 | #- os: linux
62 | # dist: trusty
63 | # sudo: required
64 | # language: generic
65 | # python: "2.7"
66 | # env:
67 | # - QT=5
68 | # addons:
69 | # apt:
70 | # packages:
71 | # - cmake
72 | # - pyqt5-dev-tools
73 | # - xvfb
74 | # before_install:
75 | # - sudo apt-get update
76 | # - sudo apt-get install -y python-pip
77 | # - sudo pip install lxml
78 | # - pyrcc5 --help || true # does QT5 support python2 out of the box?
79 | # - make qt5py3
80 | # - xvfb-run make testpy2
81 |
82 | # Python 3 + QT4
83 | - os: linux
84 | dist: trusty
85 | sudo: required
86 | language: generic
87 | python: "3.5"
88 | env:
89 | - QT=4
90 | addons:
91 | apt:
92 | packages:
93 | - cmake
94 | - python3-pyqt4
95 | - pyqt4-dev-tools
96 | - xvfb
97 | before_install:
98 | - sudo apt-get update
99 | - sudo apt-get install -y python3-pip
100 | - sudo pip3 install lxml
101 | - make qt4py3
102 | - xvfb-run make testpy3
103 |
104 | # Python 3 + QT5
105 | - os: linux
106 | dist: trusty
107 | sudo: required
108 | language: generic
109 | python: "3.5"
110 | env:
111 | - QT=5
112 | addons:
113 | apt:
114 | packages:
115 | - cmake
116 | - pyqt5-dev-tools
117 | - xvfb
118 | before_install:
119 | - sudo apt-get update
120 | - sudo apt-get install -y python3-pip
121 | - sudo pip3 install lxml
122 | - make qt5py3
123 | - xvfb-run make testpy3
124 |
125 | # Python 3 + QT5
126 | - os: linux
127 | dist: trusty
128 | sudo: required
129 | language: generic
130 | python: "3.5"
131 | env:
132 | - QT=5
133 | - CONDA=4.2.0
134 | addons:
135 | apt:
136 | packages:
137 | - cmake
138 | - xvfb
139 | before_install:
140 | # ref: https://www.continuum.io/downloads
141 | - curl -O https://repo.continuum.io/archive/Anaconda3-4.2.0-Linux-x86_64.sh
142 | # ref: http://conda.pydata.org/docs/help/silent.html
143 | - /bin/bash Anaconda3-4.2.0-Linux-x86_64.sh -b -p $HOME/anaconda3
144 | - export PATH="$HOME/anaconda3/bin:$PATH"
145 | # ref: http://stackoverflow.com/questions/21637922/how-to-install-pyqt4-in-anaconda
146 | - conda create -y -n labelImg-py3qt5 python=3.5
147 | - source activate labelImg-py3qt5
148 | - conda install -y pyqt=5
149 | - conda install -y lxml
150 | - make qt5py3
151 | - xvfb-run make testpy3
152 |
153 | # OS X 10.10 Python 3 + QT5
154 | - os: osx
155 | osx_image: xcode6.4 # Xcode 6.4, OS X 10.10
156 | sudo: required
157 | language: generic
158 | python: "3.6"
159 | env:
160 | - QT=5
161 | before_install:
162 | #- brew update
163 | - brew install libxml2
164 | - brew install pyqt5
165 | - which python3 pip3
166 | - python3 --version
167 | #- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3
168 | - sudo -H easy_install-3.6 lxml || true
169 | - python3 -c 'import sys; print(sys.path)'
170 | - python3 -c 'import lxml'
171 | - make qt5py3
172 | - python3 -c 'help("modules")'
173 | - make testpy3 # FIXME: does not work, segfault on travis-ci
174 |
175 | # OS X 10.11 Python 3 + QT5
176 | - os: osx
177 | osx_image: xcode8 # Xcode 8, OS X 10.11
178 | sudo: required
179 | language: generic
180 | python: "3.6"
181 | env:
182 | - QT=5
183 | before_install:
184 | #- brew update
185 | - brew install libxml2
186 | - brew install pyqt5
187 | - which python3 pip3
188 | - python3 --version
189 | #- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3
190 | - sudo -H easy_install-3.6 lxml || true
191 | - python3 -c 'import sys; print(sys.path)'
192 | - python3 -c 'import lxml'
193 | - make qt5py3
194 | - python3 -c 'help("modules")'
195 | - make testpy3 # FIXME: does not work, segfault on travis-ci
196 |
197 | # OS X 10.12 Python 3 + QT5
198 | - os: osx
199 | osx_image: xcode8.2 # OS X 10.12
200 | sudo: required
201 | language: generic
202 | python: "3.6"
203 | env:
204 | - QT=5
205 | before_install:
206 | #- brew update
207 | - brew install libxml2
208 | - brew install pyqt5
209 | - which python3 pip3
210 | - python3 --version
211 | #- sudo -H pip3 install --user --upgrade lxml # pyqt5 installs python3.x, which installs pip3
212 | - sudo -H easy_install-3.6 lxml || true
213 | - python3 -c 'import sys; print(sys.path)'
214 | - python3 -c 'import lxml'
215 | - make qt5py3
216 | - python3 -c 'help("modules")'
217 | #- make testpy3 # FIXME: does not work, segfault on travis-ci
218 | # just make sure the app runs... :-/
219 | - ( python3 labelImg.py ) & sleep 10; kill $!
220 |
221 | # XXX: building QT4 from source takes forever...
222 |
223 | # OS X 10.11 Python 2 + QT4
224 | #- os: osx
225 | # osx_image: xcode7.3 # OS X 10.11
226 | # sudo: required
227 | # language: generic
228 | # python: "2.7"
229 | # env:
230 | # - QT=4
231 | # before_install:
232 | # - brew install libxml2
233 | # # build PyQT4...
234 | # - curl -L -O https://sourceforge.net/projects/pyqt/files/sip/sip-4.19/sip-4.19.tar.gz
235 | # - tar zxvf sip-4.19.tar.gz
236 | # - (cd sip-4.19 && python configure.py -d /Library/Python/2.7/site-packages --arch x86_64 && make && sudo make install)
237 | # # NOTE: produces insane amounts of output...
238 | # - brew install -v cartr/qt4/qt --with-qt3support --without-webkit | sed -e's/ .* / ... /'
239 | # - brew linkapps qt
240 | # - curl -L -O http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12/PyQt4_gpl_mac-4.12.tar.gz
241 | # - tar zxvf PyQt4_gpl_mac-4.12.tar.gz
242 | # - cd PyQt4_gpl_mac-4.12
243 | # - python configure.py --help
244 | # - python configure.py -d /Library/Python/2.7/site-packages --use-arch=x86_64 --confirm-license
245 | # - make
246 | # - sudo make install
247 | # - cd -
248 | # - which python pip
249 | # - python --version
250 | # - sudo -H easy_install-2.7 lxml || true
251 | # - python -c 'import sys; print(sys.path)'
252 | # - python -c 'import lxml'
253 | # - make qt4py2
254 | # - python -c 'help("modules")'
255 | # - make testpy2
256 |
257 | # OS X 10.11 Python 3 + QT4
258 | #- os: osx
259 | # osx_image: xcode7.3 # OS X 10.11
260 | # sudo: required
261 | # language: generic
262 | # python: "3.6"
263 | # env:
264 | # - QT=4
265 | # before_install:
266 | # - brew install libxml2
267 | # - brew install python3
268 | # - which python pip python3 pip3 || true
269 | # - curl -L -O https://sourceforge.net/projects/pyqt/files/sip/sip-4.19/sip-4.19.tar.gz
270 | # - tar zxvf sip-4.19.tar.gz
271 | # - ( cd sip-4.19 && python3 configure.py -d /Library/Python/3.6/site-packages --arch x86_64 && make && sudo make install )
272 | # # NOTE: produces insane amounts of output...
273 | # - brew install -v cartr/qt4/qt --with-qt3support --without-webkit | sed -e's/ .* / ... /'
274 | # - brew linkapps qt
275 | # - curl -L -O http://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12/PyQt4_gpl_mac-4.12.tar.gz
276 | # - tar zxvf PyQt4_gpl_mac-4.12.tar.gz
277 | # - cd PyQt4_gpl_mac-4.12
278 | # - python3 configure.py --help
279 | # - python3 configure.py -d /Library/Python/3.6/site-packages --use-arch=x86_64 --confirm-license
280 | # - make
281 | # - sudo make install
282 | # - cd -
283 | # - which python3 pip3
284 | # - python3 --version
285 | # - sudo -H easy_install-3.6 lxml || true
286 | # - python3 -c 'import sys; print(sys.path)'
287 | # - python3 -c 'import lxml'
288 | # - make qt4py3
289 | # - python3 -c 'help("modules")'
290 | # - make testpy3
291 |
292 | script:
293 | - exit 0
294 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) <2015-2016> Tzutalin
2 |
3 | Copyright (C) 2013 MIT, Computer Science and Artificial Intelligence Laboratory. Bryan Russell, Antonio Torralba, William T. Freeman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # ex: set ts=8 noet:
2 |
3 | all: qt4
4 |
5 | test: testpy2
6 |
7 | testpy2:
8 | python -m unittest discover tests
9 |
10 | testpy3:
11 | python3 -m unittest discover tests
12 |
13 | qt4: qt4py2
14 |
15 | qt5: qt4py3
16 |
17 | qt4py2:
18 | pyrcc4 -py2 -o resources.py resources.qrc
19 |
20 | qt4py3:
21 | pyrcc4 -py3 -o resources.py resources.qrc
22 |
23 | qt5py3:
24 | pyrcc5 -o resources.py resources.qrc
25 |
26 | .PHONY: test
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LabelImg
2 |
3 | [](https://travis-ci.org/tzutalin/labelImg)
4 |
5 | LabelImg is a graphical image annotation tool.
6 |
7 | It is written in Python and uses Qt for its graphical interface.
8 |
9 | The annotation file will be saved as an XML file. The annotation format is PASCAL VOC format, and the format is the same as [ImageNet](http://www.image-net.org/)
10 |
11 | 
12 |
13 | 
14 |
15 | [](https://www.youtube.com/watch?v=p0nR2YsCY_U&feature=youtu.be)
16 |
17 | ## Build source and use it
18 |
19 | Linux/Ubuntu/Mac requires at least [Python 2.6](http://www.python.org/getit/) and has been tested with [PyQt
20 | 4.8](http://www.riverbankcomputing.co.uk/software/pyqt/intro).
21 |
22 | In order to build the resource and assets, you need to install pyqt4-dev-tools and lxml:
23 |
24 | ### Ubuntu
25 |
26 | sudo apt-get install pyqt4-dev-tools
27 | sudo pip install lxml
28 | make all
29 | ./labelImg.py
30 |
31 | ### OS X
32 |
33 | brew install qt qt4
34 | brew install libxml2
35 | make all
36 | ./labelImg.py
37 |
38 | ### Windows
39 |
40 | Need to download and setup [Python 2.6](https://www.python.org/downloads/windows/) or later and [PyQt4](https://www.riverbankcomputing.com/software/pyqt/download). Also, you need to install lxml.
41 |
42 | Open cmd and go to [labelImg]
43 |
44 | pyrcc4 -o resources.py resources.qrc
45 | python labelImg.py
46 |
47 |
48 | ## Download the prebuilt binary directly
49 | [http://tzutalin.github.io/labelImg/](http://tzutalin.github.io/labelImg/). However, there are only prebuilt binaries for Windows and Linux because I don't have Mac OS to do that. If someone can help me to write a script to build binary for Mac OS, I will appreciate that.
50 |
51 | ## Usage
52 | After cloning the code, you should run `$ make all` to generate the resource file.
53 |
54 | You can then start annotating by running `$ ./labelImg.py`. For usage
55 | instructions you can see [Here](https://youtu.be/p0nR2YsCY_U)
56 |
57 | At the moment annotations are saved as an XML file. The format is PASCAL VOC format, and the format is the same as [ImageNet](http://www.image-net.org/)
58 |
59 | You can also see [ImageNet Utils](https://github.com/tzutalin/ImageNet_Utils) to download image, create a label text for machine learning, etc
60 |
61 |
62 | ### General steps from scratch
63 |
64 | * Build and launch: `$ make all; python labelImg.py`
65 |
66 | * Click 'Change default saved annotation folder' in Menu/File
67 |
68 | * Click 'Open Dir'
69 |
70 | * Click 'Create RectBox'
71 |
72 | The annotation will be saved to the folder you specify
73 |
74 | ### Create pre-defined classes
75 |
76 | You can edit the [data/predefined_classes.txt](https://github.com/tzutalin/labelImg/blob/master/data/predefined_classes.txt) to load pre-defined classes
77 |
78 | ### Hotkeys
79 |
80 | * Ctrl + r : Change the defult target dir which saving annotation files
81 |
82 | * Ctrl + s : Save
83 |
84 | * w : Create a bounding box
85 |
86 | * d : Next image
87 |
88 | * a : Previous image
89 |
90 | ### How to contribute
91 | Send a pull request
92 |
93 | ### License
94 | [License](LICENSE.md)
95 |
96 |
--------------------------------------------------------------------------------
/_init_path.py:
--------------------------------------------------------------------------------
1 | """Set up paths"""
2 | import sys
3 |
4 | def add_path(path):
5 | if path not in sys.path:
6 | sys.path.insert(0, path)
7 |
8 | add_path('libs')
9 |
--------------------------------------------------------------------------------
/build-tools/.gitignore:
--------------------------------------------------------------------------------
1 | *.spec
2 | build
3 | dist
4 | pyinstaller
5 | python-2.*
6 | pywin32*
7 | virtual-wine
8 | venv_wine
9 | PyQt4-*
10 | lxml-*
11 |
--------------------------------------------------------------------------------
/build-tools/build-ubuntu-binary.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ### Ubuntu use pyinstall v3.0
3 | THIS_SCRIPT_PATH=`readlink -f $0`
4 | THIS_SCRIPT_DIR=`dirname ${THIS_SCRIPT_PATH}`
5 | cd pyinstaller
6 | git checkout v3.2
7 | cd ${THIS_SCRIPT_DIR}
8 |
9 | rm -r build
10 | rm -r dist
11 | rm labelImg.spec
12 | python pyinstaller/pyinstaller.py --hidden-import=xml \
13 | --hidden-import=xml.etree \
14 | --hidden-import=xml.etree.ElementTree \
15 | --hidden-import=lxml.etree \
16 | -D -F -n labelImg -c "../labelImg.py" -p ../libs
17 |
18 | FOLDER=$(git describe --abbrev=0 --tags)
19 | FOLDER="linux_"$FOLDER
20 | rm -rf "$FOLDER"
21 | mkdir "$FOLDER"
22 | cp dist/labelImg $FOLDER
23 | cp -rf ../data $FOLDER/data
24 | zip "$FOLDER.zip" -r $FOLDER
25 |
--------------------------------------------------------------------------------
/build-tools/build-windows-binary.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ### Window requires pyinstall v2.1
3 | THIS_SCRIPT_PATH=`readlink -f $0`
4 | THIS_SCRIPT_DIR=`dirname ${THIS_SCRIPT_PATH}`
5 | cd pyinstaller
6 | git checkout v2.1
7 | cd ${THIS_SCRIPT_DIR}
8 |
9 | . venv_wine/bin/activate
10 | rm -r build
11 | rm -r dist
12 | rm labelImg.spec
13 | wine c:/Python27/python.exe pyinstaller/pyinstaller.py --hidden-import=xml \
14 | --hidden-import=xml.etree \
15 | --hidden-import=xml.etree.ElementTree \
16 | --hidden-import=lxml.etree \
17 | -D -F -n labelImg -c "../labelImg.py" -p ../libs
18 |
19 | FOLDER=$(git describe --abbrev=0 --tags)
20 | FOLDER="windows_"$FOLDER
21 | rm -rf "$FOLDER"
22 | mkdir "$FOLDER"
23 | cp dist/labelImg.exe $FOLDER
24 | cp -rf ../data $FOLDER/data
25 | zip "$FOLDER.zip" -r $FOLDER
26 |
--------------------------------------------------------------------------------
/build-tools/envsetup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | THIS_SCRIPT_PATH=`readlink -f $0`
4 | THIS_SCRIPT_DIR=`dirname ${THIS_SCRIPT_PATH}`
5 | #OS Ubuntu 14.04
6 | ### Common packages for linux/windows
7 | if [ ! -e "pyinstaller" ]; then
8 | git clone https://github.com/pyinstaller/pyinstaller
9 | cd pyinstaller
10 | git checkout v2.1 -b v2.1
11 | cd ${THIS_SCRIPT_DIR}
12 | fi
13 |
14 | echo "Going to clone and download packages for building windows"
15 | #Pacakges
16 | #> pyinstaller (2.1)
17 | #> wine (1.6.2)
18 | #> virtual-wine (0.1)
19 | #> python-2.7.8.msi
20 | #> pywin32-218.win32-py2.7.exe
21 |
22 | ## tool to install on Ubuntu
23 | #$ sudo apt-get install wine
24 |
25 | ### Clone a repo to create virtual wine env
26 | if [ ! -e "virtual-wine" ]; then
27 | git clone https://github.com/htgoebel/virtual-wine.git
28 | fi
29 |
30 | apt-get install scons
31 | ### Create virtual env
32 | rm -rf venv_wine
33 | ./virtual-wine/vwine-setup venv_wine
34 | #### Active virutal env
35 | . venv_wine/bin/activate
36 |
37 | ### Use wine to install packages to virtual env
38 | if [ ! -e "python-2.7.8.msi" ]; then
39 | wget "https://www.python.org/ftp/python/2.7.8/python-2.7.8.msi"
40 | fi
41 |
42 | if [ ! -e "pywin32-218.win32-py2.7.exe" ]; then
43 | wget "http://nchc.dl.sourceforge.net/project/pywin32/pywin32/Build%20218/pywin32-218.win32-py2.7.exe"
44 | fi
45 |
46 | if [ ! -e "PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe" ]; then
47 | wget "http://nchc.dl.sourceforge.net/project/pyqt/PyQt4/PyQt-4.11.4/PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe"
48 | fi
49 |
50 | if [ ! -e "lxml-2.3.win32-py2.7.exe" ]; then
51 | wget "https://pypi.python.org/packages/3d/ee/affbc53073a951541b82a0ba2a70de266580c00f94dd768a60f125b04fca/lxml-2.3.win32-py2.7.exe#md5=9c02aae672870701377750121f5a6f84"
52 | fi
53 |
54 | wine msiexec -i python-2.7.8.msi
55 | wine pywin32-218.win32-py2.7.exe
56 | wine PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe
57 | wine lxml-2.3.win32-py2.7.exe
58 |
--------------------------------------------------------------------------------
/contributors.txt:
--------------------------------------------------------------------------------
1 | TzuTa Lin
2 | [LabelMe](http://labelme2.csail.mit.edu/Release3.0/index.php)
3 |
--------------------------------------------------------------------------------
/data/predefined_classes.txt:
--------------------------------------------------------------------------------
1 | dog
2 | person
3 | cat
4 | tv
5 | car
6 |
--------------------------------------------------------------------------------
/demo/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/demo/demo.png
--------------------------------------------------------------------------------
/demo/demo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/demo/demo2.png
--------------------------------------------------------------------------------
/icons/cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/cancel.png
--------------------------------------------------------------------------------
/icons/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/close.png
--------------------------------------------------------------------------------
/icons/color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/color.png
--------------------------------------------------------------------------------
/icons/color_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/color_line.png
--------------------------------------------------------------------------------
/icons/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/copy.png
--------------------------------------------------------------------------------
/icons/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/delete.png
--------------------------------------------------------------------------------
/icons/done.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/done.png
--------------------------------------------------------------------------------
/icons/done.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
401 |
--------------------------------------------------------------------------------
/icons/edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/edit.png
--------------------------------------------------------------------------------
/icons/expert1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/expert1.png
--------------------------------------------------------------------------------
/icons/expert2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/expert2.png
--------------------------------------------------------------------------------
/icons/eye.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/eye.png
--------------------------------------------------------------------------------
/icons/feBlend-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/feBlend-icon.png
--------------------------------------------------------------------------------
/icons/file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/file.png
--------------------------------------------------------------------------------
/icons/fit-width.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/fit-width.png
--------------------------------------------------------------------------------
/icons/fit-window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/fit-window.png
--------------------------------------------------------------------------------
/icons/fit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/fit.png
--------------------------------------------------------------------------------
/icons/help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/help.png
--------------------------------------------------------------------------------
/icons/labels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/labels.png
--------------------------------------------------------------------------------
/icons/labels.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/icons/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/new.png
--------------------------------------------------------------------------------
/icons/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/next.png
--------------------------------------------------------------------------------
/icons/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/objects.png
--------------------------------------------------------------------------------
/icons/open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/open.png
--------------------------------------------------------------------------------
/icons/open.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/icons/prev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/prev.png
--------------------------------------------------------------------------------
/icons/quit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/quit.png
--------------------------------------------------------------------------------
/icons/save-as.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/save-as.png
--------------------------------------------------------------------------------
/icons/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/save.png
--------------------------------------------------------------------------------
/icons/save.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
680 |
--------------------------------------------------------------------------------
/icons/undo-cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/undo-cross.png
--------------------------------------------------------------------------------
/icons/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/undo.png
--------------------------------------------------------------------------------
/icons/zoom-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/zoom-in.png
--------------------------------------------------------------------------------
/icons/zoom-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/zoom-out.png
--------------------------------------------------------------------------------
/icons/zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/icons/zoom.png
--------------------------------------------------------------------------------
/libs/canvas.py:
--------------------------------------------------------------------------------
1 |
2 | try:
3 | from PyQt5.QtGui import *
4 | from PyQt5.QtCore import *
5 | from PyQt5.QtWidgets import *
6 | except ImportError:
7 | from PyQt4.QtGui import *
8 | from PyQt4.QtCore import *
9 |
10 | #from PyQt4.QtOpenGL import *
11 |
12 | from shape import Shape
13 | from lib import distance
14 |
15 | CURSOR_DEFAULT = Qt.ArrowCursor
16 | CURSOR_POINT = Qt.PointingHandCursor
17 | CURSOR_DRAW = Qt.CrossCursor
18 | CURSOR_MOVE = Qt.ClosedHandCursor
19 | CURSOR_GRAB = Qt.OpenHandCursor
20 |
21 | #class Canvas(QGLWidget):
22 | class Canvas(QWidget):
23 | zoomRequest = pyqtSignal(int)
24 | scrollRequest = pyqtSignal(int, int)
25 | newShape = pyqtSignal()
26 | selectionChanged = pyqtSignal(bool)
27 | shapeMoved = pyqtSignal()
28 | drawingPolygon = pyqtSignal(bool)
29 |
30 | CREATE, EDIT = list(range(2))
31 |
32 | epsilon = 11.0
33 |
34 | def __init__(self, *args, **kwargs):
35 | super(Canvas, self).__init__(*args, **kwargs)
36 | # Initialise local state.
37 | self.mode = self.EDIT
38 | self.shapes = []
39 | self.current = None
40 | self.selectedShape=None # save the selected shape here
41 | self.selectedShapeCopy=None
42 | self.lineColor = QColor(0, 0, 255)
43 | self.line = Shape(line_color=self.lineColor)
44 | self.prevPoint = QPointF()
45 | self.offsets = QPointF(), QPointF()
46 | self.scale = 1.0
47 | self.pixmap = QPixmap()
48 | self.visible = {}
49 | self._hideBackround = False
50 | self.hideBackround = False
51 | self.hShape = None
52 | self.hVertex = None
53 | self._painter = QPainter()
54 | self._cursor = CURSOR_DEFAULT
55 | # Menus:
56 | self.menus = (QMenu(), QMenu())
57 | # Set widget options.
58 | self.setMouseTracking(True)
59 | self.setFocusPolicy(Qt.WheelFocus)
60 |
61 | def enterEvent(self, ev):
62 | self.overrideCursor(self._cursor)
63 |
64 | def leaveEvent(self, ev):
65 | self.restoreCursor()
66 |
67 | def focusOutEvent(self, ev):
68 | self.restoreCursor()
69 |
70 | def isVisible(self, shape):
71 | return self.visible.get(shape, True)
72 |
73 | def drawing(self):
74 | return self.mode == self.CREATE
75 |
76 | def editing(self):
77 | return self.mode == self.EDIT
78 |
79 | def setEditing(self, value=True):
80 | self.mode = self.EDIT if value else self.CREATE
81 | if not value: # Create
82 | self.unHighlight()
83 | self.deSelectShape()
84 |
85 | def unHighlight(self):
86 | if self.hShape:
87 | self.hShape.highlightClear()
88 | self.hVertex = self.hShape = None
89 |
90 | def selectedVertex(self):
91 | return self.hVertex is not None
92 |
93 | def mouseMoveEvent(self, ev):
94 | """Update line with last point and current coordinates."""
95 | pos = self.transformPos(ev.pos())
96 |
97 | self.restoreCursor()
98 |
99 | # Polygon drawing.
100 | if self.drawing():
101 | self.overrideCursor(CURSOR_DRAW)
102 | if self.current:
103 | color = self.lineColor
104 | if self.outOfPixmap(pos):
105 | # Don't allow the user to draw outside the pixmap.
106 | # Project the point to the pixmap's edges.
107 | pos = self.intersectionPoint(self.current[-1], pos)
108 | elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]):
109 | # Attract line to starting point and colorise to alert the user:
110 | pos = self.current[0]
111 | color = self.current.line_color
112 | self.overrideCursor(CURSOR_POINT)
113 | self.current.highlightVertex(0, Shape.NEAR_VERTEX)
114 | self.line[1] = pos
115 | self.line.line_color = color
116 | self.repaint()
117 | self.current.highlightClear()
118 | return
119 |
120 | # Polygon copy moving.
121 | if Qt.RightButton & ev.buttons():
122 | if self.selectedShapeCopy and self.prevPoint:
123 | self.overrideCursor(CURSOR_MOVE)
124 | self.boundedMoveShape(self.selectedShapeCopy, pos)
125 | self.repaint()
126 | elif self.selectedShape:
127 | self.selectedShapeCopy = self.selectedShape.copy()
128 | self.repaint()
129 | return
130 |
131 | # Polygon/Vertex moving.
132 | if Qt.LeftButton & ev.buttons():
133 | if self.selectedVertex():
134 | self.boundedMoveVertex(pos)
135 | self.shapeMoved.emit()
136 | self.repaint()
137 | elif self.selectedShape and self.prevPoint:
138 | self.overrideCursor(CURSOR_MOVE)
139 | self.boundedMoveShape(self.selectedShape, pos)
140 | self.shapeMoved.emit()
141 | self.repaint()
142 | return
143 |
144 | # Just hovering over the canvas, 2 posibilities:
145 | # - Highlight shapes
146 | # - Highlight vertex
147 | # Update shape/vertex fill and tooltip value accordingly.
148 | self.setToolTip("Image")
149 | for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
150 | # Look for a nearby vertex to highlight. If that fails,
151 | # check if we happen to be inside a shape.
152 | index = shape.nearestVertex(pos, self.epsilon)
153 | if index is not None:
154 | if self.selectedVertex():
155 | self.hShape.highlightClear()
156 | self.hVertex, self.hShape = index, shape
157 | shape.highlightVertex(index, shape.MOVE_VERTEX)
158 | self.overrideCursor(CURSOR_POINT)
159 | self.setToolTip("Click & drag to move point")
160 | self.setStatusTip(self.toolTip())
161 | self.update()
162 | break
163 | elif shape.containsPoint(pos):
164 | if self.selectedVertex():
165 | self.hShape.highlightClear()
166 | self.hVertex, self.hShape = None, shape
167 | self.setToolTip("Click & drag to move shape '%s'" % shape.label)
168 | self.setStatusTip(self.toolTip())
169 | self.overrideCursor(CURSOR_GRAB)
170 | self.update()
171 | break
172 | else: # Nothing found, clear highlights, reset state.
173 | if self.hShape:
174 | self.hShape.highlightClear()
175 | self.update()
176 | self.hVertex, self.hShape = None, None
177 |
178 | def mousePressEvent(self, ev):
179 | pos = self.transformPos(ev.pos())
180 |
181 | if ev.button() == Qt.LeftButton:
182 | if self.drawing():
183 | if self.current and self.current.reachMaxPoints() is False:
184 | initPos = self.current[0]
185 | minX = initPos.x()
186 | minY = initPos.y()
187 | targetPos = self.line[1]
188 | maxX = targetPos.x()
189 | maxY = targetPos.y()
190 | self.current.addPoint(QPointF(maxX, minY))
191 | self.current.addPoint(targetPos)
192 | self.current.addPoint(QPointF(minX, maxY))
193 | self.current.addPoint(initPos)
194 | self.line[0] = self.current[-1]
195 | if self.current.isClosed():
196 | self.finalise()
197 | elif not self.outOfPixmap(pos):
198 | self.current = Shape()
199 | self.current.addPoint(pos)
200 | self.line.points = [pos, pos]
201 | self.setHiding()
202 | self.drawingPolygon.emit(True)
203 | self.update()
204 | else:
205 | self.selectShapePoint(pos)
206 | self.prevPoint = pos
207 | self.repaint()
208 | elif ev.button() == Qt.RightButton and self.editing():
209 | self.selectShapePoint(pos)
210 | self.prevPoint = pos
211 | self.repaint()
212 |
213 | def mouseReleaseEvent(self, ev):
214 | if ev.button() == Qt.RightButton:
215 | menu = self.menus[bool(self.selectedShapeCopy)]
216 | self.restoreCursor()
217 | if not menu.exec_(self.mapToGlobal(ev.pos()))\
218 | and self.selectedShapeCopy:
219 | # Cancel the move by deleting the shadow copy.
220 | self.selectedShapeCopy = None
221 | self.repaint()
222 | elif ev.button() == Qt.LeftButton and self.selectedShape:
223 | self.overrideCursor(CURSOR_GRAB)
224 |
225 | def endMove(self, copy=False):
226 | assert self.selectedShape and self.selectedShapeCopy
227 | shape = self.selectedShapeCopy
228 | #del shape.fill_color
229 | #del shape.line_color
230 | if copy:
231 | self.shapes.append(shape)
232 | self.selectedShape.selected = False
233 | self.selectedShape = shape
234 | self.repaint()
235 | else:
236 | shape.label = self.selectedShape.label
237 | self.deleteSelected()
238 | self.shapes.append(shape)
239 | self.selectedShapeCopy = None
240 |
241 | def hideBackroundShapes(self, value):
242 | self.hideBackround = value
243 | if self.selectedShape:
244 | # Only hide other shapes if there is a current selection.
245 | # Otherwise the user will not be able to select a shape.
246 | self.setHiding(True)
247 | self.repaint()
248 |
249 | def setHiding(self, enable=True):
250 | self._hideBackround = self.hideBackround if enable else False
251 |
252 | def canCloseShape(self):
253 | return self.drawing() and self.current and len(self.current) > 2
254 |
255 | def mouseDoubleClickEvent(self, ev):
256 | # We need at least 4 points here, since the mousePress handler
257 | # adds an extra one before this handler is called.
258 | if self.canCloseShape() and len(self.current) > 3:
259 | self.current.popPoint()
260 | self.finalise()
261 |
262 | def selectShape(self, shape):
263 | self.deSelectShape()
264 | shape.selected = True
265 | self.selectedShape = shape
266 | self.setHiding()
267 | self.selectionChanged.emit(True)
268 | self.update()
269 |
270 | def selectShapePoint(self, point):
271 | """Select the first shape created which contains this point."""
272 | self.deSelectShape()
273 | if self.selectedVertex(): # A vertex is marked for selection.
274 | index, shape = self.hVertex, self.hShape
275 | shape.highlightVertex(index, shape.MOVE_VERTEX)
276 | return
277 | for shape in reversed(self.shapes):
278 | if self.isVisible(shape) and shape.containsPoint(point):
279 | shape.selected = True
280 | self.selectedShape = shape
281 | self.calculateOffsets(shape, point)
282 | self.setHiding()
283 | self.selectionChanged.emit(True)
284 | return
285 |
286 | def calculateOffsets(self, shape, point):
287 | rect = shape.boundingRect()
288 | x1 = rect.x() - point.x()
289 | y1 = rect.y() - point.y()
290 | x2 = (rect.x() + rect.width()) - point.x()
291 | y2 = (rect.y() + rect.height()) - point.y()
292 | self.offsets = QPointF(x1, y1), QPointF(x2, y2)
293 |
294 | def boundedMoveVertex(self, pos):
295 | index, shape = self.hVertex, self.hShape
296 | point = shape[index]
297 | if self.outOfPixmap(pos):
298 | pos = self.intersectionPoint(point, pos)
299 |
300 | shiftPos = pos - point
301 | shape.moveVertexBy(index, shiftPos)
302 |
303 | lindex = (index + 1) % 4
304 | rindex = (index + 3) % 4
305 | lshift = None
306 | rshift = None
307 | if index % 2 == 0:
308 | rshift = QPointF(shiftPos.x(), 0)
309 | lshift = QPointF(0, shiftPos.y())
310 | else:
311 | lshift = QPointF(shiftPos.x(), 0)
312 | rshift = QPointF(0, shiftPos.y())
313 | shape.moveVertexBy(rindex, rshift)
314 | shape.moveVertexBy(lindex, lshift)
315 |
316 | def boundedMoveShape(self, shape, pos):
317 | if self.outOfPixmap(pos):
318 | return False # No need to move
319 | o1 = pos + self.offsets[0]
320 | if self.outOfPixmap(o1):
321 | pos -= QPointF(min(0, o1.x()), min(0, o1.y()))
322 | o2 = pos + self.offsets[1]
323 | if self.outOfPixmap(o2):
324 | pos += QPointF(min(0, self.pixmap.width() - o2.x()),
325 | min(0, self.pixmap.height()- o2.y()))
326 | # The next line tracks the new position of the cursor
327 | # relative to the shape, but also results in making it
328 | # a bit "shaky" when nearing the border and allows it to
329 | # go outside of the shape's area for some reason. XXX
330 | #self.calculateOffsets(self.selectedShape, pos)
331 | dp = pos - self.prevPoint
332 | if dp:
333 | shape.moveBy(dp)
334 | self.prevPoint = pos
335 | return True
336 | return False
337 |
338 | def deSelectShape(self):
339 | if self.selectedShape:
340 | self.selectedShape.selected = False
341 | self.selectedShape = None
342 | self.setHiding(False)
343 | self.selectionChanged.emit(False)
344 | self.update()
345 |
346 | def deleteSelected(self):
347 | if self.selectedShape:
348 | shape = self.selectedShape
349 | self.shapes.remove(self.selectedShape)
350 | self.selectedShape = None
351 | self.update()
352 | return shape
353 |
354 | def copySelectedShape(self):
355 | if self.selectedShape:
356 | shape = self.selectedShape.copy()
357 | self.deSelectShape()
358 | self.shapes.append(shape)
359 | shape.selected = True
360 | self.selectedShape = shape
361 | self.boundedShiftShape(shape)
362 | return shape
363 |
364 | def boundedShiftShape(self, shape):
365 | # Try to move in one direction, and if it fails in another.
366 | # Give up if both fail.
367 | point = shape[0]
368 | offset = QPointF(2.0, 2.0)
369 | self.calculateOffsets(shape, point)
370 | self.prevPoint = point
371 | if not self.boundedMoveShape(shape, point - offset):
372 | self.boundedMoveShape(shape, point + offset)
373 |
374 | def paintEvent(self, event):
375 | if not self.pixmap:
376 | return super(Canvas, self).paintEvent(event)
377 |
378 | p = self._painter
379 | p.begin(self)
380 | p.setRenderHint(QPainter.Antialiasing)
381 | p.setRenderHint(QPainter.HighQualityAntialiasing)
382 | p.setRenderHint(QPainter.SmoothPixmapTransform)
383 |
384 | p.scale(self.scale, self.scale)
385 | p.translate(self.offsetToCenter())
386 |
387 | p.drawPixmap(0, 0, self.pixmap)
388 | Shape.scale = self.scale
389 | for shape in self.shapes:
390 | if (shape.selected or not self._hideBackround) and self.isVisible(shape):
391 | shape.fill = shape.selected or shape == self.hShape
392 | shape.paint(p)
393 | if self.current:
394 | self.current.paint(p)
395 | self.line.paint(p)
396 | if self.selectedShapeCopy:
397 | self.selectedShapeCopy.paint(p)
398 |
399 | # Paint rect
400 | if self.current is not None and len(self.line) == 2:
401 | leftTop = self.line[0]
402 | rightBottom = self.line[1]
403 | rectWidth = rightBottom.x() - leftTop.x()
404 | rectHeight = rightBottom.y() - leftTop.y()
405 | color = QColor(0, 220, 0)
406 | p.setPen(color)
407 | brush = QBrush(Qt.BDiagPattern)
408 | p.setBrush(brush)
409 | p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight)
410 |
411 | p.end()
412 |
413 | def transformPos(self, point):
414 | """Convert from widget-logical coordinates to painter-logical coordinates."""
415 | return point / self.scale - self.offsetToCenter()
416 |
417 | def offsetToCenter(self):
418 | s = self.scale
419 | area = super(Canvas, self).size()
420 | w, h = self.pixmap.width() * s, self.pixmap.height() * s
421 | aw, ah = area.width(), area.height()
422 | x = (aw-w)/(2*s) if aw > w else 0
423 | y = (ah-h)/(2*s) if ah > h else 0
424 | return QPointF(x, y)
425 |
426 | def outOfPixmap(self, p):
427 | w, h = self.pixmap.width(), self.pixmap.height()
428 | return not (0 <= p.x() <= w and 0 <= p.y() <= h)
429 |
430 | def finalise(self):
431 | assert self.current
432 | self.current.close()
433 | self.shapes.append(self.current)
434 | self.current = None
435 | self.setHiding(False)
436 | self.newShape.emit()
437 | self.update()
438 |
439 | def closeEnough(self, p1, p2):
440 | #d = distance(p1 - p2)
441 | #m = (p1-p2).manhattanLength()
442 | #print "d %.2f, m %d, %.2f" % (d, m, d - m)
443 | return distance(p1 - p2) < self.epsilon
444 |
445 | def intersectionPoint(self, p1, p2):
446 | # Cycle through each image edge in clockwise fashion,
447 | # and find the one intersecting the current line segment.
448 | # http://paulbourke.net/geometry/lineline2d/
449 | size = self.pixmap.size()
450 | points = [(0,0),
451 | (size.width(), 0),
452 | (size.width(), size.height()),
453 | (0, size.height())]
454 | x1, y1 = p1.x(), p1.y()
455 | x2, y2 = p2.x(), p2.y()
456 | d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))
457 | x3, y3 = points[i]
458 | x4, y4 = points[(i+1)%4]
459 | if (x, y) == (x1, y1):
460 | # Handle cases where previous point is on one of the edges.
461 | if x3 == x4:
462 | return QPointF(x3, min(max(0, y2), max(y3, y4)))
463 | else: # y3 == y4
464 | return QPointF(min(max(0, x2), max(x3, x4)), y3)
465 | return QPointF(x, y)
466 |
467 | def intersectingEdges(self, x1y1, x2y2, points):
468 | """For each edge formed by `points', yield the intersection
469 | with the line segment `(x1,y1) - (x2,y2)`, if it exists.
470 | Also return the distance of `(x2,y2)' to the middle of the
471 | edge along with its index, so that the one closest can be chosen."""
472 | x1, y1 = x1y1
473 | x2, y2 = x2y2
474 | for i in range(4):
475 | x3, y3 = points[i]
476 | x4, y4 = points[(i+1) % 4]
477 | denom = (y4-y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
478 | nua = (x4-x3) * (y1-y3) - (y4-y3) * (x1-x3)
479 | nub = (x2-x1) * (y1-y3) - (y2-y1) * (x1-x3)
480 | if denom == 0:
481 | # This covers two cases:
482 | # nua == nub == 0: Coincident
483 | # otherwise: Parallel
484 | continue
485 | ua, ub = nua / denom, nub / denom
486 | if 0 <= ua <= 1 and 0 <= ub <= 1:
487 | x = x1 + ua * (x2 - x1)
488 | y = y1 + ua * (y2 - y1)
489 | m = QPointF((x3 + x4)/2, (y3 + y4)/2)
490 | d = distance(m - QPointF(x2, y2))
491 | yield d, i, (x, y)
492 |
493 | # These two, along with a call to adjustSize are required for the
494 | # scroll area.
495 | def sizeHint(self):
496 | return self.minimumSizeHint()
497 |
498 | def minimumSizeHint(self):
499 | if self.pixmap:
500 | return self.scale * self.pixmap.size()
501 | return super(Canvas, self).minimumSizeHint()
502 |
503 | def wheelEvent(self, ev):
504 | if ev.orientation() == Qt.Vertical:
505 | mods = ev.modifiers()
506 | if Qt.ControlModifier == int(mods):
507 | self.zoomRequest.emit(ev.delta())
508 | else:
509 | self.scrollRequest.emit(ev.delta(),
510 | Qt.Horizontal if (Qt.ShiftModifier == int(mods))\
511 | else Qt.Vertical)
512 | else:
513 | self.scrollRequest.emit(ev.delta(), Qt.Horizontal)
514 | ev.accept()
515 |
516 | def keyPressEvent(self, ev):
517 | key = ev.key()
518 | if key == Qt.Key_Escape and self.current:
519 | print('ESC press')
520 | self.current = None
521 | self.drawingPolygon.emit(False)
522 | self.update()
523 | elif key == Qt.Key_Return and self.canCloseShape():
524 | self.finalise()
525 |
526 | def setLastLabel(self, text):
527 | assert text
528 | self.shapes[-1].label = text
529 | return self.shapes[-1]
530 |
531 | def undoLastLine(self):
532 | assert self.shapes
533 | self.current = self.shapes.pop()
534 | self.current.setOpen()
535 | self.line.points = [self.current[-1], self.current[0]]
536 | self.drawingPolygon.emit(True)
537 |
538 | def resetAllLines(self):
539 | assert self.shapes
540 | self.current = self.shapes.pop()
541 | self.current.setOpen()
542 | self.line.points = [self.current[-1], self.current[0]]
543 | self.drawingPolygon.emit(True)
544 | self.current = None
545 | self.drawingPolygon.emit(False)
546 | self.update()
547 |
548 | def loadPixmap(self, pixmap):
549 | self.pixmap = pixmap
550 | self.shapes = []
551 | self.repaint()
552 |
553 | def loadShapes(self, shapes):
554 | self.shapes = list(shapes)
555 | self.current = None
556 | self.repaint()
557 |
558 | def setShapeVisible(self, shape, value):
559 | self.visible[shape] = value
560 | self.repaint()
561 |
562 | def overrideCursor(self, cursor):
563 | self.restoreCursor()
564 | self._cursor = cursor
565 | QApplication.setOverrideCursor(cursor)
566 |
567 | def restoreCursor(self):
568 | QApplication.restoreOverrideCursor()
569 |
570 | def resetState(self):
571 | self.restoreCursor()
572 | self.pixmap = None
573 | self.update()
574 |
575 |
--------------------------------------------------------------------------------
/libs/colorDialog.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import QColorDialog, QDialogButtonBox
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 | BB = QDialogButtonBox
10 |
11 | class ColorDialog(QColorDialog):
12 | def __init__(self, parent=None):
13 | super(ColorDialog, self).__init__(parent)
14 | self.setOption(QColorDialog.ShowAlphaChannel)
15 | # The Mac native dialog does not support our restore button.
16 | self.setOption(QColorDialog.DontUseNativeDialog)
17 | ## Add a restore defaults button.
18 | # The default is set at invocation time, so that it
19 | # works across dialogs for different elements.
20 | self.default = None
21 | self.bb = self.layout().itemAt(1).widget()
22 | self.bb.addButton(BB.RestoreDefaults)
23 | self.bb.clicked.connect(self.checkRestore)
24 |
25 | def getColor(self, value=None, title=None, default=None):
26 | self.default = default
27 | if title:
28 | self.setWindowTitle(title)
29 | if value:
30 | self.setCurrentColor(value)
31 | return self.currentColor() if self.exec_() else None
32 |
33 | def checkRestore(self, button):
34 | if self.bb.buttonRole(button) & BB.ResetRole and self.default:
35 | self.setCurrentColor(self.default)
36 |
37 |
--------------------------------------------------------------------------------
/libs/labelDialog.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import *
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 | from lib import newIcon, labelValidator
10 |
11 | BB = QDialogButtonBox
12 |
13 | class LabelDialog(QDialog):
14 |
15 | def __init__(self, text="Enter object label", parent=None, listItem=None):
16 | super(LabelDialog, self).__init__(parent)
17 | self.edit = QLineEdit()
18 | self.edit.setText(text)
19 | self.edit.setValidator(labelValidator())
20 | self.edit.editingFinished.connect(self.postProcess)
21 | layout = QVBoxLayout()
22 | layout.addWidget(self.edit)
23 | self.buttonBox = bb = BB(BB.Ok | BB.Cancel, Qt.Horizontal, self)
24 | bb.button(BB.Ok).setIcon(newIcon('done'))
25 | bb.button(BB.Cancel).setIcon(newIcon('undo'))
26 | bb.accepted.connect(self.validate)
27 | bb.rejected.connect(self.reject)
28 | layout.addWidget(bb)
29 |
30 | if listItem is not None and len(listItem) > 0:
31 | self.listWidget = QListWidget(self)
32 | for item in listItem:
33 | self.listWidget.addItem(item)
34 | self.listWidget.itemDoubleClicked.connect(self.listItemClick)
35 | layout.addWidget(self.listWidget)
36 |
37 | self.setLayout(layout)
38 |
39 | def validate(self):
40 | try:
41 | if self.edit.text().trimmed():
42 | self.accept()
43 | except AttributeError:
44 | # PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
45 | if self.edit.text().strip():
46 | self.accept()
47 |
48 | def postProcess(self):
49 | try:
50 | self.edit.setText(self.edit.text().trimmed())
51 | except AttributeError:
52 | # PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
53 | self.edit.setText(self.edit.text())
54 |
55 | def popUp(self, text='', move=True):
56 | self.edit.setText(text)
57 | self.edit.setSelection(0, len(text))
58 | self.edit.setFocus(Qt.PopupFocusReason)
59 | if move:
60 | self.move(QCursor.pos())
61 | return self.edit.text() if self.exec_() else None
62 |
63 | def listItemClick(self, tQListWidgetItem):
64 | try:
65 | text = tQListWidgetItem.text().trimmed()
66 | except AttributeError:
67 | # PyQt5: AttributeError: 'str' object has no attribute 'trimmed'
68 | text = tQListWidgetItem.text().strip()
69 | self.edit.setText(text)
70 | self.validate()
71 |
--------------------------------------------------------------------------------
/libs/labelFile.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2016 Tzutalin
2 | # Create by TzuTaLin
3 |
4 | try:
5 | from PyQt5.QtGui import QImage
6 | except ImportError:
7 | from PyQt4.QtGui import QImage
8 |
9 | from base64 import b64encode, b64decode
10 | from pascal_voc_io import PascalVocWriter
11 | import os.path
12 | import sys
13 |
14 | class LabelFileError(Exception):
15 | pass
16 |
17 | class LabelFile(object):
18 | # It might be changed as window creates
19 | suffix = '.lif'
20 |
21 | def __init__(self, filename=None):
22 | self.shapes = ()
23 | self.imagePath = None
24 | self.imageData = None
25 | if filename is not None:
26 | self.load(filename)
27 |
28 | def savePascalVocFormat(self, filename, shapes, imagePath, imageData,
29 | lineColor=None, fillColor=None, databaseSrc=None):
30 | imgFolderPath = os.path.dirname(imagePath)
31 | imgFolderName = os.path.split(imgFolderPath)[-1]
32 | imgFileName = os.path.basename(imagePath)
33 | imgFileNameWithoutExt = os.path.splitext(imgFileName)[0]
34 | # Read from file path because self.imageData might be empty if saving to
35 | # Pascal format
36 | image = QImage()
37 | image.load(imagePath)
38 | imageShape = [image.height(), image.width(), 1 if image.isGrayscale() else 3]
39 | writer = PascalVocWriter(imgFolderName, imgFileNameWithoutExt,\
40 | imageShape, localImgPath=imagePath)
41 | bSave = False
42 | for shape in shapes:
43 | points = shape['points']
44 | label = shape['label']
45 | bndbox = LabelFile.convertPoints2BndBox(points)
46 | writer.addBndBox(bndbox[0], bndbox[1], bndbox[2], bndbox[3], label)
47 | bSave = True
48 |
49 | if bSave:
50 | writer.save(targetFile = filename)
51 | return
52 |
53 | @staticmethod
54 | def isLabelFile(filename):
55 | fileSuffix = os.path.splitext(filename)[1].lower()
56 | return fileSuffix == LabelFile.suffix
57 |
58 | @staticmethod
59 | def convertPoints2BndBox(points):
60 | xmin = float('inf')
61 | ymin = float('inf')
62 | xmax = float('-inf')
63 | ymax = float('-inf')
64 | for p in points:
65 | x = p[0]
66 | y = p[1]
67 | xmin = min(x,xmin)
68 | ymin = min(y,ymin)
69 | xmax = max(x,xmax)
70 | ymax = max(y,ymax)
71 |
72 | # Martin Kersner, 2015/11/12
73 | # 0-valued coordinates of BB caused an error while
74 | # training faster-rcnn object detector.
75 | if (xmin < 1):
76 | xmin = 1
77 |
78 | if (ymin < 1):
79 | ymin = 1
80 |
81 | return (int(xmin), int(ymin), int(xmax), int(ymax))
82 |
--------------------------------------------------------------------------------
/libs/lib.py:
--------------------------------------------------------------------------------
1 | from math import sqrt
2 |
3 | try:
4 | from PyQt5.QtGui import *
5 | from PyQt5.QtCore import *
6 | from PyQt5.QtWidgets import *
7 | except ImportError:
8 | from PyQt4.QtGui import *
9 | from PyQt4.QtCore import *
10 |
11 |
12 | def newIcon(icon):
13 | return QIcon(':/' + icon)
14 |
15 | def newButton(text, icon=None, slot=None):
16 | b = QPushButton(text)
17 | if icon is not None:
18 | b.setIcon(newIcon(icon))
19 | if slot is not None:
20 | b.clicked.connect(slot)
21 | return b
22 |
23 | def newAction(parent, text, slot=None, shortcut=None, icon=None,
24 | tip=None, checkable=False, enabled=True):
25 | """Create a new action and assign callbacks, shortcuts, etc."""
26 | a = QAction(text, parent)
27 | if icon is not None:
28 | a.setIcon(newIcon(icon))
29 | if shortcut is not None:
30 | if isinstance(shortcut, (list, tuple)):
31 | a.setShortcuts(shortcut)
32 | else:
33 | a.setShortcut(shortcut)
34 | if tip is not None:
35 | a.setToolTip(tip)
36 | a.setStatusTip(tip)
37 | if slot is not None:
38 | a.triggered.connect(slot)
39 | if checkable:
40 | a.setCheckable(True)
41 | a.setEnabled(enabled)
42 | return a
43 |
44 |
45 | def addActions(widget, actions):
46 | for action in actions:
47 | if action is None:
48 | widget.addSeparator()
49 | elif isinstance(action, QMenu):
50 | widget.addMenu(action)
51 | else:
52 | widget.addAction(action)
53 |
54 | def labelValidator():
55 | return QRegExpValidator(QRegExp(r'^[^ \t].+'), None)
56 |
57 |
58 | class struct(object):
59 | def __init__(self, **kwargs):
60 | self.__dict__.update(kwargs)
61 |
62 | def distance(p):
63 | return sqrt(p.x() * p.x() + p.y() * p.y())
64 |
65 | def fmtShortcut(text):
66 | mod, key = text.split('+', 1)
67 | return '%s+%s' % (mod, key)
68 |
69 |
--------------------------------------------------------------------------------
/libs/pascal_voc_io.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf8 -*-
3 | import _init_path
4 | import sys
5 | from xml.etree import ElementTree
6 | from xml.etree.ElementTree import Element, SubElement
7 | from lxml import etree
8 |
9 |
10 | class PascalVocWriter:
11 |
12 | def __init__(self, foldername, filename, imgSize, databaseSrc='Unknown', localImgPath=None):
13 | self.foldername = foldername
14 | self.filename = filename
15 | self.databaseSrc = databaseSrc
16 | self.imgSize = imgSize
17 | self.boxlist = []
18 | self.localImgPath = localImgPath
19 |
20 | def prettify(self, elem):
21 | """
22 | Return a pretty-printed XML string for the Element.
23 | """
24 | rough_string = ElementTree.tostring(elem, 'utf8')
25 | root = etree.fromstring(rough_string)
26 | return etree.tostring(root, pretty_print=True)
27 |
28 | def genXML(self):
29 | """
30 | Return XML root
31 | """
32 | # Check conditions
33 | if self.filename is None or \
34 | self.foldername is None or \
35 | self.imgSize is None or \
36 | len(self.boxlist) <= 0:
37 | return None
38 |
39 | top = Element('annotation')
40 | folder = SubElement(top, 'folder')
41 | folder.text = self.foldername
42 |
43 | filename = SubElement(top, 'filename')
44 | filename.text = self.filename
45 |
46 | localImgPath = SubElement(top, 'path')
47 | localImgPath.text = self.localImgPath
48 |
49 | source = SubElement(top, 'source')
50 | database = SubElement(source, 'database')
51 | database.text = self.databaseSrc
52 |
53 | size_part = SubElement(top, 'size')
54 | width = SubElement(size_part, 'width')
55 | height = SubElement(size_part, 'height')
56 | depth = SubElement(size_part, 'depth')
57 | width.text = str(self.imgSize[1])
58 | height.text = str(self.imgSize[0])
59 | if len(self.imgSize) == 3:
60 | depth.text = str(self.imgSize[2])
61 | else:
62 | depth.text = '1'
63 |
64 | segmented = SubElement(top, 'segmented')
65 | segmented.text = '0'
66 | return top
67 |
68 | def addBndBox(self, xmin, ymin, xmax, ymax, name):
69 | bndbox = {'xmin': xmin, 'ymin': ymin, 'xmax': xmax, 'ymax': ymax}
70 | bndbox['name'] = name
71 | self.boxlist.append(bndbox)
72 |
73 | def appendObjects(self, top):
74 | for each_object in self.boxlist:
75 | object_item = SubElement(top, 'object')
76 | name = SubElement(object_item, 'name')
77 | try:
78 | name.text = unicode(each_object['name'])
79 | except NameError:
80 | # Py3: NameError: name 'unicode' is not defined
81 | name.text = each_object['name']
82 | pose = SubElement(object_item, 'pose')
83 | pose.text = "Unspecified"
84 | truncated = SubElement(object_item, 'truncated')
85 | truncated.text = "0"
86 | difficult = SubElement(object_item, 'difficult')
87 | difficult.text = "0"
88 | bndbox = SubElement(object_item, 'bndbox')
89 | xmin = SubElement(bndbox, 'xmin')
90 | xmin.text = str(each_object['xmin'])
91 | ymin = SubElement(bndbox, 'ymin')
92 | ymin.text = str(each_object['ymin'])
93 | xmax = SubElement(bndbox, 'xmax')
94 | xmax.text = str(each_object['xmax'])
95 | ymax = SubElement(bndbox, 'ymax')
96 | ymax.text = str(each_object['ymax'])
97 |
98 | def save(self, targetFile=None):
99 | root = self.genXML()
100 | self.appendObjects(root)
101 | out_file = None
102 | if targetFile is None:
103 | out_file = open(self.filename + '.xml', 'w')
104 | else:
105 | out_file = open(targetFile, 'w')
106 |
107 | prettifyResult = self.prettify(root)
108 | out_file.write(prettifyResult.decode('utf8'))
109 | out_file.close()
110 |
111 |
112 | class PascalVocReader:
113 |
114 | def __init__(self, filepath):
115 | # shapes type:
116 | # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color]
117 | self.shapes = []
118 | self.filepath = filepath
119 | self.parseXML()
120 |
121 | def getShapes(self):
122 | return self.shapes
123 |
124 | def addShape(self, label, bndbox):
125 | xmin = int(bndbox.find('xmin').text)
126 | ymin = int(bndbox.find('ymin').text)
127 | xmax = int(bndbox.find('xmax').text)
128 | ymax = int(bndbox.find('ymax').text)
129 | points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
130 | self.shapes.append((label, points, None, None))
131 |
132 | def parseXML(self):
133 | assert self.filepath.endswith('.xml'), "Unsupport file format"
134 | parser = etree.XMLParser(encoding='utf-8')
135 | xmltree = ElementTree.parse(self.filepath, parser=parser).getroot()
136 | filename = xmltree.find('filename').text
137 |
138 | for object_iter in xmltree.findall('object'):
139 | bndbox = object_iter.find("bndbox")
140 | label = object_iter.find('name').text
141 | self.addShape(label, bndbox)
142 | return True
143 |
144 |
145 | # tempParseReader = PascalVocReader('test.xml')
146 | # print tempParseReader.getShapes()
147 | """
148 | # Test
149 | tmp = PascalVocWriter('temp','test', (10,20,3))
150 | tmp.addBndBox(10,10,20,30,'chair')
151 | tmp.addBndBox(1,1,600,600,'car')
152 | tmp.save()
153 | """
154 |
--------------------------------------------------------------------------------
/libs/shape.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | try:
6 | from PyQt5.QtGui import *
7 | from PyQt5.QtCore import *
8 | except ImportError:
9 | from PyQt4.QtGui import *
10 | from PyQt4.QtCore import *
11 |
12 | from lib import distance
13 |
14 | DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128)
15 | DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128)
16 | DEFAULT_SELECT_LINE_COLOR = QColor(255, 255, 255)
17 | DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 155)
18 | DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255)
19 | DEFAULT_HVERTEX_FILL_COLOR = QColor(255, 0, 0)
20 |
21 | class Shape(object):
22 | P_SQUARE, P_ROUND = range(2)
23 |
24 | MOVE_VERTEX, NEAR_VERTEX = range(2)
25 |
26 | ## The following class variables influence the drawing
27 | ## of _all_ shape objects.
28 | line_color = DEFAULT_LINE_COLOR
29 | fill_color = DEFAULT_FILL_COLOR
30 | select_line_color = DEFAULT_SELECT_LINE_COLOR
31 | select_fill_color = DEFAULT_SELECT_FILL_COLOR
32 | vertex_fill_color = DEFAULT_VERTEX_FILL_COLOR
33 | hvertex_fill_color = DEFAULT_HVERTEX_FILL_COLOR
34 | point_type = P_ROUND
35 | point_size = 8
36 | scale = 1.0
37 |
38 | def __init__(self, label=None, line_color=None):
39 | self.label = label
40 | self.points = []
41 | self.fill = False
42 | self.selected = False
43 |
44 | self._highlightIndex = None
45 | self._highlightMode = self.NEAR_VERTEX
46 | self._highlightSettings = {
47 | self.NEAR_VERTEX: (4, self.P_ROUND),
48 | self.MOVE_VERTEX: (1.5, self.P_SQUARE),
49 | }
50 |
51 | self._closed = False
52 |
53 | if line_color is not None:
54 | # Override the class line_color attribute
55 | # with an object attribute. Currently this
56 | # is used for drawing the pending line a different color.
57 | self.line_color = line_color
58 |
59 | def close(self):
60 | assert len(self.points) > 2
61 | self._closed = True
62 |
63 | def reachMaxPoints(self):
64 | if len(self.points) >=4:
65 | return True
66 | return False
67 |
68 | def addPoint(self, point):
69 | if self.points and point == self.points[0]:
70 | self.close()
71 | else:
72 | self.points.append(point)
73 |
74 | def popPoint(self):
75 | if self.points:
76 | return self.points.pop()
77 | return None
78 |
79 | def isClosed(self):
80 | return self._closed
81 |
82 | def setOpen(self):
83 | self._closed = False
84 |
85 | def paint(self, painter):
86 | if self.points:
87 | color = self.select_line_color if self.selected else self.line_color
88 | pen = QPen(color)
89 | # Try using integer sizes for smoother drawing(?)
90 | pen.setWidth(max(1, int(round(2.0 / self.scale))))
91 | painter.setPen(pen)
92 |
93 | line_path = QPainterPath()
94 | vrtx_path = QPainterPath()
95 |
96 | line_path.moveTo(self.points[0])
97 | # Uncommenting the following line will draw 2 paths
98 | # for the 1st vertex, and make it non-filled, which
99 | # may be desirable.
100 | #self.drawVertex(vrtx_path, 0)
101 |
102 | for i, p in enumerate(self.points):
103 | line_path.lineTo(p)
104 | self.drawVertex(vrtx_path, i)
105 | if self.isClosed():
106 | line_path.lineTo(self.points[0])
107 |
108 | painter.drawPath(line_path)
109 | painter.drawPath(vrtx_path)
110 | painter.fillPath(vrtx_path, self.vertex_fill_color)
111 | if self.fill:
112 | color = self.select_fill_color if self.selected else self.fill_color
113 | painter.fillPath(line_path, color)
114 |
115 | def drawVertex(self, path, i):
116 | d = self.point_size / self.scale
117 | shape = self.point_type
118 | point = self.points[i]
119 | if i == self._highlightIndex:
120 | size, shape = self._highlightSettings[self._highlightMode]
121 | d *= size
122 | if self._highlightIndex is not None:
123 | self.vertex_fill_color = self.hvertex_fill_color
124 | else:
125 | self.vertex_fill_color = Shape.vertex_fill_color
126 | if shape == self.P_SQUARE:
127 | path.addRect(point.x() - d/2, point.y() - d/2, d, d)
128 | elif shape == self.P_ROUND:
129 | path.addEllipse(point, d/2.0, d/2.0)
130 | else:
131 | assert False, "unsupported vertex shape"
132 |
133 | def nearestVertex(self, point, epsilon):
134 | for i, p in enumerate(self.points):
135 | if distance(p - point) <= epsilon:
136 | return i
137 | return None
138 |
139 | def containsPoint(self, point):
140 | return self.makePath().contains(point)
141 |
142 | def makePath(self):
143 | path = QPainterPath(self.points[0])
144 | for p in self.points[1:]:
145 | path.lineTo(p)
146 | return path
147 |
148 | def boundingRect(self):
149 | return self.makePath().boundingRect()
150 |
151 | def moveBy(self, offset):
152 | self.points = [p + offset for p in self.points]
153 |
154 | def moveVertexBy(self, i, offset):
155 | self.points[i] = self.points[i] + offset
156 |
157 | def highlightVertex(self, i, action):
158 | self._highlightIndex = i
159 | self._highlightMode = action
160 |
161 | def highlightClear(self):
162 | self._highlightIndex = None
163 |
164 | def copy(self):
165 | shape = Shape("Copy of %s" % self.label )
166 | shape.points= [p for p in self.points]
167 | shape.fill = self.fill
168 | shape.selected = self.selected
169 | shape._closed = self._closed
170 | if self.line_color != Shape.line_color:
171 | shape.line_color = self.line_color
172 | if self.fill_color != Shape.fill_color:
173 | shape.fill_color = self.fill_color
174 | return shape
175 |
176 | def __len__(self):
177 | return len(self.points)
178 |
179 | def __getitem__(self, key):
180 | return self.points[key]
181 |
182 | def __setitem__(self, key, value):
183 | self.points[key] = value
184 |
185 |
--------------------------------------------------------------------------------
/libs/toolBar.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import *
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 |
10 | class ToolBar(QToolBar):
11 | def __init__(self, title):
12 | super(ToolBar, self).__init__(title)
13 | layout = self.layout()
14 | m = (0, 0, 0, 0)
15 | layout.setSpacing(0)
16 | layout.setContentsMargins(*m)
17 | self.setContentsMargins(*m)
18 | self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
19 |
20 | def addAction(self, action):
21 | if isinstance(action, QWidgetAction):
22 | return super(ToolBar, self).addAction(action)
23 | btn = ToolButton()
24 | btn.setDefaultAction(action)
25 | btn.setToolButtonStyle(self.toolButtonStyle())
26 | self.addWidget(btn)
27 |
28 |
29 | class ToolButton(QToolButton):
30 | """ToolBar companion class which ensures all buttons have the same size."""
31 | minSize = (60, 60)
32 | def minimumSizeHint(self):
33 | ms = super(ToolButton, self).minimumSizeHint()
34 | w1, h1 = ms.width(), ms.height()
35 | w2, h2 = self.minSize
36 | ToolButton.minSize = max(w1, w2), max(h1, h2)
37 | return QSize(*ToolButton.minSize)
38 |
39 |
--------------------------------------------------------------------------------
/libs/zoomWidget.py:
--------------------------------------------------------------------------------
1 | try:
2 | from PyQt5.QtGui import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtWidgets import *
5 | except ImportError:
6 | from PyQt4.QtGui import *
7 | from PyQt4.QtCore import *
8 |
9 | class ZoomWidget(QSpinBox):
10 | def __init__(self, value=100):
11 | super(ZoomWidget, self).__init__()
12 | self.setButtonSymbols(QAbstractSpinBox.NoButtons)
13 | self.setRange(1, 500)
14 | self.setSuffix(' %')
15 | self.setValue(value)
16 | self.setToolTip(u'Zoom Level')
17 | self.setStatusTip(self.toolTip())
18 | self.setAlignment(Qt.AlignCenter)
19 |
20 | def minimumSizeHint(self):
21 | height = super(ZoomWidget, self).minimumSizeHint().height()
22 | fm = QFontMetrics(self.font())
23 | width = fm.width(str(self.maximum()))
24 | return QSize(width, height)
25 |
26 |
--------------------------------------------------------------------------------
/read_xml.py:
--------------------------------------------------------------------------------
1 | import lxml.etree as et
2 | import xml.etree.ElementTree as ET
3 | import math
4 |
5 |
6 | def rotate_xml(filename, Alpha):
7 | """ Parse a PASCAL VOC xml file """
8 | Alpha=math.radians(Alpha)
9 | tree = ET.parse(filename)
10 | objects = []
11 | for obj in tree.findall('object'):
12 | # if not check(obj.find('name').text):
13 | # continue
14 | obj_struct = {}
15 | obj_struct['name'] = obj.find('name').text
16 | bbox = obj.find('bndbox')
17 | obj_struct['bbox'] = [int(bbox.find('xmin').text) - 1,
18 | int(bbox.find('ymin').text) - 1,
19 | int(bbox.find('xmax').text) - 1,
20 | int(bbox.find('ymax').text) - 1]
21 | # Read the original coordination
22 | x1=int(bbox.find('xmin').text)
23 | y1=int(bbox.find('ymin').text)
24 | x2=int(bbox.find('xmax').text)
25 | y2=int(bbox.find('ymax').text)
26 | # Transformation
27 | x1_n=x1+ int(y1*math.sin(Alpha))
28 | y1_n=y1-int(x1*math.sin(Alpha))
29 | x2_n=x2+int(y2*math.sin(Alpha))
30 | y2_n=y2-int(x2*math.sin(Alpha))
31 | print x1_n, y1_n, x2_n, y2_n, math.sin(Alpha), math.cos(Alpha)
32 | x1_new=x1_n
33 | y1_new=y1_n-int((x2-x1)*math.sin(Alpha))
34 | x2_new=x1_n+int((x2-x1)*math.cos(Alpha)+(y2-y1)*math.sin(Alpha))
35 | y2_new=y1_n+int((y2-y1)*math.cos(Alpha))
36 | bbox.find('xmin').text=str(x1_new)
37 | bbox.find('ymin').text=str(y1_new)
38 | bbox.find('xmax').text=str(x2_new)
39 | bbox.find('ymax').text=str(y2_new)
40 | tree.write(filename)
41 | rotate_xml("test2.xml", 5)
--------------------------------------------------------------------------------
/read_xml_correct_rotation.py:
--------------------------------------------------------------------------------
1 | import lxml.etree as et
2 | import xml.etree.ElementTree as ET
3 | import math
4 | import cv2
5 | import numpy as numpy
6 | import imutils
7 |
8 | from shutil import copyfile
9 |
10 | def copy_file(file_input, file_output):
11 | copyfile(file_input, file_output)
12 | return file_output
13 | def rotate_file_image(filename, Alpha):
14 | img=cv2.imread(filename,1)
15 | dst=imutils.rotate_bound(img,Alpha)
16 | w_n,h_n,d_n=dst.shape
17 | file_rotate_image=str(Alpha)+filename
18 | cv2.imwrite(file_rotate_image,dst)
19 | file_annotate=filename.replace("png", "xml")
20 | file_annotate_rotate=file_rotate_image.replace("png", "xml")
21 | copy_file(file_annotate,file_annotate_rotate)
22 | rotate_xml(file_annotate_rotate, Alpha,w_n, h_n)
23 |
24 | def rotate_xml(filename, Alpha, w,h):
25 | """ Parse a PASCAL VOC xml file """
26 | Alpha=math.radians(Alpha)
27 | tree = ET.parse(filename)
28 | objects = []
29 |
30 |
31 | for st in tree.findall('size'):
32 | w=int(st.find("width").text) -1
33 | h=int(st.find("height").text) -1
34 | h_n=int(w*math.sin(Alpha) + h*math.cos(Alpha))
35 | w_n=int(h*math.sin(Alpha) + w*math.cos(Alpha))
36 | st.find("width").text=str(w_n)
37 | st.find("height").text=str(h_n)
38 | tree.write(filename)
39 |
40 | for obj in tree.findall('object'):
41 | # if not check(obj.find('name').text):
42 | # continue
43 | obj_struct = {}
44 | obj_struct['name'] = obj.find('name').text
45 | bbox = obj.find('bndbox')
46 | obj_struct['bbox'] = [int(bbox.find('xmin').text) - 1,
47 | int(bbox.find('ymin').text) - 1,
48 | int(bbox.find('xmax').text) - 1,
49 | int(bbox.find('ymax').text) - 1]
50 | # Read the original coordination
51 | x1=int(bbox.find('xmin').text)
52 | y1=int(bbox.find('ymin').text)
53 | x2=int(bbox.find('xmax').text)
54 | y2=int(bbox.find('ymax').text)
55 | # Transformation
56 | y1_t=int(y1*math.cos(Alpha) + x1*math.sin(Alpha))
57 | x1_t=int(x1*math.cos(Alpha) + y1*math.sin(Alpha))
58 | x2_t=int(x2*math.cos(Alpha) + y2*math.sin(Alpha))
59 | y2_t=int(y2*math.cos(Alpha) + x2*math.sin(Alpha))
60 |
61 |
62 | x1_n=int((x1*math.cos(Alpha)-y1*math.sin(Alpha)) + (w_n-y2*(1-math.tan(Alpha)))*math.sin(Alpha))
63 | x2_n=x1_n+int((x2-x1)*math.cos(Alpha)+(y2-y1)*math.sin(Alpha))
64 | y1_n=y1_t
65 | y2_n=y2_t
66 | if(y2_n>w_n):
67 | y2_n=w_n
68 | if(x2_n>h_n):
69 | x2_n=h_n
70 | bbox.find('xmin').text=str(x1_n)
71 | bbox.find('ymin').text=str(y1_n)
72 | bbox.find('xmax').text=str(x2_n)
73 | bbox.find('ymax').text=str(y2_n)
74 | tree.write(filename)
75 |
76 |
77 | file_image="new1.png"
78 | rotate_file_image(file_image,5)
79 |
80 |
81 | #rotate_xml("test2.xml", 5)
--------------------------------------------------------------------------------
/read_xml_correct_rotation_left.py:
--------------------------------------------------------------------------------
1 | import lxml.etree as et
2 | import xml.etree.ElementTree as ET
3 | import math
4 | import cv2
5 | import numpy as numpy
6 | import imutils
7 |
8 | from shutil import copyfile
9 |
10 | def copy_file(file_input, file_output):
11 | copyfile(file_input, file_output)
12 | return file_output
13 | def rotate_file_image(filename, Alpha):
14 | img=cv2.imread(filename,1)
15 | dst=imutils.rotate_bound(img,Alpha)
16 | w_n,h_n,d_n=dst.shape
17 | file_rotate_image=str(Alpha)+filename
18 | Alpha=-1*Alpha
19 | cv2.imwrite(file_rotate_image,dst)
20 | file_annotate=filename.replace("png", "xml")
21 | file_annotate_rotate=file_rotate_image.replace("png", "xml")
22 | copy_file(file_annotate,file_annotate_rotate)
23 | rotate_xml(file_annotate_rotate, Alpha,w_n, h_n)
24 |
25 | def rotate_xml(filename, Alpha, w,h):
26 | """ Parse a PASCAL VOC xml file """
27 | Alpha=math.radians(Alpha)
28 | tree = ET.parse(filename)
29 | objects = []
30 |
31 |
32 | for st in tree.findall('size'):
33 | w=int(st.find("width").text) -1
34 | h=int(st.find("height").text) -1
35 | h_n=int(w*math.sin(Alpha) + h*math.cos(Alpha))
36 | w_n=int(h*math.sin(Alpha) + w*math.cos(Alpha))
37 | st.find("width").text=str(w_n)
38 | st.find("height").text=str(h_n)
39 | tree.write(filename)
40 |
41 | for obj in tree.findall('object'):
42 | # if not check(obj.find('name').text):
43 | # continue
44 | obj_struct = {}
45 | obj_struct['name'] = obj.find('name').text
46 | bbox = obj.find('bndbox')
47 | obj_struct['bbox'] = [int(bbox.find('xmin').text) - 1,
48 | int(bbox.find('ymin').text) - 1,
49 | int(bbox.find('xmax').text) - 1,
50 | int(bbox.find('ymax').text) - 1]
51 | # Read the original coordination
52 | x1=int(bbox.find('xmin').text)
53 | y1=int(bbox.find('ymin').text)
54 | x2=int(bbox.find('xmax').text)
55 | y2=int(bbox.find('ymax').text)
56 | # Transformation
57 | y1_t=int(y1*math.cos(Alpha) + x1*math.sin(Alpha))
58 | x1_t=int(x1*math.cos(Alpha) + y1*math.sin(Alpha))
59 | x2_t=int(x2*math.cos(Alpha) + y2*math.sin(Alpha))
60 | y2_t=int(y2*math.cos(Alpha) + x2*math.sin(Alpha))
61 |
62 | # Transformation
63 |
64 |
65 | #x1_n=int((x1*math.cos(Alpha)-y1*math.sin(Alpha)) - (w_n-y2*(1-math.tan(Alpha)))*math.sin(Alpha))
66 | #x2_n=x1_n+int((x2-x1)*math.cos(Alpha)+(y2-y1)*math.sin(Alpha))
67 | x1_n=x1_t
68 | x2_n=x1_n+int((x2-x1)*math.cos(Alpha)+(y2-y1)*math.sin(Alpha))
69 | y1_n=y1_t
70 | y2_n=y2_t
71 | # if(y2_n>w_n):
72 | # y2_n=w_n
73 | # if(x2_n>h_n):
74 | # x2_n=h_n
75 | bbox.find('xmin').text=str(x1_n)
76 | bbox.find('ymin').text=str(y1_n)
77 | bbox.find('xmax').text=str(x2_n)
78 | bbox.find('ymax').text=str(y2_n)
79 | tree.write(filename)
80 |
81 |
82 | file_image="new1.png"
83 | rotate_file_image(file_image,-3)
84 |
85 |
86 | #rotate_xml("test2.xml", 5)
--------------------------------------------------------------------------------
/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | icons/help.png
5 | icons/expert2.png
6 | icons/expert2.png
7 | icons/done.png
8 | icons/file.png
9 | icons/labels.png
10 | icons/objects.png
11 | icons/close.png
12 | icons/fit-width.png
13 | icons/fit-window.png
14 | icons/undo.png
15 | icons/eye.png
16 | icons/quit.png
17 | icons/copy.png
18 | icons/edit.png
19 | icons/open.png
20 | icons/save.png
21 | icons/save-as.png
22 | icons/color.png
23 | icons/color_line.png
24 | icons/zoom.png
25 | icons/zoom-in.png
26 | icons/zoom-out.png
27 | icons/cancel.png
28 | icons/next.png
29 | icons/prev.png
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/rotation_opencv_fasterRCNN.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as numpy
3 | import math
4 | img=cv2.imread("new2.png",1)
5 | x1,y1=412,254
6 | x2,y2=1000,310
7 | Alpha=math.radians(5)
8 | print (x2-x1)*math.cos(Alpha)
9 | rect=cv2.rectangle(img, (x1, y1), (x2, y2), (255,255,0), 2)
10 | cv2.imshow("rect", rect)
11 | rows,cols, tt=img.shape
12 | M=cv2.getRotationMatrix2D((0, 0),5,1)
13 | dst=cv2.warpAffine(img,M, (cols,rows))
14 | cv2.imwrite("result.png",dst)
15 | cv2.imshow("img", dst)
16 | # Transformation
17 | x1_n=x1+ int(y1*math.sin(Alpha))
18 | y1_n=y1-int(x1*math.sin(Alpha))
19 | x2_n=x2+int(y2*math.sin(Alpha))
20 | y2_n=y2-int(x2*math.sin(Alpha))
21 | print x1_n, y1_n, x2_n, y2_n, math.sin(Alpha), math.cos(Alpha)
22 | # Calcul bouding box
23 | x1_new=x1_n
24 | y1_new=y1_n-int((x2-x1)*math.sin(Alpha))
25 | x2_new=x1_n+int((x2-x1)*math.cos(Alpha)+(y2-y1)*math.sin(Alpha))
26 | y2_new=y1_n+int((y2-y1)*math.cos(Alpha))
27 |
28 | print x1_new,y1_new,x2_new, y2_new
29 | rect_new=cv2.rectangle(dst, (x1_new, y1_new), (x2_new,y2_new), (255,0,0), 2)
30 | cv2.imshow("rect_new", rect_new)
31 | cv2.waitKey(0)
--------------------------------------------------------------------------------
/rotation_opencv_fasterRCNN_correct_rotation.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as numpy
3 | import math
4 | import imutils
5 | img=cv2.imread("new2.png",1)
6 | x1,y1=100,200
7 | x2,y2=550,350
8 | Alpha=math.radians(5)
9 | t=math.atan(y1*1.0/x1)
10 | #t=math.degrees(t)
11 | #print "goc: ", t
12 |
13 | y1_t=int(y1*math.cos(Alpha) + x1*math.sin(Alpha))
14 | x1_t=int(x1*math.cos(Alpha) + y1*math.sin(Alpha))
15 |
16 | x2_t=int(x2*math.cos(Alpha) + y2*math.sin(Alpha))
17 | y2_t=int(y2*math.cos(Alpha) + x2*math.sin(Alpha))
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | print y1_t, x1_t, y2_t, x2_t
29 |
30 |
31 |
32 |
33 | print (x2-x1)*math.cos(Alpha)
34 | rect=cv2.rectangle(img, (x1, y1), (x2, y2), (255,255,0), 2)
35 | w,h,d=rect.shape
36 | cv2.imshow("rect", rect)
37 | rows,cols, tt=img.shape
38 | #M=cv2.getRotationMatrix2D((0, 0),10,1)
39 | #dst=cv2.warpAffine(img,M, (cols,rows))
40 | dst=imutils.rotate_bound(img,5)
41 |
42 | w_n,h_n,d_n=dst.shape
43 |
44 | print "size: ",w , w*math.sin(Alpha) + h*math.cos(Alpha), w_n, h_n
45 |
46 | x1_n=int((x1*math.cos(Alpha)-y1*math.sin(Alpha)) + (w_n-y2*(1-math.tan(Alpha)))*math.sin(Alpha))
47 | y1_n=int(x1*math.sin(Alpha)+y1*math.cos(Alpha))
48 |
49 | x2_n=x1_n+int((x2-x1)*math.cos(Alpha)+(y2-y1)*math.sin(Alpha))
50 | #x1_k=int(w_n*1.0/w)*x1_n
51 |
52 |
53 | rect1=cv2.rectangle(dst, (x1_n, y1_t), (x2_n, y2_t), (0,255,0), 2)
54 | cv2.imshow("rect1", rect1)
55 |
56 |
57 |
58 | # r=h_n*1.0/h
59 | # x1_t=x1_t-(w_n-w)
60 |
61 | # x2_t=x2_t- (w_n-w)
62 | # rect1=cv2.rectangle(dst, (x1_t, y1_t), (x2_t, y2_t), (0,255,0), 2)
63 | # print "r=", r*(x2-x1)
64 | # cv2.imshow("rect1", rect1)
65 |
66 | # cv2.imwrite("result.png",dst)
67 | # #cv2.imshow("img", dst)
68 |
69 | # # Transformation
70 | # x1_n=x1_t
71 | # y1_n=y1_t
72 | # x2_n=x2_t
73 | # y2_n=y2_t
74 | # print x1_n, y1_n, x2_n, y2_n, math.sin(Alpha), math.cos(Alpha)
75 | # # Calcul bouding box
76 | # x1_new=x1_n
77 | # y1_new=y1_n-int((x2-x1)*math.sin(Alpha))
78 | # x2_new=x1_n+int((x2-x1)*math.cos(Alpha)+(y2-y1)*math.sin(Alpha))
79 | # y2_new=y1_n+int((y2-y1)*math.cos(Alpha))
80 |
81 |
82 |
83 |
84 | # print x1_new,y1_new,x2_new, y2_new
85 | # rect_new=cv2.rectangle(dst, (x1_new, y1_new), (x2_new,y2_new), (255,0,0), 2)
86 | #cv2.imshow("rect_new", rect_new)
87 | cv2.waitKey(0)
--------------------------------------------------------------------------------
/tests/test.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/tests/test.bmp
--------------------------------------------------------------------------------
/tests/test.py:
--------------------------------------------------------------------------------
1 |
2 | from unittest import TestCase
3 |
4 | from labelImg import get_main_app
5 |
6 |
7 | class TestMainWindow(TestCase):
8 |
9 | app = None
10 | win = None
11 |
12 | def setUp(self):
13 | self.app, self.win = get_main_app()
14 |
15 | def tearDown(self):
16 | self.win.close()
17 | self.app.quit()
18 |
19 | def test_noop(self):
20 | pass
21 |
22 |
--------------------------------------------------------------------------------
/tests/臉書.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manhcuogntin4/Label-Annotation-VOC-Pascal/aef69b078443ad0baab4d189ddde3ab165ae9b23/tests/臉書.jpg
--------------------------------------------------------------------------------