diff --git a/labelImg/.github/no-response.yml b/labelImg/.github/no-response.yml new file mode 100644 index 00000000..702091a1 --- /dev/null +++ b/labelImg/.github/no-response.yml @@ -0,0 +1,8 @@ +daysUntilClose: 14 +responseRequiredLabel: more-information-needed +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action or reproduce the issue. Please reach out if you have or find + the answers we need so that we can investigate further. diff --git a/labelImg/.github/workflows/package.yml b/labelImg/.github/workflows/package.yml new file mode 100644 index 00000000..6391e651 --- /dev/null +++ b/labelImg/.github/workflows/package.yml @@ -0,0 +1,66 @@ +name: Package +on: [push, pull_request] + +jobs: + package-macos: + runs-on: macos-latest + env: + PIPENV_VENV_IN_PROJECT: 1 + PIPENV_IGNORE_VIRTUALENVS: 1 + steps: + - uses: actions/checkout@v3 + - name: Setup Python Environment + run: | + pip3 install pipenv + pipenv install pyqt5 lxml + pipenv run pip install pyqt5==5.15.6 lxml + - name: Build LabelImg + run: | + pipenv run make qt5py3 + rm -rf build dist + - name: Package LabelImg + run: | + pipenv run python setup.py py2app + open dist/labelImg.app + - name: Archive macOS app + run: | + cd dist/ + tar czf labelImg.tgz labelImg.app + - uses: actions/upload-artifact@v3 + with: + name: macOS artifact + path: dist/labelImg.tgz + package-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Python Environment + run: | + pip3 install pyinstaller pyqt5==5.15.6 lxml + - name: Build LabelImg + run: | + pyrcc5 -o libs/resources.py resources.qrc + - name: Package LabelImg + run: | + pyinstaller --hidden-import=pyqt5 --hidden-import=lxml -F -n "labelImg" -c labelImg.py -p ./libs -p ./ + - uses: actions/upload-artifact@v3 + with: + name: Windows artifact + path: dist/labelImg.exe + package-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Python Environment + run: | + pip3 install pyinstaller pyqt5==5.15.6 lxml + - name: Build LabelImg + run: | + pyrcc5 -o libs/resources.py resources.qrc + - name: Package LabelImg + run: | + pyinstaller --hidden-import=pyqt5 --hidden-import=lxml -F -n "labelImg" -c labelImg.py -p ./libs -p ./ + - uses: actions/upload-artifact@v3 + with: + name: Linux artifact + path: dist/labelImg diff --git a/labelImg/.gitignore b/labelImg/.gitignore new file mode 100644 index 00000000..63754ecf --- /dev/null +++ b/labelImg/.gitignore @@ -0,0 +1,43 @@ +resources/icons/.DS_Store +resources.py +labelImg.egg-info* + +*.pyc +.*.swp + +build/ +dist/ + +tags +cscope* +.ycm_extra_conf.py +.subvimrc +.vscode +*.pkl +Pipfile +*.xml + +# MacOS System-Generated +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# IDE +*.DS_Store +*.iml +.idea +.unison* +.attach* +tmp.* diff --git a/labelImg/CONTRIBUTING.rst b/labelImg/CONTRIBUTING.rst new file mode 100644 index 00000000..4e052471 --- /dev/null +++ b/labelImg/CONTRIBUTING.rst @@ -0,0 +1,3 @@ +TzuTa Lin +[LabelMe](http://labelme2.csail.mit.edu/Release3.0/index.php) +Ryan Flynn diff --git a/labelImg/HISTORY.rst b/labelImg/HISTORY.rst new file mode 100644 index 00000000..f4900d32 --- /dev/null +++ b/labelImg/HISTORY.rst @@ -0,0 +1,130 @@ +History +======= + +1.8.6 (2021-10-10) +------------------ + +* Display box width and height + + +1.8.5 (2021-04-11) +------------------ + +* Merged a couple of PRs +* Fixed issues +* Support CreateML format + + +1.8.4 (2020-11-04) +------------------ + +* Merged a couple of PRs +* Fixed issues + +1.8.2 (2018-12-02) +------------------ + +* Fix pip depolyment issue + + +1.8.1 (2018-12-02) +------------------ + +* Fix issues +* Support zh-Tw strings + + +1.8.0 (2018-10-21) +------------------ + +* Support drawing sqaure rect +* Add item single click slot +* Fix issues + +1.7.0 (2018-05-18) +------------------ + +* Support YOLO +* Fix minor issues + + +1.6.1 (2018-04-17) +------------------ + +* Fix issue + +1.6.0 (2018-01-29) +------------------ + +* Add more pre-defined labels +* Show cursor pose in status bar +* Fix minor issues + +1.5.2 (2017-10-24) +------------------ + +* Assign different colors to different lablels + +1.5.1 (2017-9-27) +------------------ + +* Show a autosaving dialog + +1.5.0 (2017-9-14) +------------------ + +* Fix the issues +* Add feature: Draw a box easier + + +1.4.3 (2017-08-09) +------------------ + +* Refactor setting +* Fix the issues + + +1.4.0 (2017-07-07) +------------------ + +* Add feature: auto saving +* Add feature: single class mode +* Fix the issues + +1.3.4 (2017-07-07) +------------------ + +* Fix issues and improve zoom-in + +1.3.3 (2017-05-31) +------------------ + +* Fix issues + +1.3.2 (2017-05-18) +------------------ + +* Fix issues + + +1.3.1 (2017-05-11) +------------------ + +* Fix issues + +1.3.0 (2017-04-22) +------------------ + +* Fix issues +* Add difficult tag +* Create new files for pypi + +1.2.3 (2017-04-22) +------------------ + +* Fix issues + +1.2.2 (2017-01-09) +------------------ + +* Fix issues diff --git a/labelImg/LICENSE b/labelImg/LICENSE new file mode 100644 index 00000000..0ffc8fe0 --- /dev/null +++ b/labelImg/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) <2015-Present> Tzutalin + +Copyright (C) 2013 MIT, Computer Science and Artificial Intelligence Laboratory. Bryan Russell, Antonio Torralba, William T. Freeman + +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: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. diff --git a/labelImg/MANIFEST.in b/labelImg/MANIFEST.in new file mode 100644 index 00000000..f47f84f6 --- /dev/null +++ b/labelImg/MANIFEST.in @@ -0,0 +1,15 @@ +include CONTRIBUTING.rst +include HISTORY.rst +include LICENSE +include README.rst + +include resources.qrc + +recursive-include data * +recursive-include icons * +recursive-include libs * + +recursive-exclude build-tools * +recursive-exclude tests * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/labelImg/Makefile b/labelImg/Makefile new file mode 100644 index 00000000..7d72a890 --- /dev/null +++ b/labelImg/Makefile @@ -0,0 +1,35 @@ +# ex: set ts=8 noet: + +all: qt5 test + +test: testpy3 + +testpy2: + python -m unittest discover tests + +testpy3: + python3 -m unittest discover tests + +qt4: qt4py2 + +qt5: qt5py3 + +qt4py2: + pyrcc4 -py2 -o libs/resources.py resources.qrc + +qt4py3: + pyrcc4 -py3 -o libs/resources.py resources.qrc + +qt5py3: + pyrcc5 -o libs/resources.py resources.qrc + +clean: + rm -rf ~/.labelImgSettings.pkl *.pyc dist labelImg.egg-info __pycache__ build + +pip_upload: + python3 setup.py upload + +long_description: + restview --long-description + +.PHONY: all diff --git a/labelImg/README.rst b/labelImg/README.rst new file mode 100644 index 00000000..ef061f46 --- /dev/null +++ b/labelImg/README.rst @@ -0,0 +1,317 @@ +.. image:: /readme/images/labelimg.png + :target: https://github.com/heartexlabs/label-studio + +Label Studio is a modern, multi-modal data annotation tool +======= + +LabelImg, the popular image annotation tool created by Tzutalin with the help of dozens contributors, is no longer actively being developed and has become part of the Label Studio community. Check out `Label Studio `__, the most flexible open source data labeling tool for images, text, hypertext, audio, video and time-series data. `Install `__ Label Studio and join the `slack community `__ to get started. + +.. image:: /readme/images/label-studio-1-6-player-screenshot.png + :target: https://github.com/heartexlabs/label-studio + +About LabelImg +======== + +.. image:: https://img.shields.io/pypi/v/labelimg.svg + :target: https://pypi.python.org/pypi/labelimg + +.. image:: https://img.shields.io/github/workflow/status/tzutalin/labelImg/Package?style=for-the-badge + :alt: GitHub Workflow Status + +.. image:: https://img.shields.io/badge/lang-en-blue.svg + :target: https://github.com/tzutalin/labelImg + +.. image:: https://img.shields.io/badge/lang-zh-green.svg + :target: https://github.com/tzutalin/labelImg/blob/master/readme/README.zh.rst + +.. image:: https://img.shields.io/badge/lang-jp-green.svg + :target: https://github.com/tzutalin/labelImg/blob/master/readme/README.jp.rst + +LabelImg is a graphical image annotation tool. + +It is written in Python and uses Qt for its graphical interface. + +Annotations are saved as XML files in PASCAL VOC format, the format used +by `ImageNet `__. Besides, it also supports YOLO and CreateML formats. + +.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo3.jpg + :alt: Demo Image + +.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo.jpg + :alt: Demo Image + +`Watch a demo video `__ + +Installation +------------------ + +Get from PyPI but only python3.0 or above +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is the simplest (one-command) install method on modern Linux distributions such as Ubuntu and Fedora. + +.. code:: shell + + pip3 install labelImg + labelImg + labelImg [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + + +Build from source +~~~~~~~~~~~~~~~~~ + +Linux/Ubuntu/Mac requires at least `Python +2.6 `__ and has been tested with `PyQt +4.8 `__. However, `Python +3 or above `__ and `PyQt5 `__ are strongly recommended. + + +Ubuntu Linux +^^^^^^^^^^^^ + +Python 3 + Qt5 + +.. code:: shell + + sudo apt-get install pyqt5-dev-tools + sudo pip3 install -r requirements/requirements-linux-python3.txt + make qt5py3 + python3 labelImg.py + python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +macOS +^^^^^ + +Python 3 + Qt5 + +.. code:: shell + + brew install qt # Install qt-5.x.x by Homebrew + brew install libxml2 + + or using pip + + pip3 install pyqt5 lxml # Install qt and lxml by pip + + make qt5py3 + python3 labelImg.py + python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + + +Python 3 Virtualenv (Recommended) + +Virtualenv can avoid a lot of the QT / Python version issues + +.. code:: shell + + brew install python3 + pip3 install pipenv + pipenv run pip install pyqt5==5.15.2 lxml + pipenv run make qt5py3 + pipenv run python3 labelImg.py + [Optional] rm -rf build dist; pipenv run python setup.py py2app -A;mv "dist/labelImg.app" /Applications + +Note: The Last command gives you a nice .app file with a new SVG Icon in your /Applications folder. You can consider using the script: build-tools/build-for-macos.sh + + +Windows +^^^^^^^ + +Install `Python `__, +`PyQt5 `__ +and `install lxml `__. + +Open cmd and go to the `labelImg <#labelimg>`__ directory + +.. code:: shell + + pyrcc4 -o libs/resources.py resources.qrc + For pyqt5, pyrcc5 -o libs/resources.py resources.qrc + + python labelImg.py + python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +If you want to package it into a separate EXE file + +.. code:: shell + + Install pyinstaller and execute: + + pip install pyinstaller + pyinstaller --hidden-import=pyqt5 --hidden-import=lxml -F -n "labelImg" -c labelImg.py -p ./libs -p ./ + +Windows + Anaconda +^^^^^^^^^^^^^^^^^^ + +Download and install `Anaconda `__ (Python 3+) + +Open the Anaconda Prompt and go to the `labelImg <#labelimg>`__ directory + +.. code:: shell + + conda install pyqt=5 + conda install -c anaconda lxml + pyrcc5 -o libs/resources.py resources.qrc + python labelImg.py + python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +Use Docker +~~~~~~~~~~~~~~~~~ +.. code:: shell + + docker run -it \ + --user $(id -u) \ + -e DISPLAY=unix$DISPLAY \ + --workdir=$(pwd) \ + --volume="/home/$USER:/home/$USER" \ + --volume="/etc/group:/etc/group:ro" \ + --volume="/etc/passwd:/etc/passwd:ro" \ + --volume="/etc/shadow:/etc/shadow:ro" \ + --volume="/etc/sudoers.d:/etc/sudoers.d:ro" \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + tzutalin/py2qt4 + + make qt4py2;./labelImg.py + +You can pull the image which has all of the installed and required dependencies. `Watch a demo video `__ + + +Usage +----- + +Steps (PascalVOC) +~~~~~~~~~~~~~~~~~ + +1. Build and launch using the instructions above. +2. Click 'Change default saved annotation folder' in Menu/File +3. Click 'Open Dir' +4. Click 'Create RectBox' +5. Click and release left mouse to select a region to annotate the rect + box +6. You can use right mouse to drag the rect box to copy or move it + +The annotation will be saved to the folder you specify. + +You can refer to the below hotkeys to speed up your workflow. + +Steps (YOLO) +~~~~~~~~~~~~ + +1. In ``data/predefined_classes.txt`` define the list of classes that will be used for your training. + +2. Build and launch using the instructions above. + +3. Right below "Save" button in the toolbar, click "PascalVOC" button to switch to YOLO format. + +4. You may use Open/OpenDIR to process single or multiple images. When finished with a single image, click save. + +A txt file of YOLO format will be saved in the same folder as your image with same name. A file named "classes.txt" is saved to that folder too. "classes.txt" defines the list of class names that your YOLO label refers to. + +Note: + +- Your label list shall not change in the middle of processing a list of images. When you save an image, classes.txt will also get updated, while previous annotations will not be updated. + +- You shouldn't use "default class" function when saving to YOLO format, it will not be referred. + +- When saving as YOLO format, "difficult" flag is discarded. + +Create pre-defined classes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can edit the +`data/predefined\_classes.txt `__ +to load pre-defined classes + +Annotation visualization +~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Copy the existing lables file to same folder with the images. The labels file name must be same with image file name. + +2. Click File and choose 'Open Dir' then Open the image folder. + +3. Select image in File List, it will appear the bounding box and label for all objects in that image. + +(Choose Display Labels mode in View to show/hide lablels) + + +Hotkeys +~~~~~~~ + ++--------------------+--------------------------------------------+ +| Ctrl + u | Load all of the images from a directory | ++--------------------+--------------------------------------------+ +| Ctrl + r | Change the default annotation target dir | ++--------------------+--------------------------------------------+ +| Ctrl + s | Save | ++--------------------+--------------------------------------------+ +| Ctrl + d | Copy the current label and rect box | ++--------------------+--------------------------------------------+ +| Ctrl + Shift + d | Delete the current image | ++--------------------+--------------------------------------------+ +| Space | Flag the current image as verified | ++--------------------+--------------------------------------------+ +| w | Create a rect box | ++--------------------+--------------------------------------------+ +| d | Next image | ++--------------------+--------------------------------------------+ +| a | Previous image | ++--------------------+--------------------------------------------+ +| del | Delete the selected rect box | ++--------------------+--------------------------------------------+ +| Ctrl++ | Zoom in | ++--------------------+--------------------------------------------+ +| Ctrl-- | Zoom out | ++--------------------+--------------------------------------------+ +| ↑→↓← | Keyboard arrows to move selected rect box | ++--------------------+--------------------------------------------+ + +**Verify Image:** + +When pressing space, the user can flag the image as verified, a green background will appear. +This is used when creating a dataset automatically, the user can then through all the pictures and flag them instead of annotate them. + +**Difficult:** + +The difficult field is set to 1 indicates that the object has been annotated as "difficult", for example, an object which is clearly visible but difficult to recognize without substantial use of context. +According to your deep neural network implementation, you can include or exclude difficult objects during training. + +How to reset the settings +~~~~~~~~~~~~~~~~~~~~~~~~~ + +In case there are issues with loading the classes, you can either: + +1. From the top menu of the labelimg click on Menu/File/Reset All +2. Remove the `.labelImgSettings.pkl` from your home directory. In Linux and Mac you can do: + `rm ~/.labelImgSettings.pkl` + + +How to contribute +~~~~~~~~~~~~~~~~~ + +Send a pull request + +License +~~~~~~~ +`Free software: MIT license `_ + +Citation: Tzutalin. LabelImg. Git code (2015). https://github.com/tzutalin/labelImg + +Related and additional tools +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. `Label Studio `__ to label images, text, audio, video and time-series data for machine learning and AI +2. `ImageNet Utils `__ to + download image, create a label text for machine learning, etc +3. `Use Docker to run labelImg `__ +4. `Generating the PASCAL VOC TFRecord files `__ +5. `App Icon based on Icon by Nick Roach (GPL) `__ +6. `Setup python development in vscode `__ +7. `The link of this project on iHub platform `__ +8. `Convert annotation files to CSV format or format for Google Cloud AutoML `__ + + + +Stargazers over time +~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://starchart.cc/tzutalin/labelImg.svg + diff --git a/labelImg/__init__.py b/labelImg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/labelImg/build-tools/.gitignore b/labelImg/build-tools/.gitignore new file mode 100644 index 00000000..e05bf6bf --- /dev/null +++ b/labelImg/build-tools/.gitignore @@ -0,0 +1,12 @@ +*.spec +build +dist +pyinstaller +python-2.* +pywin32* +virtual-wine +venv_wine +PyQt4-* +lxml-* +windows_v* +linux_v* diff --git a/labelImg/build-tools/README.md b/labelImg/build-tools/README.md new file mode 100644 index 00000000..8e4cc5da --- /dev/null +++ b/labelImg/build-tools/README.md @@ -0,0 +1,35 @@ +### Deploy to PyPI + +``` +cd [ROOT] +sh build-tools/build-for-pypi.sh +``` + +### Build for Ubuntu + +``` +cd build-tools +sh run-in-container.sh +sh envsetup.sh +sh build-ubuntu-binary.sh +``` + +### Build for Windows + +``` +cd build-tools +sh run-in-container.sh +sh envsetup.sh +sh build-windows-binary.sh +``` + +### Build for macOS High Sierra +``` +cd build-tools +./build-for-macos.sh +``` + +Note: If there are some problems, try to +``` +sudo rm -rf virtual-wne venv_wine +``` diff --git a/labelImg/build-tools/build-for-macos.sh b/labelImg/build-tools/build-for-macos.sh new file mode 100755 index 00000000..dfe926b7 --- /dev/null +++ b/labelImg/build-tools/build-for-macos.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +brew install python@2 +pip install --upgrade virtualenv + +# clone labelimg source +rm -rf /tmp/labelImgSetup +mkdir /tmp/labelImgSetup +cd /tmp/labelImgSetup +curl https://codeload.github.com/tzutalin/labelImg/zip/master --output labelImg.zip +unzip labelImg.zip +rm labelImg.zip + +# setup python3 space +virtualenv --system-site-packages -p python3 /tmp/labelImgSetup/labelImg-py3 +source /tmp/labelImgSetup/labelImg-py3/bin/activate +cd labelImg-master + +# build labelImg app +pip install py2app +pip install PyQt5 lxml +make qt5py3 +rm -rf build dist +python setup.py py2app -A +mv "/tmp/labelImgSetup/labelImg-master/dist/labelImg.app" /Applications +# deactivate python3 +deactivate +cd ../ +rm -rf /tmp/labelImgSetup +echo 'DONE' diff --git a/labelImg/build-tools/build-for-pypi.sh b/labelImg/build-tools/build-for-pypi.sh new file mode 100755 index 00000000..d5ad5c7c --- /dev/null +++ b/labelImg/build-tools/build-for-pypi.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Packaging and Release +docker run --workdir=$(pwd)/ --volume="/home/$USER:/home/$USER" tzutalin/py2qt4 /bin/sh -c 'make qt4py2; make test;sudo python setup.py sdist;sudo python setup.py install' + +while true; do + read -p "Do you wish to deploy this to PyPI(twine upload dist/* or pip install dist/*)?" yn + case $yn in + [Yy]* ) docker run -it --rm --workdir=$(pwd)/ --volume="/home/$USER:/home/$USER" tzutalin/py2qt4; break;; + [Nn]* ) exit;; + * ) echo "Please answer yes or no.";; + esac +done +# python setup.py register +# python setup.py sdist upload +# Net pypi: twine upload dist/* + +# Test before upladoing: pip install dist/labelImg.tar.gz diff --git a/labelImg/build-tools/build-ubuntu-binary.sh b/labelImg/build-tools/build-ubuntu-binary.sh new file mode 100755 index 00000000..dbe53028 --- /dev/null +++ b/labelImg/build-tools/build-ubuntu-binary.sh @@ -0,0 +1,24 @@ +#!/bin/bash +### Ubuntu use pyinstall v3.0 +THIS_SCRIPT_PATH=`readlink -f $0` +THIS_SCRIPT_DIR=`dirname ${THIS_SCRIPT_PATH}` +cd pyinstaller +git checkout v3.2 +cd ${THIS_SCRIPT_DIR} + +rm -r build +rm -r dist +rm labelImg.spec +python pyinstaller/pyinstaller.py --hidden-import=xml \ + --hidden-import=xml.etree \ + --hidden-import=xml.etree.ElementTree \ + --hidden-import=lxml.etree \ + -D -F -n labelImg -c "../labelImg.py" -p ../libs -p ../ + +FOLDER=$(git describe --abbrev=0 --tags) +FOLDER="linux_"$FOLDER +rm -rf "$FOLDER" +mkdir "$FOLDER" +cp dist/labelImg $FOLDER +cp -rf ../data $FOLDER/data +zip "$FOLDER.zip" -r $FOLDER diff --git a/labelImg/build-tools/build-windows-binary.sh b/labelImg/build-tools/build-windows-binary.sh new file mode 100755 index 00000000..566e88f9 --- /dev/null +++ b/labelImg/build-tools/build-windows-binary.sh @@ -0,0 +1,32 @@ +#!/bin/bash +### Window requires pyinstall v2.1 +wine msiexec -i python-2.7.8.msi +wine pywin32-218.win32-py2.7.exe +wine PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe +wine lxml-3.7.3.win32-py2.7.exe + +THIS_SCRIPT_PATH=`readlink -f $0` +THIS_SCRIPT_DIR=`dirname ${THIS_SCRIPT_PATH}` +cd pyinstaller +git checkout v2.1 +cd ${THIS_SCRIPT_DIR} +echo ${THIS_SCRIPT_DIR} + +#. venv_wine/bin/activate +rm -r build +rm -r dist +rm labelImg.spec + +wine c:/Python27/python.exe pyinstaller/pyinstaller.py --hidden-import=xml \ + --hidden-import=xml.etree \ + --hidden-import=xml.etree.ElementTree \ + --hidden-import=lxml.etree \ + -D -F -n labelImg -c "../labelImg.py" -p ../libs -p ../ + +FOLDER=$(git describe --abbrev=0 --tags) +FOLDER="windows_"$FOLDER +rm -rf "$FOLDER" +mkdir "$FOLDER" +cp dist/labelImg.exe $FOLDER +cp -rf ../data $FOLDER/data +zip "$FOLDER.zip" -r $FOLDER diff --git a/labelImg/build-tools/envsetup.sh b/labelImg/build-tools/envsetup.sh new file mode 100755 index 00000000..242b0c56 --- /dev/null +++ b/labelImg/build-tools/envsetup.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +THIS_SCRIPT_PATH=`readlink -f $0` +THIS_SCRIPT_DIR=`dirname ${THIS_SCRIPT_PATH}` +#OS Ubuntu 14.04 +### Common packages for linux/windows +if [ ! -e "pyinstaller" ]; then + git clone https://github.com/pyinstaller/pyinstaller + cd pyinstaller + git checkout v2.1 -b v2.1 + cd ${THIS_SCRIPT_DIR} +fi + +echo "Going to clone and download packages for building windows" +#Pacakges +#> pyinstaller (2.1) +#> wine (1.6.2) +#> virtual-wine (0.1) +#> python-2.7.8.msi +#> pywin32-218.win32-py2.7.exe + +## tool to install on Ubuntu +#$ sudo apt-get install wine + +### Clone a repo to create virtual wine env +if [ ! -e "virtual-wine" ]; then + git clone https://github.com/htgoebel/virtual-wine.git +fi + +apt-get install scons +### Create virtual env +rm -rf venv_wine +./virtual-wine/vwine-setup venv_wine +#### Active virutal env +. venv_wine/bin/activate + +### Use wine to install packages to virtual env +if [ ! -e "python-2.7.8.msi" ]; then + wget "https://www.python.org/ftp/python/2.7.8/python-2.7.8.msi" +fi + +if [ ! -e "pywin32-218.win32-py2.7.exe" ]; then + wget "http://nchc.dl.sourceforge.net/project/pywin32/pywin32/Build%20218/pywin32-218.win32-py2.7.exe" +fi + +if [ ! -e "PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe" ]; then + 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" +fi + +if [ ! -e "lxml-3.7.3.win32-py2.7.exe" ]; then + wget "https://pypi.python.org/packages/a3/f6/a28c5cf63873f6c55a3eb7857b736379229b85ba918261d2e88cf886905e/lxml-3.7.3.win32-py2.7.exe#md5=a0f746355876aca4ca5371cb0f1d13ce" +fi + diff --git a/labelImg/build-tools/run-in-container.sh b/labelImg/build-tools/run-in-container.sh new file mode 100755 index 00000000..980699d6 --- /dev/null +++ b/labelImg/build-tools/run-in-container.sh @@ -0,0 +1,13 @@ +#!/bin/sh +docker run -it \ + --user $(id -u) \ + -e DISPLAY=unix$DISPLAY \ + --workdir=$(pwd) \ + --volume="/home/$USER:/home/$USER" \ + --volume="/etc/group:/etc/group:ro" \ + --volume="/etc/passwd:/etc/passwd:ro" \ + --volume="/etc/shadow:/etc/shadow:ro" \ + --volume="/etc/sudoers.d:/etc/sudoers.d:ro" \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + tzutalin/py2qt4 + diff --git a/labelImg/data/predefined_classes.txt b/labelImg/data/predefined_classes.txt new file mode 100755 index 00000000..bc2eef13 --- /dev/null +++ b/labelImg/data/predefined_classes.txt @@ -0,0 +1,15 @@ +dog +person +cat +tv +car +meatballs +marinara sauce +tomato soup +chicken noodle soup +french onion soup +chicken breast +ribs +pulled pork +hamburger +cavity \ No newline at end of file diff --git a/labelImg/demo/demo.jpg b/labelImg/demo/demo.jpg new file mode 100644 index 00000000..c339af42 Binary files /dev/null and b/labelImg/demo/demo.jpg differ diff --git a/labelImg/demo/demo3.jpg b/labelImg/demo/demo3.jpg new file mode 100644 index 00000000..8d58a445 Binary files /dev/null and b/labelImg/demo/demo3.jpg differ diff --git a/labelImg/demo/demo4.png b/labelImg/demo/demo4.png new file mode 100644 index 00000000..dc219984 Binary files /dev/null and b/labelImg/demo/demo4.png differ diff --git a/labelImg/demo/demo5.png b/labelImg/demo/demo5.png new file mode 100644 index 00000000..0a857a0a Binary files /dev/null and b/labelImg/demo/demo5.png differ diff --git a/labelImg/issue_template.md b/labelImg/issue_template.md new file mode 100644 index 00000000..5222ada0 --- /dev/null +++ b/labelImg/issue_template.md @@ -0,0 +1,7 @@ + + +- **OS:** +- **PyQt version:** diff --git a/labelImg/labelImg.py b/labelImg/labelImg.py new file mode 100755 index 00000000..efd8a297 --- /dev/null +++ b/labelImg/labelImg.py @@ -0,0 +1,1722 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import argparse +import codecs +import os.path +import platform +import shutil +import sys +import webbrowser as wb +from functools import partial + +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + # needed for py3+qt4 + # Ref: + # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html + # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string + if sys.version_info.major >= 3: + import sip + sip.setapi('QVariant', 2) + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +from libs.combobox import ComboBox +from libs.default_label_combobox import DefaultLabelComboBox +from libs.resources import * +from libs.constants import * +from libs.utils import * +from libs.settings import Settings +from libs.shape import Shape, DEFAULT_LINE_COLOR, DEFAULT_FILL_COLOR +from libs.stringBundle import StringBundle +from libs.canvas import Canvas +from libs.zoomWidget import ZoomWidget +from libs.lightWidget import LightWidget +from libs.labelDialog import LabelDialog +from libs.colorDialog import ColorDialog +from libs.labelFile import LabelFile, LabelFileError, LabelFileFormat +from libs.toolBar import ToolBar +from libs.pascal_voc_io import PascalVocReader +from libs.pascal_voc_io import XML_EXT +from libs.yolo_io import YoloReader +from libs.yolo_io import TXT_EXT +from libs.create_ml_io import CreateMLReader +from libs.create_ml_io import JSON_EXT +from libs.ustr import ustr +from libs.hashableQListWidgetItem import HashableQListWidgetItem + +__appname__ = 'labelImg' + + +class WindowMixin(object): + + def menu(self, title, actions=None): + menu = self.menuBar().addMenu(title) + if actions: + add_actions(menu, actions) + return menu + + def toolbar(self, title, actions=None): + toolbar = ToolBar(title) + toolbar.setObjectName(u'%sToolBar' % title) + # toolbar.setOrientation(Qt.Vertical) + toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + if actions: + add_actions(toolbar, actions) + self.addToolBar(Qt.LeftToolBarArea, toolbar) + return toolbar + + +class MainWindow(QMainWindow, WindowMixin): + FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = list(range(3)) + + def __init__(self, default_filename=None, default_prefdef_class_file=None, default_save_dir=None): + super(MainWindow, self).__init__() + self.setWindowTitle(__appname__) + + # Load setting in the main thread + self.settings = Settings() + self.settings.load() + settings = self.settings + + self.os_name = platform.system() + + # Load string bundle for i18n + self.string_bundle = StringBundle.get_bundle() + get_str = lambda str_id: self.string_bundle.get_string(str_id) + + # Save as Pascal voc xml + self.default_save_dir = default_save_dir + self.label_file_format = settings.get(SETTING_LABEL_FILE_FORMAT, LabelFileFormat.PASCAL_VOC) + + # For loading all image under a directory + self.m_img_list = [] + self.dir_name = None + self.label_hist = [] + self.last_open_dir = None + self.cur_img_idx = 0 + self.img_count = len(self.m_img_list) + + # Whether we need to save or not. + self.dirty = False + + self._no_selection_slot = False + self._beginner = True + self.screencast = "https://youtu.be/p0nR2YsCY_U" + + # Load predefined classes to the list + self.load_predefined_classes(default_prefdef_class_file) + + if self.label_hist: + self.default_label = self.label_hist[0] + else: + print("Not find:/data/predefined_classes.txt (optional)") + + # Main widgets and related state. + self.label_dialog = LabelDialog(parent=self, list_item=self.label_hist) + + self.items_to_shapes = {} + self.shapes_to_items = {} + self.prev_label_text = '' + + list_layout = QVBoxLayout() + list_layout.setContentsMargins(0, 0, 0, 0) + + # Create a widget for using default label + self.use_default_label_checkbox = QCheckBox(get_str('useDefaultLabel')) + self.use_default_label_checkbox.setChecked(False) + self.default_label_combo_box = DefaultLabelComboBox(self,items=self.label_hist) + + use_default_label_qhbox_layout = QHBoxLayout() + use_default_label_qhbox_layout.addWidget(self.use_default_label_checkbox) + use_default_label_qhbox_layout.addWidget(self.default_label_combo_box) + use_default_label_container = QWidget() + use_default_label_container.setLayout(use_default_label_qhbox_layout) + + # Create a widget for edit and diffc button + self.diffc_button = QCheckBox(get_str('useDifficult')) + self.diffc_button.setChecked(False) + self.diffc_button.stateChanged.connect(self.button_state) + self.edit_button = QToolButton() + self.edit_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + + # Add some of widgets to list_layout + list_layout.addWidget(self.edit_button) + list_layout.addWidget(self.diffc_button) + list_layout.addWidget(use_default_label_container) + + # Create and add combobox for showing unique labels in group + self.combo_box = ComboBox(self) + list_layout.addWidget(self.combo_box) + + # Create and add a widget for showing current label items + self.label_list = QListWidget() + label_list_container = QWidget() + label_list_container.setLayout(list_layout) + self.label_list.itemActivated.connect(self.label_selection_changed) + self.label_list.itemSelectionChanged.connect(self.label_selection_changed) + self.label_list.itemDoubleClicked.connect(self.edit_label) + # Connect to itemChanged to detect checkbox changes. + self.label_list.itemChanged.connect(self.label_item_changed) + list_layout.addWidget(self.label_list) + + + + self.dock = QDockWidget(get_str('boxLabelText'), self) + self.dock.setObjectName(get_str('labels')) + self.dock.setWidget(label_list_container) + + self.file_list_widget = QListWidget() + self.file_list_widget.itemDoubleClicked.connect(self.file_item_double_clicked) + file_list_layout = QVBoxLayout() + file_list_layout.setContentsMargins(0, 0, 0, 0) + file_list_layout.addWidget(self.file_list_widget) + file_list_container = QWidget() + file_list_container.setLayout(file_list_layout) + self.file_dock = QDockWidget(get_str('fileList'), self) + self.file_dock.setObjectName(get_str('files')) + self.file_dock.setWidget(file_list_container) + + self.zoom_widget = ZoomWidget() + self.light_widget = LightWidget(get_str('lightWidgetTitle')) + self.color_dialog = ColorDialog(parent=self) + + self.canvas = Canvas(parent=self) + self.canvas.zoomRequest.connect(self.zoom_request) + self.canvas.lightRequest.connect(self.light_request) + self.canvas.set_drawing_shape_to_square(settings.get(SETTING_DRAW_SQUARE, False)) + + scroll = QScrollArea() + scroll.setWidget(self.canvas) + scroll.setWidgetResizable(True) + self.scroll_bars = { + Qt.Vertical: scroll.verticalScrollBar(), + Qt.Horizontal: scroll.horizontalScrollBar() + } + self.scroll_area = scroll + self.canvas.scrollRequest.connect(self.scroll_request) + + self.canvas.newShape.connect(self.new_shape) + self.canvas.shapeMoved.connect(self.set_dirty) + self.canvas.selectionChanged.connect(self.shape_selection_changed) + self.canvas.drawingPolygon.connect(self.toggle_drawing_sensitive) + + self.setCentralWidget(scroll) + self.addDockWidget(Qt.RightDockWidgetArea, self.dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.file_dock) + self.file_dock.setFeatures(QDockWidget.DockWidgetFloatable) + + self.dock_features = QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetFloatable + self.dock.setFeatures(self.dock.features() ^ self.dock_features) + + # Actions + action = partial(new_action, self) + quit = action(get_str('quit'), self.close, + 'Ctrl+Q', 'quit', get_str('quitApp')) + + open = action(get_str('openFile'), self.open_file, + 'Ctrl+O', 'open', get_str('openFileDetail')) + + open_dir = action(get_str('openDir'), self.open_dir_dialog, + 'Ctrl+u', 'open', get_str('openDir')) + + change_save_dir = action(get_str('changeSaveDir'), self.change_save_dir_dialog, + 'Ctrl+r', 'open', get_str('changeSavedAnnotationDir')) + + open_annotation = action(get_str('openAnnotation'), self.open_annotation_dialog, + 'Ctrl+Shift+O', 'open', get_str('openAnnotationDetail')) + copy_prev_bounding = action(get_str('copyPrevBounding'), self.copy_previous_bounding_boxes, 'Ctrl+v', 'copy', get_str('copyPrevBounding')) + + open_next_image = action(get_str('nextImg'), self.open_next_image, + 'd', 'next', get_str('nextImgDetail')) + + open_prev_image = action(get_str('prevImg'), self.open_prev_image, + 'a', 'prev', get_str('prevImgDetail')) + + verify = action(get_str('verifyImg'), self.verify_image, + 'space', 'verify', get_str('verifyImgDetail')) + + save = action(get_str('save'), self.save_file, + 'Ctrl+S', 'save', get_str('saveDetail'), enabled=False) + + def get_format_meta(format): + """ + returns a tuple containing (title, icon_name) of the selected format + """ + if format == LabelFileFormat.PASCAL_VOC: + return '&PascalVOC', 'format_voc' + elif format == LabelFileFormat.YOLO: + return '&YOLO', 'format_yolo' + elif format == LabelFileFormat.CREATE_ML: + return '&CreateML', 'format_createml' + + save_format = action(get_format_meta(self.label_file_format)[0], + self.change_format, 'Ctrl+Y', + get_format_meta(self.label_file_format)[1], + get_str('changeSaveFormat'), enabled=True) + + save_as = action(get_str('saveAs'), self.save_file_as, + 'Ctrl+Shift+S', 'save-as', get_str('saveAsDetail'), enabled=False) + + close = action(get_str('closeCur'), self.close_file, 'Ctrl+W', 'close', get_str('closeCurDetail')) + + delete_image = action(get_str('deleteImg'), self.delete_image, 'Ctrl+Shift+D', 'close', get_str('deleteImgDetail')) + + reset_all = action(get_str('resetAll'), self.reset_all, None, 'resetall', get_str('resetAllDetail')) + + color1 = action(get_str('boxLineColor'), self.choose_color1, + 'Ctrl+L', 'color_line', get_str('boxLineColorDetail')) + + create_mode = action(get_str('crtBox'), self.set_create_mode, + 'w', 'new', get_str('crtBoxDetail'), enabled=False) + edit_mode = action(get_str('editBox'), self.set_edit_mode, + 'Ctrl+J', 'edit', get_str('editBoxDetail'), enabled=False) + + create = action(get_str('crtBox'), self.create_shape, + 'w', 'new', get_str('crtBoxDetail'), enabled=False) + delete = action(get_str('delBox'), self.delete_selected_shape, + 'Delete', 'delete', get_str('delBoxDetail'), enabled=False) + copy = action(get_str('dupBox'), self.copy_selected_shape, + 'Ctrl+D', 'copy', get_str('dupBoxDetail'), + enabled=False) + + advanced_mode = action(get_str('advancedMode'), self.toggle_advanced_mode, + 'Ctrl+Shift+A', 'expert', get_str('advancedModeDetail'), + checkable=True) + + hide_all = action(get_str('hideAllBox'), partial(self.toggle_polygons, False), + 'Ctrl+H', 'hide', get_str('hideAllBoxDetail'), + enabled=False) + show_all = action(get_str('showAllBox'), partial(self.toggle_polygons, True), + 'Ctrl+A', 'hide', get_str('showAllBoxDetail'), + enabled=False) + + help_default = action(get_str('tutorialDefault'), self.show_default_tutorial_dialog, None, 'help', get_str('tutorialDetail')) + show_info = action(get_str('info'), self.show_info_dialog, None, 'help', get_str('info')) + show_shortcut = action(get_str('shortcut'), self.show_shortcuts_dialog, None, 'help', get_str('shortcut')) + + zoom = QWidgetAction(self) + zoom.setDefaultWidget(self.zoom_widget) + self.zoom_widget.setWhatsThis( + u"Zoom in or out of the image. Also accessible with" + " %s and %s from the canvas." % (format_shortcut("Ctrl+[-+]"), + format_shortcut("Ctrl+Wheel"))) + self.zoom_widget.setEnabled(False) + + zoom_in = action(get_str('zoomin'), partial(self.add_zoom, 10), + 'Ctrl++', 'zoom-in', get_str('zoominDetail'), enabled=False) + zoom_out = action(get_str('zoomout'), partial(self.add_zoom, -10), + 'Ctrl+-', 'zoom-out', get_str('zoomoutDetail'), enabled=False) + zoom_org = action(get_str('originalsize'), partial(self.set_zoom, 100), + 'Ctrl+=', 'zoom', get_str('originalsizeDetail'), enabled=False) + fit_window = action(get_str('fitWin'), self.set_fit_window, + 'Ctrl+F', 'fit-window', get_str('fitWinDetail'), + checkable=True, enabled=False) + fit_width = action(get_str('fitWidth'), self.set_fit_width, + 'Ctrl+Shift+F', 'fit-width', get_str('fitWidthDetail'), + checkable=True, enabled=False) + # Group zoom controls into a list for easier toggling. + zoom_actions = (self.zoom_widget, zoom_in, zoom_out, + zoom_org, fit_window, fit_width) + self.zoom_mode = self.MANUAL_ZOOM + self.scalers = { + self.FIT_WINDOW: self.scale_fit_window, + self.FIT_WIDTH: self.scale_fit_width, + # Set to one to scale to 100% when loading files. + self.MANUAL_ZOOM: lambda: 1, + } + + light = QWidgetAction(self) + light.setDefaultWidget(self.light_widget) + self.light_widget.setWhatsThis( + u"Brighten or darken current image. Also accessible with" + " %s and %s from the canvas." % (format_shortcut("Ctrl+Shift+[-+]"), + format_shortcut("Ctrl+Shift+Wheel"))) + self.light_widget.setEnabled(False) + + light_brighten = action(get_str('lightbrighten'), partial(self.add_light, 10), + 'Ctrl+Shift++', 'light_lighten', get_str('lightbrightenDetail'), enabled=False) + light_darken = action(get_str('lightdarken'), partial(self.add_light, -10), + 'Ctrl+Shift+-', 'light_darken', get_str('lightdarkenDetail'), enabled=False) + light_org = action(get_str('lightreset'), partial(self.set_light, 50), + 'Ctrl+Shift+=', 'light_reset', get_str('lightresetDetail'), checkable=True, enabled=False) + light_org.setChecked(True) + + # Group light controls into a list for easier toggling. + light_actions = (self.light_widget, light_brighten, + light_darken, light_org) + + edit = action(get_str('editLabel'), self.edit_label, + 'Ctrl+E', 'edit', get_str('editLabelDetail'), + enabled=False) + self.edit_button.setDefaultAction(edit) + + shape_line_color = action(get_str('shapeLineColor'), self.choose_shape_line_color, + icon='color_line', tip=get_str('shapeLineColorDetail'), + enabled=False) + shape_fill_color = action(get_str('shapeFillColor'), self.choose_shape_fill_color, + icon='color', tip=get_str('shapeFillColorDetail'), + enabled=False) + + labels = self.dock.toggleViewAction() + labels.setText(get_str('showHide')) + labels.setShortcut('Ctrl+Shift+L') + + # Label list context menu. + label_menu = QMenu() + add_actions(label_menu, (edit, delete)) + self.label_list.setContextMenuPolicy(Qt.CustomContextMenu) + self.label_list.customContextMenuRequested.connect( + self.pop_label_list_menu) + + # Draw squares/rectangles + self.draw_squares_option = QAction(get_str('drawSquares'), self) + self.draw_squares_option.setShortcut('Ctrl+Shift+R') + self.draw_squares_option.setCheckable(True) + self.draw_squares_option.setChecked(settings.get(SETTING_DRAW_SQUARE, False)) + self.draw_squares_option.triggered.connect(self.toggle_draw_square) + + # Store actions for further handling. + self.actions = Struct(save=save, save_format=save_format, saveAs=save_as, open=open, close=close, resetAll=reset_all, deleteImg=delete_image, + lineColor=color1, create=create, delete=delete, edit=edit, copy=copy, + createMode=create_mode, editMode=edit_mode, advancedMode=advanced_mode, + shapeLineColor=shape_line_color, shapeFillColor=shape_fill_color, + zoom=zoom, zoomIn=zoom_in, zoomOut=zoom_out, zoomOrg=zoom_org, + fitWindow=fit_window, fitWidth=fit_width, + zoomActions=zoom_actions, + lightBrighten=light_brighten, lightDarken=light_darken, lightOrg=light_org, + lightActions=light_actions, + fileMenuActions=( + open, open_dir, save, save_as, close, reset_all, quit), + beginner=(), advanced=(), + editMenu=(edit, copy, delete, + None, color1, self.draw_squares_option), + beginnerContext=(create, edit, copy, delete), + advancedContext=(create_mode, edit_mode, edit, copy, + delete, shape_line_color, shape_fill_color), + onLoadActive=( + close, create, create_mode, edit_mode), + onShapesPresent=(save_as, hide_all, show_all)) + + self.menus = Struct( + file=self.menu(get_str('menu_file')), + edit=self.menu(get_str('menu_edit')), + view=self.menu(get_str('menu_view')), + help=self.menu(get_str('menu_help')), + recentFiles=QMenu(get_str('menu_openRecent')), + labelList=label_menu) + + # Auto saving : Enable auto saving if pressing next + self.auto_saving = QAction(get_str('autoSaveMode'), self) + self.auto_saving.setCheckable(True) + self.auto_saving.setChecked(settings.get(SETTING_AUTO_SAVE, False)) + # Sync single class mode from PR#106 + self.single_class_mode = QAction(get_str('singleClsMode'), self) + self.single_class_mode.setShortcut("Ctrl+Shift+S") + self.single_class_mode.setCheckable(True) + self.single_class_mode.setChecked(settings.get(SETTING_SINGLE_CLASS, False)) + self.lastLabel = None + # Add option to enable/disable labels being displayed at the top of bounding boxes + self.display_label_option = QAction(get_str('displayLabel'), self) + self.display_label_option.setShortcut("Ctrl+Shift+P") + self.display_label_option.setCheckable(True) + self.display_label_option.setChecked(settings.get(SETTING_PAINT_LABEL, False)) + self.display_label_option.triggered.connect(self.toggle_paint_labels_option) + + add_actions(self.menus.file, + (open, open_dir, change_save_dir, open_annotation, copy_prev_bounding, self.menus.recentFiles, save, save_format, save_as, close, reset_all, delete_image, quit)) + add_actions(self.menus.help, (help_default, show_info, show_shortcut)) + add_actions(self.menus.view, ( + self.auto_saving, + self.single_class_mode, + self.display_label_option, + labels, advanced_mode, None, + hide_all, show_all, None, + zoom_in, zoom_out, zoom_org, None, + fit_window, fit_width, None, + light_brighten, light_darken, light_org)) + + self.menus.file.aboutToShow.connect(self.update_file_menu) + + # Custom context menu for the canvas widget: + add_actions(self.canvas.menus[0], self.actions.beginnerContext) + add_actions(self.canvas.menus[1], ( + action('&Copy here', self.copy_shape), + action('&Move here', self.move_shape))) + + self.tools = self.toolbar('Tools') + self.actions.beginner = ( + open, open_dir, change_save_dir, open_next_image, open_prev_image, verify, save, save_format, None, create, copy, delete, None, + zoom_in, zoom, zoom_out, fit_window, fit_width, None, + light_brighten, light, light_darken, light_org) + + self.actions.advanced = ( + open, open_dir, change_save_dir, open_next_image, open_prev_image, save, save_format, None, + create_mode, edit_mode, None, + hide_all, show_all) + + self.statusBar().showMessage('%s started.' % __appname__) + self.statusBar().show() + + # Application state. + self.image = QImage() + self.file_path = ustr(default_filename) + self.last_open_dir = None + self.recent_files = [] + self.max_recent = 7 + self.line_color = None + self.fill_color = None + self.zoom_level = 100 + self.fit_window = False + # Add Chris + self.difficult = False + + # Fix the compatible issue for qt4 and qt5. Convert the QStringList to python list + if settings.get(SETTING_RECENT_FILES): + if have_qstring(): + recent_file_qstring_list = settings.get(SETTING_RECENT_FILES) + self.recent_files = [ustr(i) for i in recent_file_qstring_list] + else: + self.recent_files = recent_file_qstring_list = settings.get(SETTING_RECENT_FILES) + + size = settings.get(SETTING_WIN_SIZE, QSize(600, 500)) + position = QPoint(0, 0) + saved_position = settings.get(SETTING_WIN_POSE, position) + # Fix the multiple monitors issue + for i in range(QApplication.desktop().screenCount()): + if QApplication.desktop().availableGeometry(i).contains(saved_position): + position = saved_position + break + self.resize(size) + self.move(position) + save_dir = ustr(settings.get(SETTING_SAVE_DIR, None)) + self.last_open_dir = ustr(settings.get(SETTING_LAST_OPEN_DIR, None)) + if self.default_save_dir is None and save_dir is not None and os.path.exists(save_dir): + self.default_save_dir = save_dir + self.statusBar().showMessage('%s started. Annotation will be saved to %s' % + (__appname__, self.default_save_dir)) + self.statusBar().show() + + self.restoreState(settings.get(SETTING_WIN_STATE, QByteArray())) + Shape.line_color = self.line_color = QColor(settings.get(SETTING_LINE_COLOR, DEFAULT_LINE_COLOR)) + Shape.fill_color = self.fill_color = QColor(settings.get(SETTING_FILL_COLOR, DEFAULT_FILL_COLOR)) + self.canvas.set_drawing_color(self.line_color) + # Add chris + Shape.difficult = self.difficult + + def xbool(x): + if isinstance(x, QVariant): + return x.toBool() + return bool(x) + + if xbool(settings.get(SETTING_ADVANCE_MODE, False)): + self.actions.advancedMode.setChecked(True) + self.toggle_advanced_mode() + + # Populate the File menu dynamically. + self.update_file_menu() + + # Since loading the file may take some time, make sure it runs in the background. + if self.file_path and os.path.isdir(self.file_path): + self.queue_event(partial(self.import_dir_images, self.file_path or "")) + elif self.file_path: + self.queue_event(partial(self.load_file, self.file_path or "")) + + # Callbacks: + self.zoom_widget.valueChanged.connect(self.paint_canvas) + self.light_widget.valueChanged.connect(self.paint_canvas) + + self.populate_mode_actions() + + # Display cursor coordinates at the right of status bar + self.label_coordinates = QLabel('') + self.statusBar().addPermanentWidget(self.label_coordinates) + + # Open Dir if default file + if self.file_path and os.path.isdir(self.file_path): + self.open_dir_dialog(dir_path=self.file_path, silent=True) + + def keyReleaseEvent(self, event): + if event.key() == Qt.Key_Control: + self.canvas.set_drawing_shape_to_square(False) + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Control: + # Draw rectangle if Ctrl is pressed + self.canvas.set_drawing_shape_to_square(True) + + # Support Functions # + def set_format(self, save_format): + if save_format == FORMAT_PASCALVOC: + self.actions.save_format.setText(FORMAT_PASCALVOC) + self.actions.save_format.setIcon(new_icon("format_voc")) + self.label_file_format = LabelFileFormat.PASCAL_VOC + LabelFile.suffix = XML_EXT + + elif save_format == FORMAT_YOLO: + self.actions.save_format.setText(FORMAT_YOLO) + self.actions.save_format.setIcon(new_icon("format_yolo")) + self.label_file_format = LabelFileFormat.YOLO + LabelFile.suffix = TXT_EXT + + elif save_format == FORMAT_CREATEML: + self.actions.save_format.setText(FORMAT_CREATEML) + self.actions.save_format.setIcon(new_icon("format_createml")) + self.label_file_format = LabelFileFormat.CREATE_ML + LabelFile.suffix = JSON_EXT + + def change_format(self): + if self.label_file_format == LabelFileFormat.PASCAL_VOC: + self.set_format(FORMAT_YOLO) + elif self.label_file_format == LabelFileFormat.YOLO: + self.set_format(FORMAT_CREATEML) + elif self.label_file_format == LabelFileFormat.CREATE_ML: + self.set_format(FORMAT_PASCALVOC) + else: + raise ValueError('Unknown label file format.') + self.set_dirty() + + def no_shapes(self): + return not self.items_to_shapes + + def toggle_advanced_mode(self, value=True): + self._beginner = not value + self.canvas.set_editing(True) + self.populate_mode_actions() + self.edit_button.setVisible(not value) + if value: + self.actions.createMode.setEnabled(True) + self.actions.editMode.setEnabled(False) + self.dock.setFeatures(self.dock.features() | self.dock_features) + else: + self.dock.setFeatures(self.dock.features() ^ self.dock_features) + + def populate_mode_actions(self): + if self.beginner(): + tool, menu = self.actions.beginner, self.actions.beginnerContext + else: + tool, menu = self.actions.advanced, self.actions.advancedContext + self.tools.clear() + add_actions(self.tools, tool) + self.canvas.menus[0].clear() + add_actions(self.canvas.menus[0], menu) + self.menus.edit.clear() + actions = (self.actions.create,) if self.beginner()\ + else (self.actions.createMode, self.actions.editMode) + add_actions(self.menus.edit, actions + self.actions.editMenu) + + def set_beginner(self): + self.tools.clear() + add_actions(self.tools, self.actions.beginner) + + def set_advanced(self): + self.tools.clear() + add_actions(self.tools, self.actions.advanced) + + def set_dirty(self): + self.dirty = True + self.actions.save.setEnabled(True) + + def set_clean(self): + self.dirty = False + self.actions.save.setEnabled(False) + self.actions.create.setEnabled(True) + + def toggle_actions(self, value=True): + """Enable/Disable widgets which depend on an opened image.""" + for z in self.actions.zoomActions: + z.setEnabled(value) + for z in self.actions.lightActions: + z.setEnabled(value) + for action in self.actions.onLoadActive: + action.setEnabled(value) + + def queue_event(self, function): + QTimer.singleShot(0, function) + + def status(self, message, delay=5000): + self.statusBar().showMessage(message, delay) + + def reset_state(self): + self.items_to_shapes.clear() + self.shapes_to_items.clear() + self.label_list.clear() + self.file_path = None + self.image_data = None + self.label_file = None + self.canvas.reset_state() + self.label_coordinates.clear() + self.combo_box.cb.clear() + + def current_item(self): + items = self.label_list.selectedItems() + if items: + return items[0] + return None + + def add_recent_file(self, file_path): + if file_path in self.recent_files: + self.recent_files.remove(file_path) + elif len(self.recent_files) >= self.max_recent: + self.recent_files.pop() + self.recent_files.insert(0, file_path) + + def beginner(self): + return self._beginner + + def advanced(self): + return not self.beginner() + + def show_tutorial_dialog(self, browser='default', link=None): + if link is None: + link = self.screencast + + if browser.lower() == 'default': + wb.open(link, new=2) + elif browser.lower() == 'chrome' and self.os_name == 'Windows': + if shutil.which(browser.lower()): # 'chrome' not in wb._browsers in windows + wb.register('chrome', None, wb.BackgroundBrowser('chrome')) + else: + chrome_path="D:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" + if os.path.isfile(chrome_path): + wb.register('chrome', None, wb.BackgroundBrowser(chrome_path)) + try: + wb.get('chrome').open(link, new=2) + except: + wb.open(link, new=2) + elif browser.lower() in wb._browsers: + wb.get(browser.lower()).open(link, new=2) + + def show_default_tutorial_dialog(self): + self.show_tutorial_dialog(browser='default') + + def show_info_dialog(self): + from libs.__init__ import __version__ + msg = u'Name:{0} \nApp Version:{1} \n{2} '.format(__appname__, __version__, sys.version_info) + QMessageBox.information(self, u'Information', msg) + + def show_shortcuts_dialog(self): + self.show_tutorial_dialog(browser='default', link='https://github.com/tzutalin/labelImg#Hotkeys') + + def create_shape(self): + assert self.beginner() + self.canvas.set_editing(False) + self.actions.create.setEnabled(False) + + def toggle_drawing_sensitive(self, drawing=True): + """In the middle of drawing, toggling between modes should be disabled.""" + self.actions.editMode.setEnabled(not drawing) + if not drawing and self.beginner(): + # Cancel creation. + print('Cancel creation.') + self.canvas.set_editing(True) + self.canvas.restore_cursor() + self.actions.create.setEnabled(True) + + def toggle_draw_mode(self, edit=True): + self.canvas.set_editing(edit) + self.actions.createMode.setEnabled(edit) + self.actions.editMode.setEnabled(not edit) + + def set_create_mode(self): + assert self.advanced() + self.toggle_draw_mode(False) + + def set_edit_mode(self): + assert self.advanced() + self.toggle_draw_mode(True) + self.label_selection_changed() + + def update_file_menu(self): + curr_file_path = self.file_path + + def exists(filename): + return os.path.exists(filename) + menu = self.menus.recentFiles + menu.clear() + files = [f for f in self.recent_files if f != + curr_file_path and exists(f)] + for i, f in enumerate(files): + icon = new_icon('labels') + action = QAction( + icon, '&%d %s' % (i + 1, QFileInfo(f).fileName()), self) + action.triggered.connect(partial(self.load_recent, f)) + menu.addAction(action) + + def pop_label_list_menu(self, point): + self.menus.labelList.exec_(self.label_list.mapToGlobal(point)) + + def edit_label(self): + if not self.canvas.editing(): + return + item = self.current_item() + if not item: + return + text = self.label_dialog.pop_up(item.text()) + if text is not None: + item.setText(text) + item.setBackground(generate_color_by_text(text)) + self.set_dirty() + self.update_combo_box() + + # Tzutalin 20160906 : Add file list and dock to move faster + def file_item_double_clicked(self, item=None): + self.cur_img_idx = self.m_img_list.index(ustr(item.text())) + filename = self.m_img_list[self.cur_img_idx] + if filename: + self.load_file(filename) + + # Add chris + def button_state(self, item=None): + """ Function to handle difficult examples + Update on each object """ + if not self.canvas.editing(): + return + + item = self.current_item() + if not item: # If not selected Item, take the first one + item = self.label_list.item(self.label_list.count() - 1) + + difficult = self.diffc_button.isChecked() + + try: + shape = self.items_to_shapes[item] + except: + pass + # Checked and Update + try: + if difficult != shape.difficult: + shape.difficult = difficult + self.set_dirty() + else: # User probably changed item visibility + self.canvas.set_shape_visible(shape, item.checkState() == Qt.Checked) + except: + pass + + # React to canvas signals. + def shape_selection_changed(self, selected=False): + if self._no_selection_slot: + self._no_selection_slot = False + else: + shape = self.canvas.selected_shape + if shape: + self.shapes_to_items[shape].setSelected(True) + else: + self.label_list.clearSelection() + self.actions.delete.setEnabled(selected) + self.actions.copy.setEnabled(selected) + self.actions.edit.setEnabled(selected) + self.actions.shapeLineColor.setEnabled(selected) + self.actions.shapeFillColor.setEnabled(selected) + + def add_label(self, shape): + shape.paint_label = self.display_label_option.isChecked() + item = HashableQListWidgetItem(shape.label) + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Checked) + item.setBackground(generate_color_by_text(shape.label)) + self.items_to_shapes[item] = shape + self.shapes_to_items[shape] = item + self.label_list.addItem(item) + for action in self.actions.onShapesPresent: + action.setEnabled(True) + self.update_combo_box() + + def remove_label(self, shape): + if shape is None: + # print('rm empty label') + return + item = self.shapes_to_items[shape] + self.label_list.takeItem(self.label_list.row(item)) + del self.shapes_to_items[shape] + del self.items_to_shapes[item] + self.update_combo_box() + + def load_labels(self, shapes): + s = [] + for label, points, line_color, fill_color, difficult in shapes: + shape = Shape(label=label) + for x, y in points: + + # Ensure the labels are within the bounds of the image. If not, fix them. + x, y, snapped = self.canvas.snap_point_to_canvas(x, y) + if snapped: + self.set_dirty() + + shape.add_point(QPointF(x, y)) + shape.difficult = difficult + shape.close() + s.append(shape) + + if line_color: + shape.line_color = QColor(*line_color) + else: + shape.line_color = generate_color_by_text(label) + + if fill_color: + shape.fill_color = QColor(*fill_color) + else: + shape.fill_color = generate_color_by_text(label) + + self.add_label(shape) + self.update_combo_box() + self.canvas.load_shapes(s) + + def update_combo_box(self): + # Get the unique labels and add them to the Combobox. + items_text_list = [str(self.label_list.item(i).text()) for i in range(self.label_list.count())] + + unique_text_list = list(set(items_text_list)) + # Add a null row for showing all the labels + unique_text_list.append("") + unique_text_list.sort() + + self.combo_box.update_items(unique_text_list) + + def save_labels(self, annotation_file_path): + annotation_file_path = ustr(annotation_file_path) + if self.label_file is None: + self.label_file = LabelFile() + self.label_file.verified = self.canvas.verified + + def format_shape(s): + return dict(label=s.label, + line_color=s.line_color.getRgb(), + fill_color=s.fill_color.getRgb(), + points=[(p.x(), p.y()) for p in s.points], + # add chris + difficult=s.difficult) + + shapes = [format_shape(shape) for shape in self.canvas.shapes] + # Can add different annotation formats here + try: + if self.label_file_format == LabelFileFormat.PASCAL_VOC: + if annotation_file_path[-4:].lower() != ".xml": + annotation_file_path += XML_EXT + self.label_file.save_pascal_voc_format(annotation_file_path, shapes, self.file_path, self.image_data, + self.line_color.getRgb(), self.fill_color.getRgb()) + elif self.label_file_format == LabelFileFormat.YOLO: + if annotation_file_path[-4:].lower() != ".txt": + annotation_file_path += TXT_EXT + self.label_file.save_yolo_format(annotation_file_path, shapes, self.file_path, self.image_data, self.label_hist, + self.line_color.getRgb(), self.fill_color.getRgb()) + elif self.label_file_format == LabelFileFormat.CREATE_ML: + if annotation_file_path[-5:].lower() != ".json": + annotation_file_path += JSON_EXT + self.label_file.save_create_ml_format(annotation_file_path, shapes, self.file_path, self.image_data, + self.label_hist, self.line_color.getRgb(), self.fill_color.getRgb()) + else: + self.label_file.save(annotation_file_path, shapes, self.file_path, self.image_data, + self.line_color.getRgb(), self.fill_color.getRgb()) + print('Image:{0} -> Annotation:{1}'.format(self.file_path, annotation_file_path)) + return True + except LabelFileError as e: + self.error_message(u'Error saving label data', u'%s' % e) + return False + + def copy_selected_shape(self): + self.add_label(self.canvas.copy_selected_shape()) + # fix copy and delete + self.shape_selection_changed(True) + + def combo_selection_changed(self, index): + text = self.combo_box.cb.itemText(index) + for i in range(self.label_list.count()): + if text == "": + self.label_list.item(i).setCheckState(2) + elif text != self.label_list.item(i).text(): + self.label_list.item(i).setCheckState(0) + else: + self.label_list.item(i).setCheckState(2) + + def default_label_combo_selection_changed(self, index): + self.default_label=self.label_hist[index] + + def label_selection_changed(self): + item = self.current_item() + if item and self.canvas.editing(): + self._no_selection_slot = True + self.canvas.select_shape(self.items_to_shapes[item]) + shape = self.items_to_shapes[item] + # Add Chris + self.diffc_button.setChecked(shape.difficult) + + def label_item_changed(self, item): + shape = self.items_to_shapes[item] + label = item.text() + if label != shape.label: + shape.label = item.text() + shape.line_color = generate_color_by_text(shape.label) + self.set_dirty() + else: # User probably changed item visibility + self.canvas.set_shape_visible(shape, item.checkState() == Qt.Checked) + + # Callback functions: + def new_shape(self): + """Pop-up and give focus to the label editor. + + position MUST be in global coordinates. + """ + if not self.use_default_label_checkbox.isChecked(): + if len(self.label_hist) > 0: + self.label_dialog = LabelDialog( + parent=self, list_item=self.label_hist) + + # Sync single class mode from PR#106 + if self.single_class_mode.isChecked() and self.lastLabel: + text = self.lastLabel + else: + text = self.label_dialog.pop_up(text=self.prev_label_text) + self.lastLabel = text + else: + text = self.default_label + + # Add Chris + self.diffc_button.setChecked(False) + if text is not None: + self.prev_label_text = text + generate_color = generate_color_by_text(text) + shape = self.canvas.set_last_label(text, generate_color, generate_color) + self.add_label(shape) + if self.beginner(): # Switch to edit mode. + self.canvas.set_editing(True) + self.actions.create.setEnabled(True) + else: + self.actions.editMode.setEnabled(True) + self.set_dirty() + + if text not in self.label_hist: + self.label_hist.append(text) + else: + # self.canvas.undoLastLine() + self.canvas.reset_all_lines() + + def scroll_request(self, delta, orientation): + units = - delta / (8 * 15) + bar = self.scroll_bars[orientation] + bar.setValue(int(bar.value() + bar.singleStep() * units)) + + def set_zoom(self, value): + self.actions.fitWidth.setChecked(False) + self.actions.fitWindow.setChecked(False) + self.zoom_mode = self.MANUAL_ZOOM + # Arithmetic on scaling factor often results in float + # Convert to int to avoid type errors + self.zoom_widget.setValue(int(value)) + + def add_zoom(self, increment=10): + self.set_zoom(self.zoom_widget.value() + increment) + + def zoom_request(self, delta): + # get the current scrollbar positions + # calculate the percentages ~ coordinates + h_bar = self.scroll_bars[Qt.Horizontal] + v_bar = self.scroll_bars[Qt.Vertical] + + # get the current maximum, to know the difference after zooming + h_bar_max = h_bar.maximum() + v_bar_max = v_bar.maximum() + + # get the cursor position and canvas size + # calculate the desired movement from 0 to 1 + # where 0 = move left + # 1 = move right + # up and down analogous + cursor = QCursor() + pos = cursor.pos() + relative_pos = QWidget.mapFromGlobal(self, pos) + + cursor_x = relative_pos.x() + cursor_y = relative_pos.y() + + w = self.scroll_area.width() + h = self.scroll_area.height() + + # the scaling from 0 to 1 has some padding + # you don't have to hit the very leftmost pixel for a maximum-left movement + margin = 0.1 + move_x = (cursor_x - margin * w) / (w - 2 * margin * w) + move_y = (cursor_y - margin * h) / (h - 2 * margin * h) + + # clamp the values from 0 to 1 + move_x = min(max(move_x, 0), 1) + move_y = min(max(move_y, 0), 1) + + # zoom in + units = delta // (8 * 15) + scale = 10 + self.add_zoom(scale * units) + + # get the difference in scrollbar values + # this is how far we can move + d_h_bar_max = h_bar.maximum() - h_bar_max + d_v_bar_max = v_bar.maximum() - v_bar_max + + # get the new scrollbar values + new_h_bar_value = int(h_bar.value() + move_x * d_h_bar_max) + new_v_bar_value = int(v_bar.value() + move_y * d_v_bar_max) + + h_bar.setValue(new_h_bar_value) + v_bar.setValue(new_v_bar_value) + + def light_request(self, delta): + self.add_light(5*delta // (8 * 15)) + + def set_fit_window(self, value=True): + if value: + self.actions.fitWidth.setChecked(False) + self.zoom_mode = self.FIT_WINDOW if value else self.MANUAL_ZOOM + self.adjust_scale() + + def set_fit_width(self, value=True): + if value: + self.actions.fitWindow.setChecked(False) + self.zoom_mode = self.FIT_WIDTH if value else self.MANUAL_ZOOM + self.adjust_scale() + + def set_light(self, value): + self.actions.lightOrg.setChecked(int(value) == 50) + # Arithmetic on scaling factor often results in float + # Convert to int to avoid type errors + self.light_widget.setValue(int(value)) + + def add_light(self, increment=10): + self.set_light(self.light_widget.value() + increment) + + def toggle_polygons(self, value): + for item, shape in self.items_to_shapes.items(): + item.setCheckState(Qt.Checked if value else Qt.Unchecked) + + def load_file(self, file_path=None): + """Load the specified file, or the last opened file if None.""" + self.reset_state() + self.canvas.setEnabled(False) + if file_path is None: + file_path = self.settings.get(SETTING_FILENAME) + # Make sure that filePath is a regular python string, rather than QString + file_path = ustr(file_path) + + # Fix bug: An index error after select a directory when open a new file. + unicode_file_path = ustr(file_path) + unicode_file_path = os.path.abspath(unicode_file_path) + # Tzutalin 20160906 : Add file list and dock to move faster + # Highlight the file item + if unicode_file_path and self.file_list_widget.count() > 0: + if unicode_file_path in self.m_img_list: + index = self.m_img_list.index(unicode_file_path) + file_widget_item = self.file_list_widget.item(index) + file_widget_item.setSelected(True) + else: + self.file_list_widget.clear() + self.m_img_list.clear() + + if unicode_file_path and os.path.exists(unicode_file_path): + if LabelFile.is_label_file(unicode_file_path): + try: + self.label_file = LabelFile(unicode_file_path) + except LabelFileError as e: + self.error_message(u'Error opening file', + (u"

%s

" + u"

Make sure %s is a valid label file.") + % (e, unicode_file_path)) + self.status("Error reading %s" % unicode_file_path) + + return False + self.image_data = self.label_file.image_data + self.line_color = QColor(*self.label_file.lineColor) + self.fill_color = QColor(*self.label_file.fillColor) + self.canvas.verified = self.label_file.verified + else: + # Load image: + # read data first and store for saving into label file. + self.image_data = read(unicode_file_path, None) + self.label_file = None + self.canvas.verified = False + + if isinstance(self.image_data, QImage): + image = self.image_data + else: + image = QImage.fromData(self.image_data) + if image.isNull(): + self.error_message(u'Error opening file', + u"

Make sure %s is a valid image file." % unicode_file_path) + self.status("Error reading %s" % unicode_file_path) + return False + self.status("Loaded %s" % os.path.basename(unicode_file_path)) + self.image = image + self.file_path = unicode_file_path + self.canvas.load_pixmap(QPixmap.fromImage(image)) + if self.label_file: + self.load_labels(self.label_file.shapes) + self.set_clean() + self.canvas.setEnabled(True) + self.adjust_scale(initial=True) + self.paint_canvas() + self.add_recent_file(self.file_path) + self.toggle_actions(True) + self.show_bounding_box_from_annotation_file(self.file_path) + + counter = self.counter_str() + self.setWindowTitle(__appname__ + ' ' + file_path + ' ' + counter) + + # Default : select last item if there is at least one item + if self.label_list.count(): + self.label_list.setCurrentItem(self.label_list.item(self.label_list.count() - 1)) + self.label_list.item(self.label_list.count() - 1).setSelected(True) + + self.canvas.setFocus(True) + return True + return False + + def counter_str(self): + """ + Converts image counter to string representation. + """ + return '[{} / {}]'.format(self.cur_img_idx + 1, self.img_count) + + def show_bounding_box_from_annotation_file(self, file_path): + if self.default_save_dir is not None: + basename = os.path.basename(os.path.splitext(file_path)[0]) + xml_path = os.path.join(self.default_save_dir, basename + XML_EXT) + txt_path = os.path.join(self.default_save_dir, basename + TXT_EXT) + json_path = os.path.join(self.default_save_dir, basename + JSON_EXT) + + """Annotation file priority: + PascalXML > YOLO + """ + if os.path.isfile(xml_path): + self.load_pascal_xml_by_filename(xml_path) + elif os.path.isfile(txt_path): + self.load_yolo_txt_by_filename(txt_path) + elif os.path.isfile(json_path): + self.load_create_ml_json_by_filename(json_path, file_path) + + else: + xml_path = os.path.splitext(file_path)[0] + XML_EXT + txt_path = os.path.splitext(file_path)[0] + TXT_EXT + json_path = os.path.splitext(file_path)[0] + JSON_EXT + + if os.path.isfile(xml_path): + self.load_pascal_xml_by_filename(xml_path) + elif os.path.isfile(txt_path): + self.load_yolo_txt_by_filename(txt_path) + elif os.path.isfile(json_path): + self.load_create_ml_json_by_filename(json_path, file_path) + + + def resizeEvent(self, event): + if self.canvas and not self.image.isNull()\ + and self.zoom_mode != self.MANUAL_ZOOM: + self.adjust_scale() + super(MainWindow, self).resizeEvent(event) + + def paint_canvas(self): + assert not self.image.isNull(), "cannot paint null image" + self.canvas.scale = 0.01 * self.zoom_widget.value() + self.canvas.overlay_color = self.light_widget.color() + self.canvas.label_font_size = int(0.02 * max(self.image.width(), self.image.height())) + self.canvas.adjustSize() + self.canvas.update() + + def adjust_scale(self, initial=False): + value = self.scalers[self.FIT_WINDOW if initial else self.zoom_mode]() + self.zoom_widget.setValue(int(100 * value)) + + def scale_fit_window(self): + """Figure out the size of the pixmap in order to fit the main widget.""" + e = 2.0 # So that no scrollbars are generated. + w1 = self.centralWidget().width() - e + h1 = self.centralWidget().height() - e + a1 = w1 / h1 + # Calculate a new scale value based on the pixmap's aspect ratio. + w2 = self.canvas.pixmap.width() - 0.0 + h2 = self.canvas.pixmap.height() - 0.0 + a2 = w2 / h2 + return w1 / w2 if a2 >= a1 else h1 / h2 + + def scale_fit_width(self): + # The epsilon does not seem to work too well here. + w = self.centralWidget().width() - 2.0 + return w / self.canvas.pixmap.width() + + def closeEvent(self, event): + if not self.may_continue(): + event.ignore() + settings = self.settings + # If it loads images from dir, don't load it at the beginning + if self.dir_name is None: + settings[SETTING_FILENAME] = self.file_path if self.file_path else '' + else: + settings[SETTING_FILENAME] = '' + + settings[SETTING_WIN_SIZE] = self.size() + settings[SETTING_WIN_POSE] = self.pos() + settings[SETTING_WIN_STATE] = self.saveState() + settings[SETTING_LINE_COLOR] = self.line_color + settings[SETTING_FILL_COLOR] = self.fill_color + settings[SETTING_RECENT_FILES] = self.recent_files + settings[SETTING_ADVANCE_MODE] = not self._beginner + if self.default_save_dir and os.path.exists(self.default_save_dir): + settings[SETTING_SAVE_DIR] = ustr(self.default_save_dir) + else: + settings[SETTING_SAVE_DIR] = '' + + if self.last_open_dir and os.path.exists(self.last_open_dir): + settings[SETTING_LAST_OPEN_DIR] = self.last_open_dir + else: + settings[SETTING_LAST_OPEN_DIR] = '' + + settings[SETTING_AUTO_SAVE] = self.auto_saving.isChecked() + settings[SETTING_SINGLE_CLASS] = self.single_class_mode.isChecked() + settings[SETTING_PAINT_LABEL] = self.display_label_option.isChecked() + settings[SETTING_DRAW_SQUARE] = self.draw_squares_option.isChecked() + settings[SETTING_LABEL_FILE_FORMAT] = self.label_file_format + settings.save() + + def load_recent(self, filename): + if self.may_continue(): + self.load_file(filename) + + def scan_all_images(self, folder_path): + extensions = ['.%s' % fmt.data().decode("ascii").lower() for fmt in QImageReader.supportedImageFormats()] + images = [] + + for root, dirs, files in os.walk(folder_path): + for file in files: + if file.lower().endswith(tuple(extensions)): + relative_path = os.path.join(root, file) + path = ustr(os.path.abspath(relative_path)) + images.append(path) + natural_sort(images, key=lambda x: x.lower()) + return images + + def change_save_dir_dialog(self, _value=False): + if self.default_save_dir is not None: + path = ustr(self.default_save_dir) + else: + path = '.' + + dir_path = ustr(QFileDialog.getExistingDirectory(self, + '%s - Save annotations to the directory' % __appname__, path, QFileDialog.ShowDirsOnly + | QFileDialog.DontResolveSymlinks)) + + if dir_path is not None and len(dir_path) > 1: + self.default_save_dir = dir_path + + self.show_bounding_box_from_annotation_file(self.file_path) + + self.statusBar().showMessage('%s . Annotation will be saved to %s' % + ('Change saved folder', self.default_save_dir)) + self.statusBar().show() + + + def open_annotation_dialog(self, _value=False): + if self.file_path is None: + self.statusBar().showMessage('Please select image first') + self.statusBar().show() + return + + path = os.path.dirname(ustr(self.file_path))\ + if self.file_path else '.' + if self.label_file_format == LabelFileFormat.PASCAL_VOC: + filters = "Open Annotation XML file (%s)" % ' '.join(['*.xml']) + filename = ustr(QFileDialog.getOpenFileName(self, '%s - Choose a xml file' % __appname__, path, filters)) + if filename: + if isinstance(filename, (tuple, list)): + filename = filename[0] + self.load_pascal_xml_by_filename(filename) + + elif self.label_file_format == LabelFileFormat.CREATE_ML: + + filters = "Open Annotation JSON file (%s)" % ' '.join(['*.json']) + filename = ustr(QFileDialog.getOpenFileName(self, '%s - Choose a json file' % __appname__, path, filters)) + if filename: + if isinstance(filename, (tuple, list)): + filename = filename[0] + + self.load_create_ml_json_by_filename(filename, self.file_path) + + + def open_dir_dialog(self, _value=False, dir_path=None, silent=False): + if not self.may_continue(): + return + + default_open_dir_path = dir_path if dir_path else '.' + if self.last_open_dir and os.path.exists(self.last_open_dir): + default_open_dir_path = self.last_open_dir + else: + default_open_dir_path = os.path.dirname(self.file_path) if self.file_path else '.' + if silent != True: + target_dir_path = ustr(QFileDialog.getExistingDirectory(self, + '%s - Open Directory' % __appname__, default_open_dir_path, + QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)) + else: + target_dir_path = ustr(default_open_dir_path) + self.last_open_dir = target_dir_path + self.import_dir_images(target_dir_path) + self.default_save_dir = target_dir_path + if self.file_path: + self.show_bounding_box_from_annotation_file(file_path=self.file_path) + + def import_dir_images(self, dir_path): + if not self.may_continue() or not dir_path: + return + + self.last_open_dir = dir_path + self.dir_name = dir_path + self.file_path = None + self.file_list_widget.clear() + self.m_img_list = self.scan_all_images(dir_path) + self.img_count = len(self.m_img_list) + self.open_next_image() + for imgPath in self.m_img_list: + item = QListWidgetItem(imgPath) + self.file_list_widget.addItem(item) + + def verify_image(self, _value=False): + # Proceeding next image without dialog if having any label + if self.file_path is not None: + try: + self.label_file.toggle_verify() + except AttributeError: + # If the labelling file does not exist yet, create if and + # re-save it with the verified attribute. + self.save_file() + if self.label_file is not None: + self.label_file.toggle_verify() + else: + return + + self.canvas.verified = self.label_file.verified + self.paint_canvas() + self.save_file() + + def open_prev_image(self, _value=False): + # Proceeding prev image without dialog if having any label + if self.auto_saving.isChecked(): + if self.default_save_dir is not None: + if self.dirty is True: + self.save_file() + else: + self.change_save_dir_dialog() + return + + if not self.may_continue(): + return + + if self.img_count <= 0: + return + + if self.file_path is None: + return + + if self.cur_img_idx - 1 >= 0: + self.cur_img_idx -= 1 + filename = self.m_img_list[self.cur_img_idx] + if filename: + self.load_file(filename) + + def open_next_image(self, _value=False): + # Proceeding next image without dialog if having any label + if self.auto_saving.isChecked(): + if self.default_save_dir is not None: + if self.dirty is True: + self.save_file() + else: + self.change_save_dir_dialog() + return + + if not self.may_continue(): + return + + if self.img_count <= 0: + return + + if not self.m_img_list: + return + + filename = None + if self.file_path is None: + filename = self.m_img_list[0] + self.cur_img_idx = 0 + else: + if self.cur_img_idx + 1 < self.img_count: + self.cur_img_idx += 1 + filename = self.m_img_list[self.cur_img_idx] + + if filename: + self.load_file(filename) + + def open_file(self, _value=False): + if not self.may_continue(): + return + path = os.path.dirname(ustr(self.file_path)) if self.file_path else '.' + formats = ['*.%s' % fmt.data().decode("ascii").lower() for fmt in QImageReader.supportedImageFormats()] + filters = "Image & Label files (%s)" % ' '.join(formats + ['*%s' % LabelFile.suffix]) + filename,_ = QFileDialog.getOpenFileName(self, '%s - Choose Image or Label file' % __appname__, path, filters) + if filename: + if isinstance(filename, (tuple, list)): + filename = filename[0] + self.cur_img_idx = 0 + self.img_count = 1 + self.load_file(filename) + + def save_file(self, _value=False): + if self.default_save_dir is not None and len(ustr(self.default_save_dir)): + if self.file_path: + image_file_name = os.path.basename(self.file_path) + saved_file_name = os.path.splitext(image_file_name)[0] + saved_path = os.path.join(ustr(self.default_save_dir), saved_file_name) + self._save_file(saved_path) + else: + image_file_dir = os.path.dirname(self.file_path) + image_file_name = os.path.basename(self.file_path) + saved_file_name = os.path.splitext(image_file_name)[0] + saved_path = os.path.join(image_file_dir, saved_file_name) + self._save_file(saved_path if self.label_file + else self.save_file_dialog(remove_ext=False)) + + def save_file_as(self, _value=False): + assert not self.image.isNull(), "cannot save empty image" + self._save_file(self.save_file_dialog()) + + def save_file_dialog(self, remove_ext=True): + caption = '%s - Choose File' % __appname__ + filters = 'File (*%s)' % LabelFile.suffix + open_dialog_path = self.current_path() + dlg = QFileDialog(self, caption, open_dialog_path, filters) + dlg.setDefaultSuffix(LabelFile.suffix[1:]) + dlg.setAcceptMode(QFileDialog.AcceptSave) + filename_without_extension = os.path.splitext(self.file_path)[0] + dlg.selectFile(filename_without_extension) + dlg.setOption(QFileDialog.DontUseNativeDialog, False) + if dlg.exec_(): + full_file_path = ustr(dlg.selectedFiles()[0]) + if remove_ext: + return os.path.splitext(full_file_path)[0] # Return file path without the extension. + else: + return full_file_path + return '' + + def _save_file(self, annotation_file_path): + if annotation_file_path and self.save_labels(annotation_file_path): + self.set_clean() + self.statusBar().showMessage('Saved to %s' % annotation_file_path) + self.statusBar().show() + + def close_file(self, _value=False): + if not self.may_continue(): + return + self.reset_state() + self.set_clean() + self.toggle_actions(False) + self.canvas.setEnabled(False) + self.actions.saveAs.setEnabled(False) + + def delete_image(self): + delete_path = self.file_path + if delete_path is not None: + idx = self.cur_img_idx + if os.path.exists(delete_path): + os.remove(delete_path) + self.import_dir_images(self.last_open_dir) + if self.img_count > 0: + self.cur_img_idx = min(idx, self.img_count - 1) + filename = self.m_img_list[self.cur_img_idx] + self.load_file(filename) + else: + self.close_file() + + def reset_all(self): + self.settings.reset() + self.close() + process = QProcess() + process.startDetached(os.path.abspath(__file__)) + + def may_continue(self): + if not self.dirty: + return True + else: + discard_changes = self.discard_changes_dialog() + if discard_changes == QMessageBox.No: + return True + elif discard_changes == QMessageBox.Yes: + self.save_file() + return True + else: + return False + + def discard_changes_dialog(self): + yes, no, cancel = QMessageBox.Yes, QMessageBox.No, QMessageBox.Cancel + msg = u'You have unsaved changes, would you like to save them and proceed?\nClick "No" to undo all changes.' + return QMessageBox.warning(self, u'Attention', msg, yes | no | cancel) + + def error_message(self, title, message): + return QMessageBox.critical(self, title, + '

%s

%s' % (title, message)) + + def current_path(self): + return os.path.dirname(self.file_path) if self.file_path else '.' + + def choose_color1(self): + color = self.color_dialog.getColor(self.line_color, u'Choose line color', + default=DEFAULT_LINE_COLOR) + if color: + self.line_color = color + Shape.line_color = color + self.canvas.set_drawing_color(color) + self.canvas.update() + self.set_dirty() + + def delete_selected_shape(self): + self.remove_label(self.canvas.delete_selected()) + self.set_dirty() + if self.no_shapes(): + for action in self.actions.onShapesPresent: + action.setEnabled(False) + + def choose_shape_line_color(self): + color = self.color_dialog.getColor(self.line_color, u'Choose Line Color', + default=DEFAULT_LINE_COLOR) + if color: + self.canvas.selected_shape.line_color = color + self.canvas.update() + self.set_dirty() + + def choose_shape_fill_color(self): + color = self.color_dialog.getColor(self.fill_color, u'Choose Fill Color', + default=DEFAULT_FILL_COLOR) + if color: + self.canvas.selected_shape.fill_color = color + self.canvas.update() + self.set_dirty() + + def copy_shape(self): + if self.canvas.selected_shape is None: + # True if one accidentally touches the left mouse button before releasing + return + self.canvas.end_move(copy=True) + self.add_label(self.canvas.selected_shape) + self.set_dirty() + + def move_shape(self): + self.canvas.end_move(copy=False) + self.set_dirty() + + def load_predefined_classes(self, predef_classes_file): + if os.path.exists(predef_classes_file) is True: + with codecs.open(predef_classes_file, 'r', 'utf8') as f: + for line in f: + line = line.strip() + if self.label_hist is None: + self.label_hist = [line] + else: + self.label_hist.append(line) + + def load_pascal_xml_by_filename(self, xml_path): + if self.file_path is None: + return + if os.path.isfile(xml_path) is False: + return + + self.set_format(FORMAT_PASCALVOC) + + t_voc_parse_reader = PascalVocReader(xml_path) + shapes = t_voc_parse_reader.get_shapes() + self.load_labels(shapes) + self.canvas.verified = t_voc_parse_reader.verified + + def load_yolo_txt_by_filename(self, txt_path): + if self.file_path is None: + return + if os.path.isfile(txt_path) is False: + return + + self.set_format(FORMAT_YOLO) + t_yolo_parse_reader = YoloReader(txt_path, self.image) + shapes = t_yolo_parse_reader.get_shapes() + print(shapes) + self.load_labels(shapes) + self.canvas.verified = t_yolo_parse_reader.verified + + def load_create_ml_json_by_filename(self, json_path, file_path): + if self.file_path is None: + return + if os.path.isfile(json_path) is False: + return + + self.set_format(FORMAT_CREATEML) + + create_ml_parse_reader = CreateMLReader(json_path, file_path) + shapes = create_ml_parse_reader.get_shapes() + self.load_labels(shapes) + self.canvas.verified = create_ml_parse_reader.verified + + def copy_previous_bounding_boxes(self): + current_index = self.m_img_list.index(self.file_path) + if current_index - 1 >= 0: + prev_file_path = self.m_img_list[current_index - 1] + self.show_bounding_box_from_annotation_file(prev_file_path) + self.save_file() + + def toggle_paint_labels_option(self): + for shape in self.canvas.shapes: + shape.paint_label = self.display_label_option.isChecked() + + def toggle_draw_square(self): + self.canvas.set_drawing_shape_to_square(self.draw_squares_option.isChecked()) + +def inverted(color): + return QColor(*[255 - v for v in color.getRgb()]) + + +def read(filename, default=None): + try: + reader = QImageReader(filename) + reader.setAutoTransform(True) + return reader.read() + except: + return default + + +def get_main_app(argv=None): + """ + Standard boilerplate Qt application code. + Do everything but app.exec_() -- so that we can test the application in one thread + """ + if not argv: + argv = [] + app = QApplication(argv) + app.setApplicationName(__appname__) + app.setWindowIcon(new_icon("app")) + # Tzutalin 201705+: Accept extra agruments to change predefined class file + argparser = argparse.ArgumentParser() + argparser.add_argument("image_dir", nargs="?") + argparser.add_argument("class_file", + default=os.path.join(os.path.dirname(__file__), "data", "predefined_classes.txt"), + nargs="?") + argparser.add_argument("save_dir", nargs="?") + args = argparser.parse_args(argv[1:]) + + args.image_dir = args.image_dir and os.path.normpath(args.image_dir) + args.class_file = args.class_file and os.path.normpath(args.class_file) + args.save_dir = args.save_dir and os.path.normpath(args.save_dir) + + # Usage : labelImg.py image classFile saveDir + win = MainWindow(args.image_dir, + args.class_file, + args.save_dir) + win.show() + return app, win + + +def main(): + """construct main app and run it""" + app, _win = get_main_app(sys.argv) + return app.exec_() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/labelImg/libs/__init__.py b/labelImg/libs/__init__.py new file mode 100644 index 00000000..6aaa95d4 --- /dev/null +++ b/labelImg/libs/__init__.py @@ -0,0 +1,2 @@ +__version_info__ = ('1', '8', '6') +__version__ = '.'.join(__version_info__) diff --git a/labelImg/libs/canvas.py b/labelImg/libs/canvas.py new file mode 100644 index 00000000..ca7986ff --- /dev/null +++ b/labelImg/libs/canvas.py @@ -0,0 +1,748 @@ + +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +# from PyQt4.QtOpenGL import * + +from libs.shape import Shape +from libs.utils import distance + +CURSOR_DEFAULT = Qt.ArrowCursor +CURSOR_POINT = Qt.PointingHandCursor +CURSOR_DRAW = Qt.CrossCursor +CURSOR_MOVE = Qt.ClosedHandCursor +CURSOR_GRAB = Qt.OpenHandCursor + +# class Canvas(QGLWidget): + + +class Canvas(QWidget): + zoomRequest = pyqtSignal(int) + lightRequest = pyqtSignal(int) + scrollRequest = pyqtSignal(int, int) + newShape = pyqtSignal() + selectionChanged = pyqtSignal(bool) + shapeMoved = pyqtSignal() + drawingPolygon = pyqtSignal(bool) + + CREATE, EDIT = list(range(2)) + + epsilon = 24.0 + + def __init__(self, *args, **kwargs): + super(Canvas, self).__init__(*args, **kwargs) + # Initialise local state. + self.mode = self.EDIT + self.shapes = [] + self.current = None + self.selected_shape = None # save the selected shape here + self.selected_shape_copy = None + self.drawing_line_color = QColor(0, 0, 255) + self.drawing_rect_color = QColor(0, 0, 255) + self.line = Shape(line_color=self.drawing_line_color) + self.prev_point = QPointF() + self.offsets = QPointF(), QPointF() + self.scale = 1.0 + self.overlay_color = None + self.label_font_size = 8 + self.pixmap = QPixmap() + self.visible = {} + self._hide_background = False + self.hide_background = False + self.h_shape = None + self.h_vertex = None + self._painter = QPainter() + self._cursor = CURSOR_DEFAULT + # Menus: + self.menus = (QMenu(), QMenu()) + # Set widget options. + self.setMouseTracking(True) + self.setFocusPolicy(Qt.WheelFocus) + self.verified = False + self.draw_square = False + + # initialisation for panning + self.pan_initial_pos = QPoint() + + def set_drawing_color(self, qcolor): + self.drawing_line_color = qcolor + self.drawing_rect_color = qcolor + + def enterEvent(self, ev): + self.override_cursor(self._cursor) + + def leaveEvent(self, ev): + self.restore_cursor() + + def focusOutEvent(self, ev): + self.restore_cursor() + + def isVisible(self, shape): + return self.visible.get(shape, True) + + def drawing(self): + return self.mode == self.CREATE + + def editing(self): + return self.mode == self.EDIT + + def set_editing(self, value=True): + self.mode = self.EDIT if value else self.CREATE + if not value: # Create + self.un_highlight() + self.de_select_shape() + self.prev_point = QPointF() + self.repaint() + + def un_highlight(self, shape=None): + if shape == None or shape == self.h_shape: + if self.h_shape: + self.h_shape.highlight_clear() + self.h_vertex = self.h_shape = None + + def selected_vertex(self): + return self.h_vertex is not None + + def mouseMoveEvent(self, ev): + """Update line with last point and current coordinates.""" + pos = self.transform_pos(ev.pos()) + + # Update coordinates in status bar if image is opened + window = self.parent().window() + if window.file_path is not None: + self.parent().window().label_coordinates.setText( + 'X: %d; Y: %d' % (pos.x(), pos.y())) + + # Polygon drawing. + if self.drawing(): + self.override_cursor(CURSOR_DRAW) + if self.current: + # Display annotation width and height while drawing + current_width = abs(self.current[0].x() - pos.x()) + current_height = abs(self.current[0].y() - pos.y()) + self.parent().window().label_coordinates.setText( + 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y())) + + color = self.drawing_line_color + if self.out_of_pixmap(pos): + # Don't allow the user to draw outside the pixmap. + # Clip the coordinates to 0 or max, + # if they are outside the range [0, max] + size = self.pixmap.size() + clipped_x = min(max(0, pos.x()), size.width()) + clipped_y = min(max(0, pos.y()), size.height()) + pos = QPointF(clipped_x, clipped_y) + elif len(self.current) > 1 and self.close_enough(pos, self.current[0]): + # Attract line to starting point and colorise to alert the + # user: + pos = self.current[0] + color = self.current.line_color + self.override_cursor(CURSOR_POINT) + self.current.highlight_vertex(0, Shape.NEAR_VERTEX) + + if self.draw_square: + init_pos = self.current[0] + min_x = init_pos.x() + min_y = init_pos.y() + min_size = min(abs(pos.x() - min_x), abs(pos.y() - min_y)) + direction_x = -1 if pos.x() - min_x < 0 else 1 + direction_y = -1 if pos.y() - min_y < 0 else 1 + self.line[1] = QPointF(min_x + direction_x * min_size, min_y + direction_y * min_size) + else: + self.line[1] = pos + + self.line.line_color = color + self.prev_point = QPointF() + self.current.highlight_clear() + else: + self.prev_point = pos + self.repaint() + return + + # Polygon copy moving. + if Qt.RightButton & ev.buttons(): + if self.selected_shape_copy and self.prev_point: + self.override_cursor(CURSOR_MOVE) + self.bounded_move_shape(self.selected_shape_copy, pos) + self.repaint() + elif self.selected_shape: + self.selected_shape_copy = self.selected_shape.copy() + self.repaint() + return + + # Polygon/Vertex moving. + if Qt.LeftButton & ev.buttons(): + if self.selected_vertex(): + self.bounded_move_vertex(pos) + self.shapeMoved.emit() + self.repaint() + + # Display annotation width and height while moving vertex + point1 = self.h_shape[1] + point3 = self.h_shape[3] + current_width = abs(point1.x() - point3.x()) + current_height = abs(point1.y() - point3.y()) + self.parent().window().label_coordinates.setText( + 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y())) + elif self.selected_shape and self.prev_point: + self.override_cursor(CURSOR_MOVE) + self.bounded_move_shape(self.selected_shape, pos) + self.shapeMoved.emit() + self.repaint() + + # Display annotation width and height while moving shape + point1 = self.selected_shape[1] + point3 = self.selected_shape[3] + current_width = abs(point1.x() - point3.x()) + current_height = abs(point1.y() - point3.y()) + self.parent().window().label_coordinates.setText( + 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y())) + else: + # pan + delta = ev.pos() - self.pan_initial_pos + self.scrollRequest.emit(delta.x(), Qt.Horizontal) + self.scrollRequest.emit(delta.y(), Qt.Vertical) + self.update() + return + + # Just hovering over the canvas, 2 possibilities: + # - Highlight shapes + # - Highlight vertex + # Update shape/vertex fill and tooltip value accordingly. + self.setToolTip("Image") + priority_list = self.shapes + ([self.selected_shape] if self.selected_shape else []) + for shape in reversed([s for s in priority_list if self.isVisible(s)]): + # Look for a nearby vertex to highlight. If that fails, + # check if we happen to be inside a shape. + index = shape.nearest_vertex(pos, self.epsilon) + if index is not None: + if self.selected_vertex(): + self.h_shape.highlight_clear() + self.h_vertex, self.h_shape = index, shape + shape.highlight_vertex(index, shape.MOVE_VERTEX) + self.override_cursor(CURSOR_POINT) + self.setToolTip("Click & drag to move point") + self.setStatusTip(self.toolTip()) + self.update() + break + elif shape.contains_point(pos): + if self.selected_vertex(): + self.h_shape.highlight_clear() + self.h_vertex, self.h_shape = None, shape + self.setToolTip( + "Click & drag to move shape '%s'" % shape.label) + self.setStatusTip(self.toolTip()) + self.override_cursor(CURSOR_GRAB) + self.update() + + # Display annotation width and height while hovering inside + point1 = self.h_shape[1] + point3 = self.h_shape[3] + current_width = abs(point1.x() - point3.x()) + current_height = abs(point1.y() - point3.y()) + self.parent().window().label_coordinates.setText( + 'Width: %d, Height: %d / X: %d; Y: %d' % (current_width, current_height, pos.x(), pos.y())) + break + else: # Nothing found, clear highlights, reset state. + if self.h_shape: + self.h_shape.highlight_clear() + self.update() + self.h_vertex, self.h_shape = None, None + self.override_cursor(CURSOR_DEFAULT) + + def mousePressEvent(self, ev): + pos = self.transform_pos(ev.pos()) + + if ev.button() == Qt.LeftButton: + if self.drawing(): + self.handle_drawing(pos) + else: + selection = self.select_shape_point(pos) + self.prev_point = pos + + if selection is None: + # pan + QApplication.setOverrideCursor(QCursor(Qt.OpenHandCursor)) + self.pan_initial_pos = ev.pos() + + elif ev.button() == Qt.RightButton and self.editing(): + self.select_shape_point(pos) + self.prev_point = pos + self.update() + + def mouseReleaseEvent(self, ev): + if ev.button() == Qt.RightButton: + menu = self.menus[bool(self.selected_shape_copy)] + self.restore_cursor() + if not menu.exec_(self.mapToGlobal(ev.pos()))\ + and self.selected_shape_copy: + # Cancel the move by deleting the shadow copy. + self.selected_shape_copy = None + self.repaint() + elif ev.button() == Qt.LeftButton and self.selected_shape: + if self.selected_vertex(): + self.override_cursor(CURSOR_POINT) + else: + self.override_cursor(CURSOR_GRAB) + elif ev.button() == Qt.LeftButton: + pos = self.transform_pos(ev.pos()) + if self.drawing(): + self.handle_drawing(pos) + else: + # pan + QApplication.restoreOverrideCursor() + + def end_move(self, copy=False): + assert self.selected_shape and self.selected_shape_copy + shape = self.selected_shape_copy + # del shape.fill_color + # del shape.line_color + if copy: + self.shapes.append(shape) + self.selected_shape.selected = False + self.selected_shape = shape + self.repaint() + else: + self.selected_shape.points = [p for p in shape.points] + self.selected_shape_copy = None + + def hide_background_shapes(self, value): + self.hide_background = value + if self.selected_shape: + # Only hide other shapes if there is a current selection. + # Otherwise the user will not be able to select a shape. + self.set_hiding(True) + self.repaint() + + def handle_drawing(self, pos): + if self.current and self.current.reach_max_points() is False: + init_pos = self.current[0] + min_x = init_pos.x() + min_y = init_pos.y() + target_pos = self.line[1] + max_x = target_pos.x() + max_y = target_pos.y() + self.current.add_point(QPointF(max_x, min_y)) + self.current.add_point(target_pos) + self.current.add_point(QPointF(min_x, max_y)) + self.finalise() + elif not self.out_of_pixmap(pos): + self.current = Shape() + self.current.add_point(pos) + self.line.points = [pos, pos] + self.set_hiding() + self.drawingPolygon.emit(True) + self.update() + + def set_hiding(self, enable=True): + self._hide_background = self.hide_background if enable else False + + def can_close_shape(self): + return self.drawing() and self.current and len(self.current) > 2 + + def mouseDoubleClickEvent(self, ev): + # We need at least 4 points here, since the mousePress handler + # adds an extra one before this handler is called. + if self.can_close_shape() and len(self.current) > 3: + self.current.pop_point() + self.finalise() + + def select_shape(self, shape): + self.de_select_shape() + shape.selected = True + self.selected_shape = shape + self.set_hiding() + self.selectionChanged.emit(True) + self.update() + + def select_shape_point(self, point): + """Select the first shape created which contains this point.""" + self.de_select_shape() + if self.selected_vertex(): # A vertex is marked for selection. + index, shape = self.h_vertex, self.h_shape + shape.highlight_vertex(index, shape.MOVE_VERTEX) + self.select_shape(shape) + return self.h_vertex + for shape in reversed(self.shapes): + if self.isVisible(shape) and shape.contains_point(point): + self.select_shape(shape) + self.calculate_offsets(shape, point) + return self.selected_shape + return None + + def calculate_offsets(self, shape, point): + rect = shape.bounding_rect() + x1 = rect.x() - point.x() + y1 = rect.y() - point.y() + x2 = (rect.x() + rect.width()) - point.x() + y2 = (rect.y() + rect.height()) - point.y() + self.offsets = QPointF(x1, y1), QPointF(x2, y2) + + def snap_point_to_canvas(self, x, y): + """ + Moves a point x,y to within the boundaries of the canvas. + :return: (x,y,snapped) where snapped is True if x or y were changed, False if not. + """ + if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height(): + x = max(x, 0) + y = max(y, 0) + x = min(x, self.pixmap.width()) + y = min(y, self.pixmap.height()) + return x, y, True + + return x, y, False + + def bounded_move_vertex(self, pos): + index, shape = self.h_vertex, self.h_shape + point = shape[index] + if self.out_of_pixmap(pos): + size = self.pixmap.size() + clipped_x = min(max(0, pos.x()), size.width()) + clipped_y = min(max(0, pos.y()), size.height()) + pos = QPointF(clipped_x, clipped_y) + + if self.draw_square: + opposite_point_index = (index + 2) % 4 + opposite_point = shape[opposite_point_index] + + min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y())) + direction_x = -1 if pos.x() - opposite_point.x() < 0 else 1 + direction_y = -1 if pos.y() - opposite_point.y() < 0 else 1 + shift_pos = QPointF(opposite_point.x() + direction_x * min_size - point.x(), + opposite_point.y() + direction_y * min_size - point.y()) + else: + shift_pos = pos - point + + shape.move_vertex_by(index, shift_pos) + + left_index = (index + 1) % 4 + right_index = (index + 3) % 4 + left_shift = None + right_shift = None + if index % 2 == 0: + right_shift = QPointF(shift_pos.x(), 0) + left_shift = QPointF(0, shift_pos.y()) + else: + left_shift = QPointF(shift_pos.x(), 0) + right_shift = QPointF(0, shift_pos.y()) + shape.move_vertex_by(right_index, right_shift) + shape.move_vertex_by(left_index, left_shift) + + def bounded_move_shape(self, shape, pos): + if self.out_of_pixmap(pos): + return False # No need to move + o1 = pos + self.offsets[0] + if self.out_of_pixmap(o1): + pos -= QPointF(min(0, o1.x()), min(0, o1.y())) + o2 = pos + self.offsets[1] + if self.out_of_pixmap(o2): + pos += QPointF(min(0, self.pixmap.width() - o2.x()), + min(0, self.pixmap.height() - o2.y())) + # The next line tracks the new position of the cursor + # relative to the shape, but also results in making it + # a bit "shaky" when nearing the border and allows it to + # go outside of the shape's area for some reason. XXX + # self.calculateOffsets(self.selectedShape, pos) + dp = pos - self.prev_point + if dp: + shape.move_by(dp) + self.prev_point = pos + return True + return False + + def de_select_shape(self): + if self.selected_shape: + self.selected_shape.selected = False + self.selected_shape = None + self.set_hiding(False) + self.selectionChanged.emit(False) + self.update() + + def delete_selected(self): + if self.selected_shape: + shape = self.selected_shape + self.un_highlight(shape) + self.shapes.remove(self.selected_shape) + self.selected_shape = None + self.update() + return shape + + def copy_selected_shape(self): + if self.selected_shape: + shape = self.selected_shape.copy() + self.de_select_shape() + self.shapes.append(shape) + shape.selected = True + self.selected_shape = shape + self.bounded_shift_shape(shape) + return shape + + def bounded_shift_shape(self, shape): + # Try to move in one direction, and if it fails in another. + # Give up if both fail. + point = shape[0] + offset = QPointF(2.0, 2.0) + self.calculate_offsets(shape, point) + self.prev_point = point + if not self.bounded_move_shape(shape, point - offset): + self.bounded_move_shape(shape, point + offset) + + def paintEvent(self, event): + if not self.pixmap: + return super(Canvas, self).paintEvent(event) + + p = self._painter + p.begin(self) + p.setRenderHint(QPainter.Antialiasing) + p.setRenderHint(QPainter.HighQualityAntialiasing) + p.setRenderHint(QPainter.SmoothPixmapTransform) + + p.scale(self.scale, self.scale) + p.translate(self.offset_to_center()) + + temp = self.pixmap + if self.overlay_color: + temp = QPixmap(self.pixmap) + painter = QPainter(temp) + painter.setCompositionMode(painter.CompositionMode_Overlay) + painter.fillRect(temp.rect(), self.overlay_color) + painter.end() + + p.drawPixmap(0, 0, temp) + Shape.scale = self.scale + Shape.label_font_size = self.label_font_size + for shape in self.shapes: + if (shape.selected or not self._hide_background) and self.isVisible(shape): + shape.fill = shape.selected or shape == self.h_shape + shape.paint(p) + if self.current: + self.current.paint(p) + self.line.paint(p) + if self.selected_shape_copy: + self.selected_shape_copy.paint(p) + + # Paint rect + if self.current is not None and len(self.line) == 2: + left_top = self.line[0] + right_bottom = self.line[1] + rect_width = right_bottom.x() - left_top.x() + rect_height = right_bottom.y() - left_top.y() + p.setPen(self.drawing_rect_color) + brush = QBrush(Qt.BDiagPattern) + p.setBrush(brush) + p.drawRect(int(left_top.x()), int(left_top.y()), int(rect_width), int(rect_height)) + + if self.drawing() and not self.prev_point.isNull() and not self.out_of_pixmap(self.prev_point): + p.setPen(QColor(0, 0, 0)) + p.drawLine(int(self.prev_point.x()), 0, int(self.prev_point.x()), int(self.pixmap.height())) + p.drawLine(0, int(self.prev_point.y()), int(self.pixmap.width()), int(self.prev_point.y())) + + self.setAutoFillBackground(True) + if self.verified: + pal = self.palette() + pal.setColor(self.backgroundRole(), QColor(184, 239, 38, 128)) + self.setPalette(pal) + else: + pal = self.palette() + pal.setColor(self.backgroundRole(), QColor(232, 232, 232, 255)) + self.setPalette(pal) + + p.end() + + def transform_pos(self, point): + """Convert from widget-logical coordinates to painter-logical coordinates.""" + return point / self.scale - self.offset_to_center() + + def offset_to_center(self): + s = self.scale + area = super(Canvas, self).size() + w, h = self.pixmap.width() * s, self.pixmap.height() * s + aw, ah = area.width(), area.height() + x = (aw - w) / (2 * s) if aw > w else 0 + y = (ah - h) / (2 * s) if ah > h else 0 + return QPointF(x, y) + + def out_of_pixmap(self, p): + w, h = self.pixmap.width(), self.pixmap.height() + return not (0 <= p.x() <= w and 0 <= p.y() <= h) + + def finalise(self): + assert self.current + if self.current.points[0] == self.current.points[-1]: + self.current = None + self.drawingPolygon.emit(False) + self.update() + return + + self.current.close() + self.shapes.append(self.current) + self.current = None + self.set_hiding(False) + self.newShape.emit() + self.update() + + def close_enough(self, p1, p2): + # d = distance(p1 - p2) + # m = (p1-p2).manhattanLength() + # print "d %.2f, m %d, %.2f" % (d, m, d - m) + return distance(p1 - p2) < self.epsilon + + # These two, along with a call to adjustSize are required for the + # scroll area. + def sizeHint(self): + return self.minimumSizeHint() + + def minimumSizeHint(self): + if self.pixmap: + return self.scale * self.pixmap.size() + return super(Canvas, self).minimumSizeHint() + + def wheelEvent(self, ev): + qt_version = 4 if hasattr(ev, "delta") else 5 + if qt_version == 4: + if ev.orientation() == Qt.Vertical: + v_delta = ev.delta() + h_delta = 0 + else: + h_delta = ev.delta() + v_delta = 0 + else: + delta = ev.angleDelta() + h_delta = delta.x() + v_delta = delta.y() + + mods = ev.modifiers() + if int(Qt.ControlModifier) | int(Qt.ShiftModifier) == int(mods) and v_delta: + self.lightRequest.emit(v_delta) + elif Qt.ControlModifier == int(mods) and v_delta: + self.zoomRequest.emit(v_delta) + else: + v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical) + h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal) + ev.accept() + + def keyPressEvent(self, ev): + key = ev.key() + if key == Qt.Key_Escape and self.current: + print('ESC press') + self.current = None + self.drawingPolygon.emit(False) + self.update() + elif key == Qt.Key_Return and self.can_close_shape(): + self.finalise() + elif key == Qt.Key_Left and self.selected_shape: + self.move_one_pixel('Left') + elif key == Qt.Key_Right and self.selected_shape: + self.move_one_pixel('Right') + elif key == Qt.Key_Up and self.selected_shape: + self.move_one_pixel('Up') + elif key == Qt.Key_Down and self.selected_shape: + self.move_one_pixel('Down') + + def move_one_pixel(self, direction): + # print(self.selectedShape.points) + if direction == 'Left' and not self.move_out_of_bound(QPointF(-1.0, 0)): + # print("move Left one pixel") + self.selected_shape.points[0] += QPointF(-1.0, 0) + self.selected_shape.points[1] += QPointF(-1.0, 0) + self.selected_shape.points[2] += QPointF(-1.0, 0) + self.selected_shape.points[3] += QPointF(-1.0, 0) + elif direction == 'Right' and not self.move_out_of_bound(QPointF(1.0, 0)): + # print("move Right one pixel") + self.selected_shape.points[0] += QPointF(1.0, 0) + self.selected_shape.points[1] += QPointF(1.0, 0) + self.selected_shape.points[2] += QPointF(1.0, 0) + self.selected_shape.points[3] += QPointF(1.0, 0) + elif direction == 'Up' and not self.move_out_of_bound(QPointF(0, -1.0)): + # print("move Up one pixel") + self.selected_shape.points[0] += QPointF(0, -1.0) + self.selected_shape.points[1] += QPointF(0, -1.0) + self.selected_shape.points[2] += QPointF(0, -1.0) + self.selected_shape.points[3] += QPointF(0, -1.0) + elif direction == 'Down' and not self.move_out_of_bound(QPointF(0, 1.0)): + # print("move Down one pixel") + self.selected_shape.points[0] += QPointF(0, 1.0) + self.selected_shape.points[1] += QPointF(0, 1.0) + self.selected_shape.points[2] += QPointF(0, 1.0) + self.selected_shape.points[3] += QPointF(0, 1.0) + self.shapeMoved.emit() + self.repaint() + + def move_out_of_bound(self, step): + points = [p1 + p2 for p1, p2 in zip(self.selected_shape.points, [step] * 4)] + return True in map(self.out_of_pixmap, points) + + def set_last_label(self, text, line_color=None, fill_color=None): + assert text + self.shapes[-1].label = text + if line_color: + self.shapes[-1].line_color = line_color + + if fill_color: + self.shapes[-1].fill_color = fill_color + + return self.shapes[-1] + + def undo_last_line(self): + assert self.shapes + self.current = self.shapes.pop() + self.current.set_open() + self.line.points = [self.current[-1], self.current[0]] + self.drawingPolygon.emit(True) + + def reset_all_lines(self): + assert self.shapes + self.current = self.shapes.pop() + self.current.set_open() + self.line.points = [self.current[-1], self.current[0]] + self.drawingPolygon.emit(True) + self.current = None + self.drawingPolygon.emit(False) + self.update() + + def load_pixmap(self, pixmap): + self.pixmap = pixmap + self.shapes = [] + self.repaint() + + def load_shapes(self, shapes): + self.shapes = list(shapes) + self.current = None + self.repaint() + + def set_shape_visible(self, shape, value): + self.visible[shape] = value + self.repaint() + + def current_cursor(self): + cursor = QApplication.overrideCursor() + if cursor is not None: + cursor = cursor.shape() + return cursor + + def override_cursor(self, cursor): + self._cursor = cursor + if self.current_cursor() is None: + QApplication.setOverrideCursor(cursor) + else: + QApplication.changeOverrideCursor(cursor) + + def restore_cursor(self): + QApplication.restoreOverrideCursor() + + def reset_state(self): + self.de_select_shape() + self.un_highlight() + self.selected_shape_copy = None + + self.restore_cursor() + self.pixmap = None + self.update() + + def set_drawing_shape_to_square(self, status): + self.draw_square = status diff --git a/labelImg/libs/colorDialog.py b/labelImg/libs/colorDialog.py new file mode 100644 index 00000000..5cd28025 --- /dev/null +++ b/labelImg/libs/colorDialog.py @@ -0,0 +1,37 @@ +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import QColorDialog, QDialogButtonBox +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +BB = QDialogButtonBox + + +class ColorDialog(QColorDialog): + + def __init__(self, parent=None): + super(ColorDialog, self).__init__(parent) + self.setOption(QColorDialog.ShowAlphaChannel) + # The Mac native dialog does not support our restore button. + self.setOption(QColorDialog.DontUseNativeDialog) + # Add a restore defaults button. + # The default is set at invocation time, so that it + # works across dialogs for different elements. + self.default = None + self.bb = self.layout().itemAt(1).widget() + self.bb.addButton(BB.RestoreDefaults) + self.bb.clicked.connect(self.check_restore) + + def getColor(self, value=None, title=None, default=None): + self.default = default + if title: + self.setWindowTitle(title) + if value: + self.setCurrentColor(value) + return self.currentColor() if self.exec_() else None + + def check_restore(self, button): + if self.bb.buttonRole(button) & BB.ResetRole and self.default: + self.setCurrentColor(self.default) diff --git a/labelImg/libs/combobox.py b/labelImg/libs/combobox.py new file mode 100644 index 00000000..8743b648 --- /dev/null +++ b/labelImg/libs/combobox.py @@ -0,0 +1,33 @@ +import sys +try: + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QComboBox +except ImportError: + # needed for py3+qt4 + # Ref: + # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html + # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string + if sys.version_info.major >= 3: + import sip + sip.setapi('QVariant', 2) + from PyQt4.QtGui import QWidget, QHBoxLayout, QComboBox + + +class ComboBox(QWidget): + def __init__(self, parent=None, items=[]): + super(ComboBox, self).__init__(parent) + + layout = QHBoxLayout() + self.cb = QComboBox() + self.items = items + self.cb.addItems(self.items) + + self.cb.currentIndexChanged.connect(parent.combo_selection_changed) + + layout.addWidget(self.cb) + self.setLayout(layout) + + def update_items(self, items): + self.items = items + + self.cb.clear() + self.cb.addItems(self.items) diff --git a/labelImg/libs/constants.py b/labelImg/libs/constants.py new file mode 100644 index 00000000..1efda037 --- /dev/null +++ b/labelImg/libs/constants.py @@ -0,0 +1,20 @@ +SETTING_FILENAME = 'filename' +SETTING_RECENT_FILES = 'recentFiles' +SETTING_WIN_SIZE = 'window/size' +SETTING_WIN_POSE = 'window/position' +SETTING_WIN_GEOMETRY = 'window/geometry' +SETTING_LINE_COLOR = 'line/color' +SETTING_FILL_COLOR = 'fill/color' +SETTING_ADVANCE_MODE = 'advanced' +SETTING_WIN_STATE = 'window/state' +SETTING_SAVE_DIR = 'savedir' +SETTING_PAINT_LABEL = 'paintlabel' +SETTING_LAST_OPEN_DIR = 'lastOpenDir' +SETTING_AUTO_SAVE = 'autosave' +SETTING_SINGLE_CLASS = 'singleclass' +FORMAT_PASCALVOC='PascalVOC' +FORMAT_YOLO='YOLO' +FORMAT_CREATEML='CreateML' +SETTING_DRAW_SQUARE = 'draw/square' +SETTING_LABEL_FILE_FORMAT= 'labelFileFormat' +DEFAULT_ENCODING = 'utf-8' diff --git a/labelImg/libs/create_ml_io.py b/labelImg/libs/create_ml_io.py new file mode 100644 index 00000000..3aca8d67 --- /dev/null +++ b/labelImg/libs/create_ml_io.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- +import json +from pathlib import Path + +from libs.constants import DEFAULT_ENCODING +import os + +JSON_EXT = '.json' +ENCODE_METHOD = DEFAULT_ENCODING + + +class CreateMLWriter: + def __init__(self, folder_name, filename, img_size, shapes, output_file, database_src='Unknown', local_img_path=None): + self.folder_name = folder_name + self.filename = filename + self.database_src = database_src + self.img_size = img_size + self.box_list = [] + self.local_img_path = local_img_path + self.verified = False + self.shapes = shapes + self.output_file = output_file + + def write(self): + if os.path.isfile(self.output_file): + with open(self.output_file, "r") as file: + input_data = file.read() + output_dict = json.loads(input_data) + else: + output_dict = [] + + output_image_dict = { + "image": self.filename, + "verified": self.verified, + "annotations": [] + } + + for shape in self.shapes: + points = shape["points"] + + x1 = points[0][0] + y1 = points[0][1] + x2 = points[1][0] + y2 = points[2][1] + + height, width, x, y = self.calculate_coordinates(x1, x2, y1, y2) + + shape_dict = { + "label": shape["label"], + "coordinates": { + "x": x, + "y": y, + "width": width, + "height": height + } + } + output_image_dict["annotations"].append(shape_dict) + + # check if image already in output + exists = False + for i in range(0, len(output_dict)): + if output_dict[i]["image"] == output_image_dict["image"]: + exists = True + output_dict[i] = output_image_dict + break + + if not exists: + output_dict.append(output_image_dict) + + Path(self.output_file).write_text(json.dumps(output_dict), ENCODE_METHOD) + + def calculate_coordinates(self, x1, x2, y1, y2): + if x1 < x2: + x_min = x1 + x_max = x2 + else: + x_min = x2 + x_max = x1 + if y1 < y2: + y_min = y1 + y_max = y2 + else: + y_min = y2 + y_max = y1 + width = x_max - x_min + if width < 0: + width = width * -1 + height = y_max - y_min + # x and y from center of rect + x = x_min + width / 2 + y = y_min + height / 2 + return height, width, x, y + + +class CreateMLReader: + def __init__(self, json_path, file_path): + self.json_path = json_path + self.shapes = [] + self.verified = False + self.filename = os.path.basename(file_path) + try: + self.parse_json() + except ValueError: + print("JSON decoding failed") + + def parse_json(self): + with open(self.json_path, "r") as file: + input_data = file.read() + + # Returns a list + output_list = json.loads(input_data) + + if output_list: + self.verified = output_list[0].get("verified", False) + + if len(self.shapes) > 0: + self.shapes = [] + for image in output_list: + if image["image"] == self.filename: + for shape in image["annotations"]: + self.add_shape(shape["label"], shape["coordinates"]) + + def add_shape(self, label, bnd_box): + x_min = bnd_box["x"] - (bnd_box["width"] / 2) + y_min = bnd_box["y"] - (bnd_box["height"] / 2) + + x_max = bnd_box["x"] + (bnd_box["width"] / 2) + y_max = bnd_box["y"] + (bnd_box["height"] / 2) + + points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)] + self.shapes.append((label, points, None, None, True)) + + def get_shapes(self): + return self.shapes diff --git a/labelImg/libs/default_label_combobox.py b/labelImg/libs/default_label_combobox.py new file mode 100644 index 00000000..f86e62dc --- /dev/null +++ b/labelImg/libs/default_label_combobox.py @@ -0,0 +1,27 @@ +import sys +try: + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QComboBox +except ImportError: + # needed for py3+qt4 + # Ref: + # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html + # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string + if sys.version_info.major >= 3: + import sip + sip.setapi('QVariant', 2) + from PyQt4.QtGui import QWidget, QHBoxLayout, QComboBox + + +class DefaultLabelComboBox(QWidget): + def __init__(self, parent=None, items=[]): + super(DefaultLabelComboBox, self).__init__(parent) + + layout = QHBoxLayout() + self.cb = QComboBox() + self.items = items + self.cb.addItems(self.items) + + self.cb.currentIndexChanged.connect(parent.default_label_combo_selection_changed) + + layout.addWidget(self.cb) + self.setLayout(layout) diff --git a/labelImg/libs/hashableQListWidgetItem.py b/labelImg/libs/hashableQListWidgetItem.py new file mode 100644 index 00000000..ac7818a9 --- /dev/null +++ b/labelImg/libs/hashableQListWidgetItem.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + # needed for py3+qt4 + # Ref: + # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html + # http://stackoverflow.com/questions/21217399/pyqt4-qtcore-qvariant-object-instead-of-a-string + if sys.version_info.major >= 3: + import sip + sip.setapi('QVariant', 2) + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +# PyQt5: TypeError: unhashable type: 'QListWidgetItem' + + +class HashableQListWidgetItem(QListWidgetItem): + + def __init__(self, *args): + super(HashableQListWidgetItem, self).__init__(*args) + + def __hash__(self): + return hash(id(self)) diff --git a/labelImg/libs/labelDialog.py b/labelImg/libs/labelDialog.py new file mode 100644 index 00000000..eb0d651c --- /dev/null +++ b/labelImg/libs/labelDialog.py @@ -0,0 +1,95 @@ +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +from libs.utils import new_icon, label_validator, trimmed + +BB = QDialogButtonBox + + +class LabelDialog(QDialog): + + def __init__(self, text="Enter object label", parent=None, list_item=None): + super(LabelDialog, self).__init__(parent) + + self.edit = QLineEdit() + self.edit.setText(text) + self.edit.setValidator(label_validator()) + self.edit.editingFinished.connect(self.post_process) + + model = QStringListModel() + model.setStringList(list_item) + completer = QCompleter() + completer.setModel(model) + self.edit.setCompleter(completer) + + self.button_box = bb = BB(BB.Ok | BB.Cancel, Qt.Horizontal, self) + bb.button(BB.Ok).setIcon(new_icon('done')) + bb.button(BB.Cancel).setIcon(new_icon('undo')) + bb.accepted.connect(self.validate) + bb.rejected.connect(self.reject) + + layout = QVBoxLayout() + layout.addWidget(bb, alignment=Qt.AlignmentFlag.AlignLeft) + layout.addWidget(self.edit) + + if list_item is not None and len(list_item) > 0: + self.list_widget = QListWidget(self) + for item in list_item: + self.list_widget.addItem(item) + self.list_widget.itemClicked.connect(self.list_item_click) + self.list_widget.itemDoubleClicked.connect(self.list_item_double_click) + layout.addWidget(self.list_widget) + + self.setLayout(layout) + + def validate(self): + if trimmed(self.edit.text()): + self.accept() + + def post_process(self): + self.edit.setText(trimmed(self.edit.text())) + + def pop_up(self, text='', move=True): + """ + Shows the dialog, setting the current text to `text`, and blocks the caller until the user has made a choice. + If the user entered a label, that label is returned, otherwise (i.e. if the user cancelled the action) + `None` is returned. + """ + self.edit.setText(text) + self.edit.setSelection(0, len(text)) + self.edit.setFocus(Qt.PopupFocusReason) + if move: + cursor_pos = QCursor.pos() + + # move OK button below cursor + btn = self.button_box.buttons()[0] + self.adjustSize() + btn.adjustSize() + offset = btn.mapToGlobal(btn.pos()) - self.pos() + offset += QPoint(btn.size().width() // 4, btn.size().height() // 2) + cursor_pos.setX(max(0, cursor_pos.x() - offset.x())) + cursor_pos.setY(max(0, cursor_pos.y() - offset.y())) + + parent_bottom_right = self.parentWidget().geometry() + max_x = parent_bottom_right.x() + parent_bottom_right.width() - self.sizeHint().width() + max_y = parent_bottom_right.y() + parent_bottom_right.height() - self.sizeHint().height() + max_global = self.parentWidget().mapToGlobal(QPoint(max_x, max_y)) + if cursor_pos.x() > max_global.x(): + cursor_pos.setX(max_global.x()) + if cursor_pos.y() > max_global.y(): + cursor_pos.setY(max_global.y()) + self.move(cursor_pos) + return trimmed(self.edit.text()) if self.exec_() else None + + def list_item_click(self, t_qlist_widget_item): + text = trimmed(t_qlist_widget_item.text()) + self.edit.setText(text) + + def list_item_double_click(self, t_qlist_widget_item): + self.list_item_click(t_qlist_widget_item) + self.validate() diff --git a/labelImg/libs/labelFile.py b/labelImg/libs/labelFile.py new file mode 100644 index 00000000..185570bc --- /dev/null +++ b/labelImg/libs/labelFile.py @@ -0,0 +1,174 @@ +# Copyright (c) 2016 Tzutalin +# Create by TzuTaLin + +try: + from PyQt5.QtGui import QImage +except ImportError: + from PyQt4.QtGui import QImage + +import os.path +from enum import Enum + +from libs.create_ml_io import CreateMLWriter +from libs.pascal_voc_io import PascalVocWriter +from libs.pascal_voc_io import XML_EXT +from libs.yolo_io import YOLOWriter + + +class LabelFileFormat(Enum): + PASCAL_VOC = 1 + YOLO = 2 + CREATE_ML = 3 + + +class LabelFileError(Exception): + pass + + +class LabelFile(object): + # It might be changed as window creates. By default, using XML ext + # suffix = '.lif' + suffix = XML_EXT + + def __init__(self, filename=None): + self.shapes = () + self.image_path = None + self.image_data = None + self.verified = False + + def save_create_ml_format(self, filename, shapes, image_path, image_data, class_list, line_color=None, fill_color=None, database_src=None): + img_folder_name = os.path.basename(os.path.dirname(image_path)) + img_file_name = os.path.basename(image_path) + + image = QImage() + image.load(image_path) + image_shape = [image.height(), image.width(), + 1 if image.isGrayscale() else 3] + writer = CreateMLWriter(img_folder_name, img_file_name, + image_shape, shapes, filename, local_img_path=image_path) + writer.verified = self.verified + writer.write() + return + + + def save_pascal_voc_format(self, filename, shapes, image_path, image_data, + line_color=None, fill_color=None, database_src=None): + img_folder_path = os.path.dirname(image_path) + img_folder_name = os.path.split(img_folder_path)[-1] + img_file_name = os.path.basename(image_path) + # imgFileNameWithoutExt = os.path.splitext(img_file_name)[0] + # Read from file path because self.imageData might be empty if saving to + # Pascal format + if isinstance(image_data, QImage): + image = image_data + else: + image = QImage() + image.load(image_path) + image_shape = [image.height(), image.width(), + 1 if image.isGrayscale() else 3] + writer = PascalVocWriter(img_folder_name, img_file_name, + image_shape, local_img_path=image_path) + writer.verified = self.verified + + for shape in shapes: + points = shape['points'] + label = shape['label'] + # Add Chris + difficult = int(shape['difficult']) + bnd_box = LabelFile.convert_points_to_bnd_box(points) + writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult) + + writer.save(target_file=filename) + return + + def save_yolo_format(self, filename, shapes, image_path, image_data, class_list, + line_color=None, fill_color=None, database_src=None): + img_folder_path = os.path.dirname(image_path) + img_folder_name = os.path.split(img_folder_path)[-1] + img_file_name = os.path.basename(image_path) + # imgFileNameWithoutExt = os.path.splitext(img_file_name)[0] + # Read from file path because self.imageData might be empty if saving to + # Pascal format + if isinstance(image_data, QImage): + image = image_data + else: + image = QImage() + image.load(image_path) + image_shape = [image.height(), image.width(), + 1 if image.isGrayscale() else 3] + writer = YOLOWriter(img_folder_name, img_file_name, + image_shape, local_img_path=image_path) + writer.verified = self.verified + + for shape in shapes: + points = shape['points'] + label = shape['label'] + # Add Chris + difficult = int(shape['difficult']) + bnd_box = LabelFile.convert_points_to_bnd_box(points) + writer.add_bnd_box(bnd_box[0], bnd_box[1], bnd_box[2], bnd_box[3], label, difficult) + + writer.save(target_file=filename, class_list=class_list) + return + + def toggle_verify(self): + self.verified = not self.verified + + ''' ttf is disable + def load(self, filename): + import json + with open(filename, 'rb') as f: + data = json.load(f) + imagePath = data['imagePath'] + imageData = b64decode(data['imageData']) + lineColor = data['lineColor'] + fillColor = data['fillColor'] + shapes = ((s['label'], s['points'], s['line_color'], s['fill_color'])\ + for s in data['shapes']) + # Only replace data after everything is loaded. + self.shapes = shapes + self.imagePath = imagePath + self.imageData = imageData + self.lineColor = lineColor + self.fillColor = fillColor + + def save(self, filename, shapes, imagePath, imageData, lineColor=None, fillColor=None): + import json + with open(filename, 'wb') as f: + json.dump(dict( + shapes=shapes, + lineColor=lineColor, fillColor=fillColor, + imagePath=imagePath, + imageData=b64encode(imageData)), + f, ensure_ascii=True, indent=2) + ''' + + @staticmethod + def is_label_file(filename): + file_suffix = os.path.splitext(filename)[1].lower() + return file_suffix == LabelFile.suffix + + @staticmethod + def convert_points_to_bnd_box(points): + x_min = float('inf') + y_min = float('inf') + x_max = float('-inf') + y_max = float('-inf') + for p in points: + x = p[0] + y = p[1] + x_min = min(x, x_min) + y_min = min(y, y_min) + x_max = max(x, x_max) + y_max = max(y, y_max) + + # Martin Kersner, 2015/11/12 + # 0-valued coordinates of BB caused an error while + # training faster-rcnn object detector. + if x_min < 1: + x_min = 1 + + if y_min < 1: + y_min = 1 + + return int(x_min), int(y_min), int(x_max), int(y_max) diff --git a/labelImg/libs/lightWidget.py b/labelImg/libs/lightWidget.py new file mode 100644 index 00000000..f177ed7d --- /dev/null +++ b/labelImg/libs/lightWidget.py @@ -0,0 +1,33 @@ +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + + +class LightWidget(QSpinBox): + + def __init__(self, title, value=50): + super(LightWidget, self).__init__() + self.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.setRange(0, 100) + self.setSuffix(' %') + self.setValue(value) + self.setToolTip(title) + self.setStatusTip(self.toolTip()) + self.setAlignment(Qt.AlignCenter) + + def minimumSizeHint(self): + height = super(LightWidget, self).minimumSizeHint().height() + fm = QFontMetrics(self.font()) + width = fm.width(str(self.maximum())) + return QSize(width, height) + + def color(self): + if self.value() == 50: + return None + + strength = int(self.value()/100 * 255 + 0.5) + return QColor(strength, strength, strength) diff --git a/labelImg/libs/pascal_voc_io.py b/labelImg/libs/pascal_voc_io.py new file mode 100644 index 00000000..d8f7d690 --- /dev/null +++ b/labelImg/libs/pascal_voc_io.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- +import sys +from xml.etree import ElementTree +from xml.etree.ElementTree import Element, SubElement +from lxml import etree +import codecs +from libs.constants import DEFAULT_ENCODING +from libs.ustr import ustr + + +XML_EXT = '.xml' +ENCODE_METHOD = DEFAULT_ENCODING + +class PascalVocWriter: + + def __init__(self, folder_name, filename, img_size, database_src='Unknown', local_img_path=None): + self.folder_name = folder_name + self.filename = filename + self.database_src = database_src + self.img_size = img_size + self.box_list = [] + self.local_img_path = local_img_path + self.verified = False + + def prettify(self, elem): + """ + Return a pretty-printed XML string for the Element. + """ + rough_string = ElementTree.tostring(elem, 'utf8') + root = etree.fromstring(rough_string) + return etree.tostring(root, pretty_print=True, encoding=ENCODE_METHOD).replace(" ".encode(), "\t".encode()) + # minidom does not support UTF-8 + # reparsed = minidom.parseString(rough_string) + # return reparsed.toprettyxml(indent="\t", encoding=ENCODE_METHOD) + + def gen_xml(self): + """ + Return XML root + """ + # Check conditions + if self.filename is None or \ + self.folder_name is None or \ + self.img_size is None: + return None + + top = Element('annotation') + if self.verified: + top.set('verified', 'yes') + + folder = SubElement(top, 'folder') + folder.text = self.folder_name + + filename = SubElement(top, 'filename') + filename.text = self.filename + + if self.local_img_path is not None: + local_img_path = SubElement(top, 'path') + local_img_path.text = self.local_img_path + + source = SubElement(top, 'source') + database = SubElement(source, 'database') + database.text = self.database_src + + size_part = SubElement(top, 'size') + width = SubElement(size_part, 'width') + height = SubElement(size_part, 'height') + depth = SubElement(size_part, 'depth') + width.text = str(self.img_size[1]) + height.text = str(self.img_size[0]) + if len(self.img_size) == 3: + depth.text = str(self.img_size[2]) + else: + depth.text = '1' + + segmented = SubElement(top, 'segmented') + segmented.text = '0' + return top + + def add_bnd_box(self, x_min, y_min, x_max, y_max, name, difficult): + bnd_box = {'xmin': x_min, 'ymin': y_min, 'xmax': x_max, 'ymax': y_max} + bnd_box['name'] = name + bnd_box['difficult'] = difficult + self.box_list.append(bnd_box) + + def append_objects(self, top): + for each_object in self.box_list: + object_item = SubElement(top, 'object') + name = SubElement(object_item, 'name') + name.text = ustr(each_object['name']) + pose = SubElement(object_item, 'pose') + pose.text = "Unspecified" + truncated = SubElement(object_item, 'truncated') + if int(float(each_object['ymax'])) == int(float(self.img_size[0])) or (int(float(each_object['ymin'])) == 1): + truncated.text = "1" # max == height or min + elif (int(float(each_object['xmax'])) == int(float(self.img_size[1]))) or (int(float(each_object['xmin'])) == 1): + truncated.text = "1" # max == width or min + else: + truncated.text = "0" + difficult = SubElement(object_item, 'difficult') + difficult.text = str(bool(each_object['difficult']) & 1) + bnd_box = SubElement(object_item, 'bndbox') + x_min = SubElement(bnd_box, 'xmin') + x_min.text = str(each_object['xmin']) + y_min = SubElement(bnd_box, 'ymin') + y_min.text = str(each_object['ymin']) + x_max = SubElement(bnd_box, 'xmax') + x_max.text = str(each_object['xmax']) + y_max = SubElement(bnd_box, 'ymax') + y_max.text = str(each_object['ymax']) + + def save(self, target_file=None): + root = self.gen_xml() + self.append_objects(root) + out_file = None + if target_file is None: + out_file = codecs.open( + self.filename + XML_EXT, 'w', encoding=ENCODE_METHOD) + else: + out_file = codecs.open(target_file, 'w', encoding=ENCODE_METHOD) + + prettify_result = self.prettify(root) + out_file.write(prettify_result.decode('utf8')) + out_file.close() + + +class PascalVocReader: + + def __init__(self, file_path): + # shapes type: + # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] + self.shapes = [] + self.file_path = file_path + self.verified = False + try: + self.parse_xml() + except: + pass + + def get_shapes(self): + return self.shapes + + def add_shape(self, label, bnd_box, difficult): + x_min = int(float(bnd_box.find('xmin').text)) + y_min = int(float(bnd_box.find('ymin').text)) + x_max = int(float(bnd_box.find('xmax').text)) + y_max = int(float(bnd_box.find('ymax').text)) + points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)] + self.shapes.append((label, points, None, None, difficult)) + + def parse_xml(self): + assert self.file_path.endswith(XML_EXT), "Unsupported file format" + parser = etree.XMLParser(encoding=ENCODE_METHOD) + xml_tree = ElementTree.parse(self.file_path, parser=parser).getroot() + filename = xml_tree.find('filename').text + try: + verified = xml_tree.attrib['verified'] + if verified == 'yes': + self.verified = True + except KeyError: + self.verified = False + + for object_iter in xml_tree.findall('object'): + bnd_box = object_iter.find("bndbox") + label = object_iter.find('name').text + # Add chris + difficult = False + if object_iter.find('difficult') is not None: + difficult = bool(int(object_iter.find('difficult').text)) + self.add_shape(label, bnd_box, difficult) + return True diff --git a/labelImg/libs/settings.py b/labelImg/libs/settings.py new file mode 100644 index 00000000..a6e8b868 --- /dev/null +++ b/labelImg/libs/settings.py @@ -0,0 +1,45 @@ +import os +import pickle + + +class Settings(object): + def __init__(self): + # Be default, the home will be in the same folder as labelImg + home = os.path.expanduser("~") + self.data = {} + self.path = os.path.join(home, '.labelImgSettings.pkl') + + def __setitem__(self, key, value): + self.data[key] = value + + def __getitem__(self, key): + return self.data[key] + + def get(self, key, default=None): + if key in self.data: + return self.data[key] + return default + + def save(self): + if self.path: + with open(self.path, 'wb') as f: + pickle.dump(self.data, f, pickle.HIGHEST_PROTOCOL) + return True + return False + + def load(self): + try: + if os.path.exists(self.path): + with open(self.path, 'rb') as f: + self.data = pickle.load(f) + return True + except: + print('Loading setting failed') + return False + + def reset(self): + if os.path.exists(self.path): + os.remove(self.path) + print('Remove setting pkl file ${0}'.format(self.path)) + self.data = {} + self.path = None diff --git a/labelImg/libs/shape.py b/labelImg/libs/shape.py new file mode 100644 index 00000000..65b5bac1 --- /dev/null +++ b/labelImg/libs/shape.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +from libs.utils import distance +import sys + +DEFAULT_LINE_COLOR = QColor(0, 255, 0, 128) +DEFAULT_FILL_COLOR = QColor(255, 0, 0, 128) +DEFAULT_SELECT_LINE_COLOR = QColor(255, 255, 255) +DEFAULT_SELECT_FILL_COLOR = QColor(0, 128, 255, 155) +DEFAULT_VERTEX_FILL_COLOR = QColor(0, 255, 0, 255) +DEFAULT_HVERTEX_FILL_COLOR = QColor(255, 0, 0) + + +class Shape(object): + P_SQUARE, P_ROUND = range(2) + + MOVE_VERTEX, NEAR_VERTEX = range(2) + + # The following class variables influence the drawing + # of _all_ shape objects. + line_color = DEFAULT_LINE_COLOR + fill_color = DEFAULT_FILL_COLOR + select_line_color = DEFAULT_SELECT_LINE_COLOR + select_fill_color = DEFAULT_SELECT_FILL_COLOR + vertex_fill_color = DEFAULT_VERTEX_FILL_COLOR + h_vertex_fill_color = DEFAULT_HVERTEX_FILL_COLOR + point_type = P_ROUND + point_size = 16 + scale = 1.0 + label_font_size = 8 + + def __init__(self, label=None, line_color=None, difficult=False, paint_label=False): + self.label = label + self.points = [] + self.fill = False + self.selected = False + self.difficult = difficult + self.paint_label = paint_label + + self._highlight_index = None + self._highlight_mode = self.NEAR_VERTEX + self._highlight_settings = { + self.NEAR_VERTEX: (4, self.P_ROUND), + self.MOVE_VERTEX: (1.5, self.P_SQUARE), + } + + self._closed = False + + if line_color is not None: + # Override the class line_color attribute + # with an object attribute. Currently this + # is used for drawing the pending line a different color. + self.line_color = line_color + + def close(self): + self._closed = True + + def reach_max_points(self): + if len(self.points) >= 4: + return True + return False + + def add_point(self, point): + if not self.reach_max_points(): + self.points.append(point) + + def pop_point(self): + if self.points: + return self.points.pop() + return None + + def is_closed(self): + return self._closed + + def set_open(self): + self._closed = False + + def paint(self, painter): + if self.points: + color = self.select_line_color if self.selected else self.line_color + pen = QPen(color) + # Try using integer sizes for smoother drawing(?) + pen.setWidth(max(1, int(round(2.0 / self.scale)))) + painter.setPen(pen) + + line_path = QPainterPath() + vertex_path = QPainterPath() + + line_path.moveTo(self.points[0]) + # Uncommenting the following line will draw 2 paths + # for the 1st vertex, and make it non-filled, which + # may be desirable. + # self.drawVertex(vertex_path, 0) + + for i, p in enumerate(self.points): + line_path.lineTo(p) + self.draw_vertex(vertex_path, i) + if self.is_closed(): + line_path.lineTo(self.points[0]) + + painter.drawPath(line_path) + painter.drawPath(vertex_path) + painter.fillPath(vertex_path, self.vertex_fill_color) + + # Draw text at the top-left + if self.paint_label: + min_x = sys.maxsize + min_y = sys.maxsize + min_y_label = int(1.25 * self.label_font_size) + for point in self.points: + min_x = min(min_x, point.x()) + min_y = min(min_y, point.y()) + if min_x != sys.maxsize and min_y != sys.maxsize: + font = QFont() + font.setPointSize(self.label_font_size) + font.setBold(True) + painter.setFont(font) + if self.label is None: + self.label = "" + if min_y < min_y_label: + min_y += min_y_label + painter.drawText(int(min_x), int(min_y), self.label) + + if self.fill: + color = self.select_fill_color if self.selected else self.fill_color + painter.fillPath(line_path, color) + + def draw_vertex(self, path, i): + d = self.point_size / self.scale + shape = self.point_type + point = self.points[i] + if i == self._highlight_index: + size, shape = self._highlight_settings[self._highlight_mode] + d *= size + if self._highlight_index is not None: + self.vertex_fill_color = self.h_vertex_fill_color + else: + self.vertex_fill_color = Shape.vertex_fill_color + if shape == self.P_SQUARE: + path.addRect(point.x() - d / 2, point.y() - d / 2, d, d) + elif shape == self.P_ROUND: + path.addEllipse(point, d / 2.0, d / 2.0) + else: + assert False, "unsupported vertex shape" + + def nearest_vertex(self, point, epsilon): + index = None + for i, p in enumerate(self.points): + dist = distance(p - point) + if dist <= epsilon: + index = i + epsilon = dist + return index + + def contains_point(self, point): + return self.make_path().contains(point) + + def make_path(self): + path = QPainterPath(self.points[0]) + for p in self.points[1:]: + path.lineTo(p) + return path + + def bounding_rect(self): + return self.make_path().boundingRect() + + def move_by(self, offset): + self.points = [p + offset for p in self.points] + + def move_vertex_by(self, i, offset): + self.points[i] = self.points[i] + offset + + def highlight_vertex(self, i, action): + self._highlight_index = i + self._highlight_mode = action + + def highlight_clear(self): + self._highlight_index = None + + def copy(self): + shape = Shape("%s" % self.label) + shape.points = [p for p in self.points] + shape.fill = self.fill + shape.selected = self.selected + shape._closed = self._closed + if self.line_color != Shape.line_color: + shape.line_color = self.line_color + if self.fill_color != Shape.fill_color: + shape.fill_color = self.fill_color + shape.difficult = self.difficult + return shape + + def __len__(self): + return len(self.points) + + def __getitem__(self, key): + return self.points[key] + + def __setitem__(self, key, value): + self.points[key] = value diff --git a/labelImg/libs/stringBundle.py b/labelImg/libs/stringBundle.py new file mode 100644 index 00000000..c8877d66 --- /dev/null +++ b/labelImg/libs/stringBundle.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +if items were added in files in the resources/strings folder, +then execute "pyrcc5 resources.qrc -o resources.py" in the root directory +and execute "pyrcc5 ../resources.qrc -o resources.py" in the libs directory +""" +import re +import os +import sys +import locale +from libs.ustr import ustr + +try: + from PyQt5.QtCore import * +except ImportError: + if sys.version_info.major >= 3: + import sip + sip.setapi('QVariant', 2) + from PyQt4.QtCore import * + + +class StringBundle: + + __create_key = object() + + def __init__(self, create_key, locale_str): + assert(create_key == StringBundle.__create_key), "StringBundle must be created using StringBundle.getBundle" + self.id_to_message = {} + paths = self.__create_lookup_fallback_list(locale_str) + for path in paths: + self.__load_bundle(path) + + @classmethod + def get_bundle(cls, locale_str=None): + if locale_str is None: + try: + locale_str = locale.getdefaultlocale()[0] if locale.getdefaultlocale() and len( + locale.getdefaultlocale()) > 0 else os.getenv('LANG') + except: + print('Invalid locale') + locale_str = 'en' + + return StringBundle(cls.__create_key, locale_str) + + def get_string(self, string_id): + assert(string_id in self.id_to_message), "Missing string id : " + string_id + return self.id_to_message[string_id] + + def __create_lookup_fallback_list(self, locale_str): + result_paths = [] + base_path = ":/strings" + result_paths.append(base_path) + if locale_str is not None: + # Don't follow standard BCP47. Simple fallback + tags = re.split('[^a-zA-Z]', locale_str) + for tag in tags: + last_path = result_paths[-1] + result_paths.append(last_path + '-' + tag) + + return result_paths + + def __load_bundle(self, path): + PROP_SEPERATOR = '=' + f = QFile(path) + if f.exists(): + if f.open(QIODevice.ReadOnly | QFile.Text): + text = QTextStream(f) + text.setCodec("UTF-8") + + while not text.atEnd(): + line = ustr(text.readLine()) + key_value = line.split(PROP_SEPERATOR) + key = key_value[0].strip() + value = PROP_SEPERATOR.join(key_value[1:]).strip().strip('"') + self.id_to_message[key] = value + + f.close() diff --git a/labelImg/libs/toolBar.py b/labelImg/libs/toolBar.py new file mode 100644 index 00000000..b11887ef --- /dev/null +++ b/labelImg/libs/toolBar.py @@ -0,0 +1,39 @@ +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + + +class ToolBar(QToolBar): + + def __init__(self, title): + super(ToolBar, self).__init__(title) + layout = self.layout() + m = (0, 0, 0, 0) + layout.setSpacing(0) + layout.setContentsMargins(*m) + self.setContentsMargins(*m) + self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint) + + def addAction(self, action): + if isinstance(action, QWidgetAction): + return super(ToolBar, self).addAction(action) + btn = ToolButton() + btn.setDefaultAction(action) + btn.setToolButtonStyle(self.toolButtonStyle()) + self.addWidget(btn) + + +class ToolButton(QToolButton): + """ToolBar companion class which ensures all buttons have the same size.""" + minSize = (60, 60) + + def minimumSizeHint(self): + ms = super(ToolButton, self).minimumSizeHint() + w1, h1 = ms.width(), ms.height() + w2, h2 = self.minSize + ToolButton.minSize = max(w1, w2), max(h1, h2) + return QSize(*ToolButton.minSize) diff --git a/labelImg/libs/ustr.py b/labelImg/libs/ustr.py new file mode 100644 index 00000000..bfcaff5d --- /dev/null +++ b/labelImg/libs/ustr.py @@ -0,0 +1,17 @@ +import sys +from libs.constants import DEFAULT_ENCODING + +def ustr(x): + """py2/py3 unicode helper""" + + if sys.version_info < (3, 0, 0): + from PyQt4.QtCore import QString + if type(x) == str: + return x.decode(DEFAULT_ENCODING) + if type(x) == QString: + # https://blog.csdn.net/friendan/article/details/51088476 + # https://blog.csdn.net/xxm524/article/details/74937308 + return unicode(x.toUtf8(), DEFAULT_ENCODING, 'ignore') + return x + else: + return x diff --git a/labelImg/libs/utils.py b/labelImg/libs/utils.py new file mode 100644 index 00000000..d511c151 --- /dev/null +++ b/labelImg/libs/utils.py @@ -0,0 +1,117 @@ +from math import sqrt +from libs.ustr import ustr +import hashlib +import re +import sys + +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * + QT5 = True +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + QT5 = False + + +def new_icon(icon): + return QIcon(':/' + icon) + + +def new_button(text, icon=None, slot=None): + b = QPushButton(text) + if icon is not None: + b.setIcon(new_icon(icon)) + if slot is not None: + b.clicked.connect(slot) + return b + + +def new_action(parent, text, slot=None, shortcut=None, icon=None, + tip=None, checkable=False, enabled=True): + """Create a new action and assign callbacks, shortcuts, etc.""" + a = QAction(text, parent) + if icon is not None: + a.setIcon(new_icon(icon)) + if shortcut is not None: + if isinstance(shortcut, (list, tuple)): + a.setShortcuts(shortcut) + else: + a.setShortcut(shortcut) + if tip is not None: + a.setToolTip(tip) + a.setStatusTip(tip) + if slot is not None: + a.triggered.connect(slot) + if checkable: + a.setCheckable(True) + a.setEnabled(enabled) + return a + + +def add_actions(widget, actions): + for action in actions: + if action is None: + widget.addSeparator() + elif isinstance(action, QMenu): + widget.addMenu(action) + else: + widget.addAction(action) + + +def label_validator(): + return QRegExpValidator(QRegExp(r'^[^ \t].+'), None) + + +class Struct(object): + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +def distance(p): + return sqrt(p.x() * p.x() + p.y() * p.y()) + + +def format_shortcut(text): + mod, key = text.split('+', 1) + return '%s+%s' % (mod, key) + + +def generate_color_by_text(text): + s = ustr(text) + hash_code = int(hashlib.sha256(s.encode('utf-8')).hexdigest(), 16) + r = int((hash_code / 255) % 255) + g = int((hash_code / 65025) % 255) + b = int((hash_code / 16581375) % 255) + return QColor(r, g, b, 100) + + +def have_qstring(): + """p3/qt5 get rid of QString wrapper as py3 has native unicode str type""" + return not (sys.version_info.major >= 3 or QT_VERSION_STR.startswith('5.')) + + +def util_qt_strlistclass(): + return QStringList if have_qstring() else list + + +def natural_sort(list, key=lambda s:s): + """ + Sort the list into natural alphanumeric order. + """ + def get_alphanum_key_func(key): + convert = lambda text: int(text) if text.isdigit() else text + return lambda s: [convert(c) for c in re.split('([0-9]+)', key(s))] + sort_key = get_alphanum_key_func(key) + list.sort(key=sort_key) + + +# QT4 has a trimmed method, in QT5 this is called strip +if QT5: + def trimmed(text): + return text.strip() +else: + def trimmed(text): + return text.trimmed() diff --git a/labelImg/libs/yolo_io.py b/labelImg/libs/yolo_io.py new file mode 100644 index 00000000..192e2c78 --- /dev/null +++ b/labelImg/libs/yolo_io.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- +import codecs +import os + +from libs.constants import DEFAULT_ENCODING + +TXT_EXT = '.txt' +ENCODE_METHOD = DEFAULT_ENCODING + +class YOLOWriter: + + def __init__(self, folder_name, filename, img_size, database_src='Unknown', local_img_path=None): + self.folder_name = folder_name + self.filename = filename + self.database_src = database_src + self.img_size = img_size + self.box_list = [] + self.local_img_path = local_img_path + self.verified = False + + def add_bnd_box(self, x_min, y_min, x_max, y_max, name, difficult): + bnd_box = {'xmin': x_min, 'ymin': y_min, 'xmax': x_max, 'ymax': y_max} + bnd_box['name'] = name + bnd_box['difficult'] = difficult + self.box_list.append(bnd_box) + + def bnd_box_to_yolo_line(self, box, class_list=[]): + x_min = box['xmin'] + x_max = box['xmax'] + y_min = box['ymin'] + y_max = box['ymax'] + + x_center = float((x_min + x_max)) / 2 / self.img_size[1] + y_center = float((y_min + y_max)) / 2 / self.img_size[0] + + w = float((x_max - x_min)) / self.img_size[1] + h = float((y_max - y_min)) / self.img_size[0] + + # PR387 + box_name = box['name'] + if box_name not in class_list: + class_list.append(box_name) + + class_index = class_list.index(box_name) + + return class_index, x_center, y_center, w, h + + def save(self, class_list=[], target_file=None): + + out_file = None # Update yolo .txt + out_class_file = None # Update class list .txt + + if target_file is None: + out_file = open( + self.filename + TXT_EXT, 'w', encoding=ENCODE_METHOD) + classes_file = os.path.join(os.path.dirname(os.path.abspath(self.filename)), "classes.txt") + out_class_file = open(classes_file, 'w') + + else: + out_file = codecs.open(target_file, 'w', encoding=ENCODE_METHOD) + classes_file = os.path.join(os.path.dirname(os.path.abspath(target_file)), "classes.txt") + out_class_file = open(classes_file, 'w') + + + for box in self.box_list: + class_index, x_center, y_center, w, h = self.bnd_box_to_yolo_line(box, class_list) + # print (classIndex, x_center, y_center, w, h) + out_file.write("%d %.6f %.6f %.6f %.6f\n" % (class_index, x_center, y_center, w, h)) + + # print (classList) + # print (out_class_file) + for c in class_list: + out_class_file.write(c+'\n') + + out_class_file.close() + out_file.close() + + + +class YoloReader: + + def __init__(self, file_path, image, class_list_path=None): + # shapes type: + # [labbel, [(x1,y1), (x2,y2), (x3,y3), (x4,y4)], color, color, difficult] + self.shapes = [] + self.file_path = file_path + + if class_list_path is None: + dir_path = os.path.dirname(os.path.realpath(self.file_path)) + self.class_list_path = os.path.join(dir_path, "classes.txt") + else: + self.class_list_path = class_list_path + + # print (file_path, self.class_list_path) + + classes_file = open(self.class_list_path, 'r') + self.classes = classes_file.read().strip('\n').split('\n') + + # print (self.classes) + + img_size = [image.height(), image.width(), + 1 if image.isGrayscale() else 3] + + self.img_size = img_size + + self.verified = False + # try: + self.parse_yolo_format() + # except: + # pass + + def get_shapes(self): + return self.shapes + + def add_shape(self, label, x_min, y_min, x_max, y_max, difficult): + + points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)] + self.shapes.append((label, points, None, None, difficult)) + + def yolo_line_to_shape(self, class_index, x_center, y_center, w, h): + label = self.classes[int(class_index)] + + x_min = max(float(x_center) - float(w) / 2, 0) + x_max = min(float(x_center) + float(w) / 2, 1) + y_min = max(float(y_center) - float(h) / 2, 0) + y_max = min(float(y_center) + float(h) / 2, 1) + + x_min = round(self.img_size[1] * x_min) + x_max = round(self.img_size[1] * x_max) + y_min = round(self.img_size[0] * y_min) + y_max = round(self.img_size[0] * y_max) + + return label, x_min, y_min, x_max, y_max + + def parse_yolo_format(self): + bnd_box_file = open(self.file_path, 'r') + for bndBox in bnd_box_file: + class_index, x_center, y_center, w, h = bndBox.strip().split(' ') + label, x_min, y_min, x_max, y_max = self.yolo_line_to_shape(class_index, x_center, y_center, w, h) + + # Caveat: difficult flag is discarded when saved as yolo format. + self.add_shape(label, x_min, y_min, x_max, y_max, False) diff --git a/labelImg/libs/zoomWidget.py b/labelImg/libs/zoomWidget.py new file mode 100644 index 00000000..d33de0d7 --- /dev/null +++ b/labelImg/libs/zoomWidget.py @@ -0,0 +1,26 @@ +try: + from PyQt5.QtGui import * + from PyQt5.QtCore import * + from PyQt5.QtWidgets import * +except ImportError: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + + +class ZoomWidget(QSpinBox): + + def __init__(self, value=100): + super(ZoomWidget, self).__init__() + self.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.setRange(1, 500) + self.setSuffix(' %') + self.setValue(value) + self.setToolTip(u'Zoom Level') + self.setStatusTip(self.toolTip()) + self.setAlignment(Qt.AlignCenter) + + def minimumSizeHint(self): + height = super(ZoomWidget, self).minimumSizeHint().height() + fm = QFontMetrics(self.font()) + width = fm.width(str(self.maximum())) + return QSize(width, height) diff --git a/labelImg/readme/README.jp.rst b/labelImg/readme/README.jp.rst new file mode 100644 index 00000000..00a550ec --- /dev/null +++ b/labelImg/readme/README.jp.rst @@ -0,0 +1,197 @@ +labelImg +======== + +.. image:: https://img.shields.io/pypi/v/labelimg.svg + :target: https://pypi.python.org/pypi/labelimg + +.. image:: https://img.shields.io/github/workflow/status/tzutalin/labelImg/Package?style=for-the-badge :alt: GitHub Workflow Status + + +.. image:: https://img.shields.io/badge/lang-en-blue.svg + :target: https://github.com/tzutalin/labelImg + +.. image:: https://img.shields.io/badge/lang-zh-green.svg + :target: https://github.com/tzutalin/labelImg/blob/master/readme/README.zh.rst + +.. image:: https://img.shields.io/badge/lang-jp-green.svg + :target: https://github.com/tzutalin/labelImg/blob/master/readme/README.jp.rst + +.. image:: /resources/icons/app.png + :width: 200px + :align: center + +LabelImgは、PythonとQtを使うアノテーション補助ツールです。 + +このツールはPascalVOCフォーマットとYOLOとCreateMLをサポートしています。 + +.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo3.jpg + :alt: Demo Image + +.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo.jpg + :alt: Demo Image + +`サンプル動画は にあります。`__ + +インストール方法 +------------------- + + +ソースからビルドする +~~~~~~~~~~~~~~~~~~~~ + +Linuxまたは、Ubuntuまたは、macOSの場合は + +Ubuntuの場合 +^^^^^^^^^^^^ + +Python 3とQt5を使う場合 + +.. code:: shell + + sudo apt-get install pyqt5-dev-tools + sudo pip3 install -r requirements/requirements-linux-python3.txt + make qt5py3 + python3 labelImg.py + python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +macOSの場合 +^^^^^^^^^^^ + +Python 3とQt5を使う場合 + +.. code:: shell + + brew install qt # Install qt-5.x.x by Homebrew + brew install libxml2 + + or using pip + + pip3 install pyqt5 lxml # Install qt and lxml by pip + + make qt5py3 + python3 labelImg.py + python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + + +Python 3 Virtualenv (推奨) + +VirtualenvはQtとPythonのバージョン衝突問題を解消できます。 + +.. code:: shell + + brew install python3 + pip3 install pipenv + pipenv run pip install pyqt5==5.15.2 lxml + pipenv run make qt5py3 + pipenv run python3 labelImg.py + [任意で] rm -rf build dist; python setup.py py2app -A;mv "dist/labelImg.app" /Applications + + +注意:最後のコマンドを実行すると、/ApplicationsフォルダにSVGアイコンを含む.appファイルが生成されます。build-tools/build-for-macos.shというスクリプトの仕様も検討してください。 + + +Windowsの場合 +^^^^^^^^^^^^^ + +最初に`Python `__ と +`PyQt5 `__ と +`install lxml `__ をインストールしてください。 + +コマンドプロンプトを起動し `labelImg <#labelimg>`__ がインストールされているフォルダに移動してから以下のコマンドを実行します。 + +.. code:: shell + + pyrcc4 -o libs/resources.py resources.qrc + (pyqt5の場合は、 pyrcc5 -o libs/resources.py resources.qrc) + + python labelImg.py + python labelImg.py [画像パス] [定義済みクラスファイル] + +Windows + Anaconda +^^^^^^^^^^^^^^^^^^ + +`Anaconda `__ をダウンロードしてからインストールしてください。 + +Anaconda Promptを起動し `labelImg <#labelimg>`__ インストールされているフォルダに移動してから以下のコマンドを実行します。 + +.. code:: shell + + conda install pyqt=5 + conda install -c anaconda lxml + pyrcc5 -o libs/resources.py resources.qrc + python labelImg.py + python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +PyPIから入手する(Python 3以降のみ) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +現代的なLinuxディストリビューションの場合は以下のコマンドを入力するだけでインストールできます。 + +.. code:: shell + + pip3 install labelImg + labelImg + labelImg [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +Dockerの場合 +~~~~~~~~~~~~~~~~~ +.. code:: shell + + docker run -it \ + --user $(id -u) \ + -e DISPLAY=unix$DISPLAY \ + --workdir=$(pwd) \ + --volume="/home/$USER:/home/$USER" \ + --volume="/etc/group:/etc/group:ro" \ + --volume="/etc/passwd:/etc/passwd:ro" \ + --volume="/etc/shadow:/etc/shadow:ro" \ + --volume="/etc/sudoers.d:/etc/sudoers.d:ro" \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + tzutalin/py2qt4 + + make qt4py2;./labelImg.py + +あとは`サンプル動画`__ +を見るだけです。 + + +定義済みクラスを作成するには? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`data/predefined\_classes.txt `__ +を編集してください。 + +ショートカット一覧 +~~~~~~~~~~~~~~~~~~ + ++--------------------+--------------------------------------------+ +| Ctrl + u | そのディレクトリの画像を読み込む | ++--------------------+--------------------------------------------+ +| Ctrl + r | アノテーションの生成ディレクトリを変更 | ++--------------------+--------------------------------------------+ +| Ctrl + s | 保存する | ++--------------------+--------------------------------------------+ +| Ctrl + d | 現在選択している矩形トラベルをコピー | ++--------------------+--------------------------------------------+ +| Ctrl + Shift + d | 現在表示している画像を削除 | ++--------------------+--------------------------------------------+ +| Space | 現在の画像に検証済みフラグを立てる | ++--------------------+--------------------------------------------+ +| w | 矩形を生成する | ++--------------------+--------------------------------------------+ +| d | 次の画像へ移動する | ++--------------------+--------------------------------------------+ +| a | 前の画像へ移動する | ++--------------------+--------------------------------------------+ +| del | 選択した矩形を削除 | ++--------------------+--------------------------------------------+ +| Ctrl++ | 画像を拡大 | ++--------------------+--------------------------------------------+ +| Ctrl-- | 画像を縮小 | ++--------------------+--------------------------------------------+ +| ↑→↓← | 十字キーで矩形を移動する | ++--------------------+--------------------------------------------+ + +開発に参加するには? +~~~~~~~~~~~~~~~~~~~~~ + +このリポジトリにPull Request を送ってください。 diff --git a/labelImg/readme/README.zh.rst b/labelImg/readme/README.zh.rst new file mode 100644 index 00000000..4ea22e0e --- /dev/null +++ b/labelImg/readme/README.zh.rst @@ -0,0 +1,195 @@ +LabelImg +======== + +.. image:: https://img.shields.io/pypi/v/labelimg.svg + :target: https://pypi.python.org/pypi/labelimg + +.. image:: https://img.shields.io/github/workflow/status/tzutalin/labelImg/Package?style=for-the-badge :alt: GitHub Workflow Status + +.. image:: https://img.shields.io/badge/lang-en-blue.svg + :target: https://github.com/tzutalin/labelImg + +.. image:: https://img.shields.io/badge/lang-zh-green.svg + :target: https://github.com/tzutalin/labelImg/blob/master/readme/README.zh.rst + +.. image:: https://img.shields.io/badge/lang-jp-green.svg + :target: https://github.com/tzutalin/labelImg/blob/master/readme/README.jp.rst + +.. image:: /resources/icons/app.png + :width: 200px + :align: center + +LabelImg 是影像標註工具,它是用python 和 QT 寫成的. + +支持的儲存格式包括PASCAL VOC format, YOLO, createML. + +.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo3.jpg + :alt: Demo Image + +.. image:: https://raw.githubusercontent.com/tzutalin/labelImg/master/demo/demo.jpg + :alt: Demo Image + +`展示影片 `__ + +安裝 +------------------ + + +透過編譯原始碼 +~~~~~~~~~~~~~~~~~ + +Linux/Ubuntu/Mac 需要 Python 和 `PyQt `__ + +Ubuntu Linux +^^^^^^^^^^^^ + +Python 3 + Qt5 + +.. code:: shell + + sudo apt-get install pyqt5-dev-tools + sudo pip3 install -r requirements/requirements-linux-python3.txt + make qt5py3 + python3 labelImg.py + python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +macOS +^^^^^ + +Python 3 + Qt5 + +.. code:: shell + + brew install qt # Install qt-5.x.x by Homebrew + brew install libxml2 + + or using pip + + pip3 install pyqt5 lxml # Install qt and lxml by pip + + make qt5py3 + python3 labelImg.py + python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + + +Python 3 Virtualenv (推薦方法) + +Virtualenv 可以避免版本和相依性問題 + +.. code:: shell + + brew install python3 + pip3 install pipenv + pipenv run pip install pyqt5==5.15.2 lxml + pipenv run make qt5py3 + pipenv run python3 labelImg.py + [Optional] rm -rf build dist; python setup.py py2app -A;mv "dist/labelImg.app" /Applications + + +Windows +^^^^^^^ + +安裝 `Python `__, +`PyQt5 `__ +和 `install lxml `__. + +安裝並到 `labelImg <#labelimg>`__ 目錄 + +.. code:: shell + + pyrcc4 -o libs/resources.py resources.qrc + For pyqt5, pyrcc5 -o libs/resources.py resources.qrc + + python labelImg.py + python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +Windows + Anaconda +^^^^^^^^^^^^^^^^^^ + +下載並安裝 `Anaconda `__ (Python 3+) + +打開 Anaconda Prompt 然後到 `labelImg <#labelimg>`__ 目錄 + +.. code:: shell + + conda install pyqt=5 + conda install -c anaconda lxml + pyrcc5 -o libs/resources.py resources.qrc + python labelImg.py + python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + +Get from PyPI but only python3.0 or above +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: shell + + pip3 install labelImg + labelImg + labelImg [IMAGE_PATH] [PRE-DEFINED CLASS FILE] + + +Use Docker +~~~~~~~~~~~~~~~~~ +.. code:: shell + + docker run -it \ + --user $(id -u) \ + -e DISPLAY=unix$DISPLAY \ + --workdir=$(pwd) \ + --volume="/home/$USER:/home/$USER" \ + --volume="/etc/group:/etc/group:ro" \ + --volume="/etc/passwd:/etc/passwd:ro" \ + --volume="/etc/shadow:/etc/shadow:ro" \ + --volume="/etc/sudoers.d:/etc/sudoers.d:ro" \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + tzutalin/py2qt4 + + make qt4py2;./labelImg.py + +`你可以參考影片 `__ + + +使用方法 +----- + +你可以先產生標籤 +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +修改這個檔案 +`data/predefined\_classes.txt `__ + +快捷鍵 +~~~~~~~ + ++--------------------+--------------------------------------------+ +| Ctrl + u | 讀取所有影像從每個目錄 | ++--------------------+--------------------------------------------+ +| Ctrl + r | 改變標示結果的存檔目錄 | ++--------------------+--------------------------------------------+ +| Ctrl + s | 存檔 | ++--------------------+--------------------------------------------+ +| Ctrl + d | 複製目前的標籤和物件的區塊 | ++--------------------+--------------------------------------------+ +| Ctrl + Shift + d | 刪除目前影像 | ++--------------------+--------------------------------------------+ +| Space | 標示目前照片已經處理過 | ++--------------------+--------------------------------------------+ +| w | 產生新的物件區塊 | ++--------------------+--------------------------------------------+ +| d | 下張影像 | ++--------------------+--------------------------------------------+ +| a | 上張影像 | ++--------------------+--------------------------------------------+ +| del | 刪除所選的物件區塊 | ++--------------------+--------------------------------------------+ +| Ctrl++ | 放大影像 | ++--------------------+--------------------------------------------+ +| Ctrl-- | 縮小影像 | ++--------------------+--------------------------------------------+ +| ↑→↓← | 移動所選的物件區塊 | ++--------------------+--------------------------------------------+ + +如何貢獻 +~~~~~~~~~~~~~~~~~ + +歡迎上傳程式碼直接貢獻 diff --git a/labelImg/readme/images/label-studio-1-6-player-screenshot.png b/labelImg/readme/images/label-studio-1-6-player-screenshot.png new file mode 100644 index 00000000..0d24a461 Binary files /dev/null and b/labelImg/readme/images/label-studio-1-6-player-screenshot.png differ diff --git a/labelImg/readme/images/labelimg.png b/labelImg/readme/images/labelimg.png new file mode 100644 index 00000000..15c631f0 Binary files /dev/null and b/labelImg/readme/images/labelimg.png differ diff --git a/labelImg/requirements/requirements-linux-python3.txt b/labelImg/requirements/requirements-linux-python3.txt new file mode 100644 index 00000000..d27cf7b8 --- /dev/null +++ b/labelImg/requirements/requirements-linux-python3.txt @@ -0,0 +1,2 @@ +pyqt5==5.14.1 +lxml==4.9.1 diff --git a/labelImg/resources.qrc b/labelImg/resources.qrc new file mode 100644 index 00000000..7479b9e3 --- /dev/null +++ b/labelImg/resources.qrc @@ -0,0 +1,43 @@ + + + +resources/icons/help.png +resources/icons/app.png +resources/icons/expert2.png +resources/icons/done.png +resources/icons/file.png +resources/icons/labels.png +resources/icons/objects.png +resources/icons/close.png +resources/icons/fit-width.png +resources/icons/fit-window.png +resources/icons/undo.png +resources/icons/eye.png +resources/icons/quit.png +resources/icons/copy.png +resources/icons/edit.png +resources/icons/open.png +resources/icons/save.png +resources/icons/format_voc.png +resources/icons/format_yolo.png +resources/icons/format_createml.png +resources/icons/save-as.png +resources/icons/color.png +resources/icons/color_line.png +resources/icons/zoom.png +resources/icons/zoom-in.png +resources/icons/zoom-out.png +resources/icons/light_reset.png +resources/icons/light_lighten.png +resources/icons/light_darken.png +resources/icons/cancel.png +resources/icons/next.png +resources/icons/prev.png +resources/icons/resetall.png +resources/icons/verify.png +resources/strings/strings.properties +resources/strings/strings-zh-TW.properties +resources/strings/strings-zh-CN.properties +resources/strings/strings-ja-JP.properties + + diff --git a/labelImg/resources/icons/app.icns b/labelImg/resources/icons/app.icns new file mode 100644 index 00000000..9409991b Binary files /dev/null and b/labelImg/resources/icons/app.icns differ diff --git a/labelImg/resources/icons/app.png b/labelImg/resources/icons/app.png new file mode 100644 index 00000000..f2dc33e3 Binary files /dev/null and b/labelImg/resources/icons/app.png differ diff --git a/labelImg/resources/icons/app.svg b/labelImg/resources/icons/app.svg new file mode 100644 index 00000000..b691a2e9 --- /dev/null +++ b/labelImg/resources/icons/app.svg @@ -0,0 +1,30 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + diff --git a/labelImg/resources/icons/cancel.png b/labelImg/resources/icons/cancel.png new file mode 100644 index 00000000..8fbfab89 Binary files /dev/null and b/labelImg/resources/icons/cancel.png differ diff --git a/labelImg/resources/icons/close.png b/labelImg/resources/icons/close.png new file mode 100644 index 00000000..aa52a8d6 Binary files /dev/null and b/labelImg/resources/icons/close.png differ diff --git a/labelImg/resources/icons/color.png b/labelImg/resources/icons/color.png new file mode 100644 index 00000000..1a1e1ad0 Binary files /dev/null and b/labelImg/resources/icons/color.png differ diff --git a/labelImg/resources/icons/color_line.png b/labelImg/resources/icons/color_line.png new file mode 100644 index 00000000..6ef10bfc Binary files /dev/null and b/labelImg/resources/icons/color_line.png differ diff --git a/labelImg/resources/icons/copy.png b/labelImg/resources/icons/copy.png new file mode 100644 index 00000000..a4c9bddc Binary files /dev/null and b/labelImg/resources/icons/copy.png differ diff --git a/labelImg/resources/icons/delete.png b/labelImg/resources/icons/delete.png new file mode 100644 index 00000000..a1a4074f Binary files /dev/null and b/labelImg/resources/icons/delete.png differ diff --git a/labelImg/resources/icons/done.png b/labelImg/resources/icons/done.png new file mode 100644 index 00000000..d8a03f4f Binary files /dev/null and b/labelImg/resources/icons/done.png differ diff --git a/labelImg/resources/icons/done.svg b/labelImg/resources/icons/done.svg new file mode 100644 index 00000000..aa8fd288 --- /dev/null +++ b/labelImg/resources/icons/done.svg @@ -0,0 +1,400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2003-12-22T22:34:35+02:00 + +2004-04-17T21:25:50Z + +Adobe Illustrator 10.0 + +2004-01-19T17:51:02+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX +Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY +q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq +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 + + + + + + +uuid:4b4d592f-95b8-4bcd-a892-74a536c5e52f + + + +image/svg+xml + + + +test.ai + + + + + + end='w' + + + + + + + + + + diff --git a/labelImg/resources/icons/edit.png b/labelImg/resources/icons/edit.png new file mode 100644 index 00000000..36775390 Binary files /dev/null and b/labelImg/resources/icons/edit.png differ diff --git a/labelImg/resources/icons/expert1.png b/labelImg/resources/icons/expert1.png new file mode 100644 index 00000000..28136454 Binary files /dev/null and b/labelImg/resources/icons/expert1.png differ diff --git a/labelImg/resources/icons/expert2.png b/labelImg/resources/icons/expert2.png new file mode 100644 index 00000000..7c470b6a Binary files /dev/null and b/labelImg/resources/icons/expert2.png differ diff --git a/labelImg/resources/icons/eye.png b/labelImg/resources/icons/eye.png new file mode 100644 index 00000000..c4b65505 Binary files /dev/null and b/labelImg/resources/icons/eye.png differ diff --git a/labelImg/resources/icons/feBlend-icon.png b/labelImg/resources/icons/feBlend-icon.png new file mode 100644 index 00000000..1c1aca84 Binary files /dev/null and b/labelImg/resources/icons/feBlend-icon.png differ diff --git a/labelImg/resources/icons/file.png b/labelImg/resources/icons/file.png new file mode 100644 index 00000000..1ec0515a Binary files /dev/null and b/labelImg/resources/icons/file.png differ diff --git a/labelImg/resources/icons/fit-width.png b/labelImg/resources/icons/fit-width.png new file mode 100644 index 00000000..0a549074 Binary files /dev/null and b/labelImg/resources/icons/fit-width.png differ diff --git a/labelImg/resources/icons/fit-window.png b/labelImg/resources/icons/fit-window.png new file mode 100644 index 00000000..585e9707 Binary files /dev/null and b/labelImg/resources/icons/fit-window.png differ diff --git a/labelImg/resources/icons/fit.png b/labelImg/resources/icons/fit.png new file mode 100644 index 00000000..9e0e817b Binary files /dev/null and b/labelImg/resources/icons/fit.png differ diff --git a/labelImg/resources/icons/format_createml.png b/labelImg/resources/icons/format_createml.png new file mode 100644 index 00000000..c08af942 Binary files /dev/null and b/labelImg/resources/icons/format_createml.png differ diff --git a/labelImg/resources/icons/format_voc.png b/labelImg/resources/icons/format_voc.png new file mode 100644 index 00000000..cb15e439 Binary files /dev/null and b/labelImg/resources/icons/format_voc.png differ diff --git a/labelImg/resources/icons/format_yolo.png b/labelImg/resources/icons/format_yolo.png new file mode 100644 index 00000000..ca9acc71 Binary files /dev/null and b/labelImg/resources/icons/format_yolo.png differ diff --git a/labelImg/resources/icons/help.png b/labelImg/resources/icons/help.png new file mode 100644 index 00000000..93bf094a Binary files /dev/null and b/labelImg/resources/icons/help.png differ diff --git a/labelImg/resources/icons/labels.png b/labelImg/resources/icons/labels.png new file mode 100644 index 00000000..c82ffb7b Binary files /dev/null and b/labelImg/resources/icons/labels.png differ diff --git a/labelImg/resources/icons/labels.svg b/labelImg/resources/icons/labels.svg new file mode 100644 index 00000000..652cef3f --- /dev/null +++ b/labelImg/resources/icons/labels.svg @@ -0,0 +1,819 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + + +Adobe PDF library 5.00 + + + + + +2004-01-26T11:58:28+02:00 + +2004-03-28T20:41:40Z + +Adobe Illustrator 10.0 + +2004-02-16T23:58:32+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmDzFo +3l7TJdT1e5W1tItuTbszHoiKN2Y+AxV4j5g/5ydvTcMnl/SYlgU0Se/LOzDxMcTIF/4M4qk//QzP +nv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8 +sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5F +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 +f//Z + + + + + + +uuid:4ee3f24b-6ed2-4a2e-8f7a-50b762c8da8b + + + +image/svg+xml + + + +mime.ai + + + + image/svg+xml + end='w' + + +Labels + \ No newline at end of file diff --git a/labelImg/resources/icons/light_brighten.svg b/labelImg/resources/icons/light_brighten.svg new file mode 100644 index 00000000..f257e184 --- /dev/null +++ b/labelImg/resources/icons/light_brighten.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/labelImg/resources/icons/light_darken.png b/labelImg/resources/icons/light_darken.png new file mode 100644 index 00000000..4fa5adb6 Binary files /dev/null and b/labelImg/resources/icons/light_darken.png differ diff --git a/labelImg/resources/icons/light_darken.svg b/labelImg/resources/icons/light_darken.svg new file mode 100644 index 00000000..1002ca85 --- /dev/null +++ b/labelImg/resources/icons/light_darken.svg @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/labelImg/resources/icons/light_lighten.png b/labelImg/resources/icons/light_lighten.png new file mode 100644 index 00000000..c8679b42 Binary files /dev/null and b/labelImg/resources/icons/light_lighten.png differ diff --git a/labelImg/resources/icons/light_reset.png b/labelImg/resources/icons/light_reset.png new file mode 100644 index 00000000..225765d0 Binary files /dev/null and b/labelImg/resources/icons/light_reset.png differ diff --git a/labelImg/resources/icons/light_reset.svg b/labelImg/resources/icons/light_reset.svg new file mode 100644 index 00000000..6aea0030 --- /dev/null +++ b/labelImg/resources/icons/light_reset.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/labelImg/resources/icons/new.png b/labelImg/resources/icons/new.png new file mode 100644 index 00000000..dd795cff Binary files /dev/null and b/labelImg/resources/icons/new.png differ diff --git a/labelImg/resources/icons/next.png b/labelImg/resources/icons/next.png new file mode 100644 index 00000000..163a343c Binary files /dev/null and b/labelImg/resources/icons/next.png differ diff --git a/labelImg/resources/icons/objects.png b/labelImg/resources/icons/objects.png new file mode 100644 index 00000000..593bb6d8 Binary files /dev/null and b/labelImg/resources/icons/objects.png differ diff --git a/labelImg/resources/icons/open.png b/labelImg/resources/icons/open.png new file mode 100644 index 00000000..45fa2883 Binary files /dev/null and b/labelImg/resources/icons/open.png differ diff --git a/labelImg/resources/icons/open.svg b/labelImg/resources/icons/open.svg new file mode 100644 index 00000000..48e7a343 --- /dev/null +++ b/labelImg/resources/icons/open.svg @@ -0,0 +1,577 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/labelImg/resources/icons/prev.png b/labelImg/resources/icons/prev.png new file mode 100644 index 00000000..31b45451 Binary files /dev/null and b/labelImg/resources/icons/prev.png differ diff --git a/labelImg/resources/icons/quit.png b/labelImg/resources/icons/quit.png new file mode 100644 index 00000000..74458879 Binary files /dev/null and b/labelImg/resources/icons/quit.png differ diff --git a/labelImg/resources/icons/resetall.png b/labelImg/resources/icons/resetall.png new file mode 100644 index 00000000..acc12c33 Binary files /dev/null and b/labelImg/resources/icons/resetall.png differ diff --git a/labelImg/resources/icons/save-as.png b/labelImg/resources/icons/save-as.png new file mode 100644 index 00000000..1b5d9000 Binary files /dev/null and b/labelImg/resources/icons/save-as.png differ diff --git a/labelImg/resources/icons/save-as.svg b/labelImg/resources/icons/save-as.svg new file mode 100644 index 00000000..c8441a1f --- /dev/null +++ b/labelImg/resources/icons/save-as.svg @@ -0,0 +1,1358 @@ + + + + + + + + + + + + + + + + + + + + +begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2004-01-26T11:58:28+02:00 + +2004-03-28T20:41:40Z + +Adobe Illustrator 10.0 + +2004-02-16T23:58:32+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmDzFo +3l7TJdT1e5W1tItuTbszHoiKN2Y+AxV4j5g/5ydvTcMnl/SYlgU0Se/LOzDxMcTIF/4M4qk//QzP +nv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8 +sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5F +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 +f//Z + + + + + + +uuid:4ee3f24b-6ed2-4a2e-8f7a-50b762c8da8b + + + +image/svg+xml + + + +mime.ai + + + + + + +end='w' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2004-02-04T02:08:51+02:00 + +2004-03-29T09:20:16Z + +Adobe Illustrator 10.0 + +2004-02-29T14:54:28+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX +Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY +q7FXzd+b/wDzlWum3k+h+QxFc3EJMdzrkoEkKuNiLZPsyU/nb4fAEb50vZ/YXEBPLsP5v62meXue +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 + + + + + + +uuid:f3c53255-be8a-4b04-817b-695bf2c54c8b + + + +image/svg+xml + + + +filesave.ai + + + + + + +end='w' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/labelImg/resources/icons/save.png b/labelImg/resources/icons/save.png new file mode 100644 index 00000000..daba865f Binary files /dev/null and b/labelImg/resources/icons/save.png differ diff --git a/labelImg/resources/icons/save.svg b/labelImg/resources/icons/save.svg new file mode 100644 index 00000000..5533e489 --- /dev/null +++ b/labelImg/resources/icons/save.svg @@ -0,0 +1,679 @@ + + + + + + + + begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2004-02-04T02:08:51+02:00 + +2004-03-29T09:20:16Z + +Adobe Illustrator 10.0 + +2004-02-29T14:54:28+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX +Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY +q7FXzd+b/wDzlWum3k+h+QxFc3EJMdzrkoEkKuNiLZPsyU/nb4fAEb50vZ/YXEBPLsP5v62meXue +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 + + + + + + +uuid:f3c53255-be8a-4b04-817b-695bf2c54c8b + + + +image/svg+xml + + + +filesave.ai + + + + + + end='w' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/labelImg/resources/icons/undo-cross.png b/labelImg/resources/icons/undo-cross.png new file mode 100644 index 00000000..7d57dcbd Binary files /dev/null and b/labelImg/resources/icons/undo-cross.png differ diff --git a/labelImg/resources/icons/undo.png b/labelImg/resources/icons/undo.png new file mode 100644 index 00000000..b2ac62bc Binary files /dev/null and b/labelImg/resources/icons/undo.png differ diff --git a/labelImg/resources/icons/verify.png b/labelImg/resources/icons/verify.png new file mode 100644 index 00000000..3f4a3b5f Binary files /dev/null and b/labelImg/resources/icons/verify.png differ diff --git a/labelImg/resources/icons/zoom-in.png b/labelImg/resources/icons/zoom-in.png new file mode 100644 index 00000000..1ac4864d Binary files /dev/null and b/labelImg/resources/icons/zoom-in.png differ diff --git a/labelImg/resources/icons/zoom-out.png b/labelImg/resources/icons/zoom-out.png new file mode 100644 index 00000000..d67a87de Binary files /dev/null and b/labelImg/resources/icons/zoom-out.png differ diff --git a/labelImg/resources/icons/zoom.png b/labelImg/resources/icons/zoom.png new file mode 100644 index 00000000..8265f278 Binary files /dev/null and b/labelImg/resources/icons/zoom.png differ diff --git a/labelImg/resources/strings/strings-ja-JP.properties b/labelImg/resources/strings/strings-ja-JP.properties new file mode 100644 index 00000000..239c52b3 --- /dev/null +++ b/labelImg/resources/strings/strings-ja-JP.properties @@ -0,0 +1,77 @@ +openFile=ファイルを開く +openFileDetail=画像かラベルファイルを開きます。 +quit=アプリを終了 +quitApp=このアプリを終了します。 +openDir=ディレクトリを開く +openAnnotation=アノテーションを開く +openAnnotationDetail=アノテーションファイルを開きます。 +changeSaveDir=保存先を変更する +resetAll=すべてリセットする +hideAll=すべての図形を非表示にする +viewAll=すべての図形を表示する +crtBox=矩形を作成する +crtBoxDetail=矩形を描画します。 +dupBox=矩形を複製する +delBox=矩形を削除する +editBox=矩形を編集する(&E) +hideAllBox=矩形を非表示にする(&H) +showAllBox=矩形を表示する(&S) +tutorialDefault=チュートリアル動画を見る +tutorialChrome=Chromeでチュートリアル動画を見る +tutorialDetail=LabelImgのデモ動画を見ます。 +info=LabelImgについて +verifyImg=画像を検証する +verifyImgDetail=画像を検証します。 +save=保存する +saveDetail=現在の編集を保存します。 +saveAs=名前を付けて保存 +saveAsDetail=ファイルに名前を付けて現在の編集を保存します。 +closeCur=このファイルを閉じる +deleteImg=この画像を削除する +resetAll=すべて初期化する +resetAllDetail=データをすべて初期状態に戻します。 +boxLineColor=線の色の編集 +boxLineColorDetail=色の編集を行います。 +chooseLineColor=線の色の編集 +chooseFillColor=塗りつぶす色の編集 +shortcut=ショートカット一覧を見る(英語) +editLabel=ラベルを編集する +editLabelDetail=選択した矩形のラベル名を変更します。 +changeSaveFormat=保存するデータのフォーマットを変更します。 +nextImg=次の画像 +nextImgDetail=次の画像を開きます。 +prevImg=前の画像 +prevImgDetail=前の画像を開きます。 +zoomin=拡大 +zoomout=縮小 +useDefaultLabel=指定されたラベル名を使う +advancedMode=上級者向け機能 +advancedModeDetail=上級者向け機能を使います。 +boxLabelText=矩形ラベル一覧 +labels=ラベル一覧 +autoSaveMode=自動で保存する +singleClsMode=単一のラベルだけをアノテーションする +displayLabel=ラベルを表示する +menu_file=ファイル(&F) +menu_edit=編集(&E) +menu_view=表示(&V) +menu_help=ヘルプ(&H) +menu_openRecent=最近開いたファイル(&R) +drawSquares=四角形を書く +fitWin=ウィンドウに合わせる +fitWinDetail=ウィンドウサイズに合わせて拡大します。 +fitWidth=ウィンドウの幅に合わせる +fitWidthDetail=ウインドウの幅に合わせて拡大します。 +lightbrighten=明るくする +lightbrightenDetail=画像の明るさを上げる +lightdarken=暗くする +lightdarkenDetail=画像の明るさを下げる +lightreset=元の明るさ +lightresetDetail=元の明るさを復元する +lightWidgetTitle=画像の明るさ +originalsize=原寸に戻す +displayLabel=ラベルを表示する +fileList=ファイル一覧 +files=ファイル +boxLabelText=矩形ラベル +copyPrevBounding=前の画像の矩形ラベルをこの画像にコピー diff --git a/labelImg/resources/strings/strings-zh-CN.properties b/labelImg/resources/strings/strings-zh-CN.properties new file mode 100644 index 00000000..e944509a --- /dev/null +++ b/labelImg/resources/strings/strings-zh-CN.properties @@ -0,0 +1,89 @@ +openFile=打开文件 +openFileDetail=打开图像文件 +quit=退出 +quitApp=退出程序 +openDir=打开目录 +copyPrevBounding=复制当前图像中的上一个边界框 +changeSavedAnnotationDir=更改保存标签文件的预设目录 +openAnnotation=开启标签 +openAnnotationDetail=打开标签文件 +changeSaveDir=改变存放目录 +nextImg=下一个图像 +nextImgDetail=下一个图像 +prevImg=上一个图像 +prevImgDetail=上一个图像 +verifyImg=验证图像 +verifyImgDetail=验证图像 +save=保存 +saveDetail=保存标签文件 +changeSaveFormat=更改存储格式 +saveAs=另存为 +saveAsDetail=將标签保存到其他文件 +closeCur=关闭文件 +closeCurDetail=关闭当前文件 +deleteImg=删除图像 +deleteImgDetail=删除当前图像 +resetAll=全部重置 +resetAllDetail=重置所有设定 +boxLineColor=区块线条颜色 +boxLineColorDetail=选择线框颜色 +crtBox=创建区块 +crtBoxDetail=创建一个新的区块 +delBox=删除选择的区块 +delBoxDetail=删除区块 +dupBox=复制区块 +dupBoxDetail=复制区块 +editBox=编辑区块 +editBoxDetail=编辑区块 +hideAllBox=隐藏所有区块 +showAllBox=显示所有区块 +tutorialDefault=YouTube教学 +tutorialChrome=YouTube教学(Chrome) +tutorialDetail=显示示范内容 +info=版本信息 +shortcut=快捷键 +zoomin=放大画面 +zoominDetail=放大画面 +zoomout=缩小画面 +zoomoutDetail=缩小画面 +originalsize=原始大小 +originalsizeDetail=放大到原始大小 +fitWin=调整到窗口大小 +fitWinDetail=缩放到当前窗口大小 +fitWidth=缩放到跟当前画面一样宽 +fitWidthDetail=调整宽度适应到窗口宽度 +lightbrighten=提亮 +lightbrightenDetail=增加图像亮度 +lightdarken=变暗 +lightdarkenDetail=降低图像亮度 +lightreset=原始亮度 +lightresetDetail=恢复原来的亮度 +lightWidgetTitle=图像亮度 +editLabel=编辑标签 +editLabelDetail=修改当前所选的区块颜色 +shapeLineColor=形状线条颜色 +shapeLineColorDetail=更改线条颜色 +shapeFillColor=填充颜色 +shapeFillColorDetail=更改填充颜色 +showHide=显示/隐藏标签 +useDefaultLabel=使用预设标签 +useDifficult=有难度的 +boxLabelText=区块的标签 +labels=标签 +autoSaveMode=自动保存模式 +singleClsMode=单一类别模式 +displayLabel=显示类别 +fileList=文件列表 +files=文件 +advancedMode=专家模式 +advancedModeDetail=切换到专家模式 +showAllBoxDetail=显示所有区块 +hideAllBoxDetail=隐藏所有区块 +menu_file=文件(&F) +menu_edit=编辑(&E) +menu_view=查看(&V) +menu_help=帮助(&H) +menu_openRecent=最近打开(&R) +chooseLineColor=选择线条颜色 +chooseFillColor=选择填充颜色 +drawSquares=绘制正方形 diff --git a/labelImg/resources/strings/strings-zh-TW.properties b/labelImg/resources/strings/strings-zh-TW.properties new file mode 100644 index 00000000..162ab215 --- /dev/null +++ b/labelImg/resources/strings/strings-zh-TW.properties @@ -0,0 +1,83 @@ +saveAsDetail=將標籤保存到其他文件 +changeSaveDir=改變存放目錄 +openFile=開啟檔案 +shapeLineColorDetail=更改線條顏色 +resetAll=重置 +crtBox=創建區塊 +crtBoxDetail=畫一個區塊 +dupBoxDetail=複製區塊 +verifyImg=驗證圖像 +zoominDetail=放大 +verifyImgDetail=驗證圖像 +saveDetail=將標籤存到 +openFileDetail=打開圖像 +fitWidthDetail=調整到窗口寬度 +lightbrighten=變亮 +lightbrightenDetail=增加影像亮度 +lightdarken=變暗 +lightdarkenDetail=降低影像亮度 +lightreset=原本亮度 +lightresetDetail=恢復原本亮度 +lightWidgetTitle=影像量度 +tutorialDefault=YouTube教學 +tutorialChrome=YouTube教學(Chrome) +info=版本信息 +shortcut=快捷鍵 +editLabel=編輯標籤 +openAnnotationDetail=打開標籤文件 +quit=結束 +shapeFillColorDetail=更改填充顏色 +closeCurDetail=關閉目前檔案 +closeCur=關閉 +deleteImg=刪除圖像 +deleteImgDetail=刪除目前圖像 +fitWin=調整到跟窗口一樣大小 +delBox=刪除選取區塊 +boxLineColorDetail=選擇框線顏色 +originalsize=原始大小 +resetAllDetail=重設所有設定 +zoomoutDetail=畫面放大 +save=儲存 +saveAs=另存為 +fitWinDetail=縮放到窗口一樣 +openDir=開啟目錄 +copyPrevBounding=複製當前圖像中的上一個邊界框 +showHide=顯示/隱藏標籤 +changeSaveFormat=更改儲存格式 +shapeFillColor=填充顏色 +quitApp=離開本程式 +dupBox=複製區塊 +delBoxDetail=刪除區塊 +zoomin=放大畫面 +info=資訊 +openAnnotation=開啟標籤 +prevImgDetail=上一個圖像 +fitWidth=縮放到跟畫面一樣寬 +zoomout=縮小畫面 +changeSavedAnnotationDir=更改預設標籤存的目錄 +nextImgDetail=下一個圖像 +originalsizeDetail=放大到原始大小 +prevImg=上一個圖像 +tutorialDetail=顯示示範內容 +shapeLineColor=形狀線條顏色 +boxLineColor=日期分隔線顏色 +editLabelDetail=修改所選區塊的標籤 +nextImg=下一張圖片 +useDefaultLabel=使用預設標籤 +useDifficult=有難度的 +boxLabelText=區塊的標籤 +labels=標籤 +autoSaveMode=自動儲存模式 +singleClsMode=單一類別模式 +displayLabel=顯示類別 +fileList=檔案清單 +files=檔案 +advancedMode=進階模式 +advancedModeDetail=切到進階模式 +showAllBoxDetail=顯示所有區塊 +hideAllBoxDetail=隱藏所有區塊 +menu_file=檔案(&F) +menu_edit=編輯(&E) +menu_view=檢視(&V) +menu_help=說明(&H) +menu_openRecent=最近開啟(&R) diff --git a/labelImg/resources/strings/strings.properties b/labelImg/resources/strings/strings.properties new file mode 100644 index 00000000..8c5d14ce --- /dev/null +++ b/labelImg/resources/strings/strings.properties @@ -0,0 +1,89 @@ +openFile=Open +openFileDetail=Open image or label file +quit=Quit +quitApp=Quit application +openDir=Open Dir +copyPrevBounding=Copy previous Bounding Boxes in the current image +changeSavedAnnotationDir=Change default saved Annotation dir +openAnnotation=Open Annotation +openAnnotationDetail=Open an annotation file +changeSaveDir=Change Save Dir +nextImg=Next Image +nextImgDetail=Open the next Image +prevImg=Prev Image +prevImgDetail=Open the previous Image +verifyImg=Verify Image +verifyImgDetail=Verify Image +save=Save +saveDetail=Save the labels to a file +changeSaveFormat=Change save format +saveAs=Save As +saveAsDetail=Save the labels to a different file +closeCur=Close +closeCurDetail=Close the current file +deleteImg=Delete current image +deleteImgDetail=Delete the current image +resetAll=Reset All +resetAllDetail=Reset All +boxLineColor=Box Line Color +boxLineColorDetail=Choose Box line color +crtBox=Create RectBox +crtBoxDetail=Draw a new box +delBox=Delete RectBox +delBoxDetail=Remove the box +dupBox=Duplicate RectBox +dupBoxDetail=Create a duplicate of the selected box +editBox=&Edit RectBox +editBoxDetail=Move and edit Boxs +hideAllBox=&Hide RectBox +showAllBox=&Show RectBox +tutorialDefault=Tutorial +tutorialChrome=Tutorial(Chrome) +tutorialDetail=Show demo +info=Information +shortcut=Keyboard shortcuts +zoomin=Zoom In +zoominDetail=Increase zoom level +zoomout=Zoom Out +zoomoutDetail=Decrease zoom level +originalsize=Original size +originalsizeDetail=Zoom to original size +fitWin=Fit Window +fitWinDetail=Zoom follows window size +fitWidth=Fit Width +fitWidthDetail=Zoom follows window width +lightbrighten=Brighten +lightbrightenDetail=Increase Image Brightness +lightdarken=Darken +lightdarkenDetail=Decrease Image Brightness +lightreset=Original Brightness +lightresetDetail=Restore original brightness +lightWidgetTitle=Image Brightness +editLabel=Edit Label +editLabelDetail=Modify the label of the selected Box +shapeLineColor=Shape Line Color +shapeLineColorDetail=Change the line color for this specific shape +shapeFillColor=Shape Fill Color +shapeFillColorDetail=Change the fill color for this specific shape +showHide=Show/Hide Label Panel +useDefaultLabel=Use default label +useDifficult=difficult +boxLabelText=Box Labels +labels=Labels +autoSaveMode=Auto Save mode +singleClsMode=Single Class Mode +displayLabel=Display Labels +fileList=File List +files=Files +advancedMode=Advanced Mode +advancedModeDetail=Swtich to advanced mode +showAllBoxDetail=Show all bounding boxes +hideAllBoxDetail=Hide all bounding boxes +menu_file=&File +menu_edit=&Edit +menu_view=&View +menu_help=&Help +menu_openRecent=Open &Recent +chooseLineColor=Choose Line Color +chooseFillColor=Choose Fill Color +drawSquares=Draw Squares \ No newline at end of file diff --git a/labelImg/setup.cfg b/labelImg/setup.cfg new file mode 100644 index 00000000..3b273b63 --- /dev/null +++ b/labelImg/setup.cfg @@ -0,0 +1,8 @@ +[bumpversion] +commit = True +tag = True + +[bumpversion:file:setup.py] + +[bdist_wheel] +universal = 1 diff --git a/labelImg/setup.py b/labelImg/setup.py new file mode 100644 index 00000000..39f7eb5e --- /dev/null +++ b/labelImg/setup.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages, Command +from sys import platform as _platform +from shutil import rmtree +import sys +import os + +here = os.path.abspath(os.path.dirname(__file__)) +NAME = 'labelImg' +REQUIRES_PYTHON = '>=3.0.0' +REQUIRED_DEP = ['pyqt5', 'lxml'] +about = {} + +with open(os.path.join(here, 'libs', '__init__.py')) as f: + exec(f.read(), about) + +with open("README.rst", "rb") as readme_file: + readme = readme_file.read().decode("UTF-8") + +with open("HISTORY.rst", "rb") as history_file: + history = history_file.read().decode("UTF-8") + +# OS specific settings +SET_REQUIRES = [] +if _platform == "linux" or _platform == "linux2": + # linux + print('linux') +elif _platform == "darwin": + # MAC OS X + SET_REQUIRES.append('py2app') + +required_packages = find_packages() +required_packages.append('labelImg') + +APP = [NAME + '.py'] +OPTIONS = { + 'argv_emulation': True, + 'iconfile': 'resources/icons/app.icns' +} + +class UploadCommand(Command): + """Support setup.py upload.""" + + description=readme + '\n\n' + history, + + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print('\033[1m{0}\033[0m'.format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status('Removing previous builds…') + rmtree(os.path.join(here, 'dist')) + except OSError: + self.status('Fail to remove previous builds..') + pass + + self.status('Building Source and Wheel (universal) distribution…') + os.system( + '{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) + + self.status('Uploading the package to PyPI via Twine…') + os.system('twine upload dist/*') + + self.status('Pushing git tags…') + os.system('git tag -d v{0}'.format(about['__version__'])) + os.system('git tag v{0}'.format(about['__version__'])) + # os.system('git push --tags') + + sys.exit() + + +setup( + app=APP, + name=NAME, + version=about['__version__'], + description="LabelImg is a graphical image annotation tool and label object bounding boxes in images", + long_description=readme + '\n\n' + history, + author="TzuTa Lin", + author_email='tzu.ta.lin@gmail.com', + url='https://github.com/tzutalin/labelImg', + python_requires=REQUIRES_PYTHON, + package_dir={'labelImg': '.'}, + packages=required_packages, + entry_points={ + 'console_scripts': [ + 'labelImg=labelImg.labelImg:main' + ] + }, + include_package_data=True, + install_requires=REQUIRED_DEP, + license="MIT license", + zip_safe=False, + keywords='labelImg labelTool development annotation deeplearning', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + package_data={'data/predefined_classes.txt': ['data/predefined_classes.txt']}, + options={'py2app': OPTIONS}, + setup_requires=SET_REQUIRES, + # $ setup.py publish support. + cmdclass={ + 'upload': UploadCommand, + } +) diff --git a/labelImg/tests/.gitignore b/labelImg/tests/.gitignore new file mode 100644 index 00000000..a6535f35 --- /dev/null +++ b/labelImg/tests/.gitignore @@ -0,0 +1 @@ +test.xml diff --git a/labelImg/tests/test.512.512.bmp b/labelImg/tests/test.512.512.bmp new file mode 100644 index 00000000..fe7415de Binary files /dev/null and b/labelImg/tests/test.512.512.bmp differ diff --git a/labelImg/tests/test_io.py b/labelImg/tests/test_io.py new file mode 100644 index 00000000..7bc31b3a --- /dev/null +++ b/labelImg/tests/test_io.py @@ -0,0 +1,101 @@ +import os +import sys +import unittest + +class TestPascalVocRW(unittest.TestCase): + + def test_upper(self): + dir_name = os.path.abspath(os.path.dirname(__file__)) + libs_path = os.path.join(dir_name, '..', 'libs') + sys.path.insert(0, libs_path) + from pascal_voc_io import PascalVocWriter + from pascal_voc_io import PascalVocReader + + # Test Write/Read + writer = PascalVocWriter('tests', 'test', (512, 512, 1), local_img_path='tests/test.512.512.bmp') + difficult = 1 + writer.add_bnd_box(60, 40, 430, 504, 'person', difficult) + writer.add_bnd_box(113, 40, 450, 403, 'face', difficult) + writer.save('tests/test.xml') + + reader = PascalVocReader('tests/test.xml') + shapes = reader.get_shapes() + + person_bnd_box = shapes[0] + face = shapes[1] + self.assertEqual(person_bnd_box[0], 'person') + self.assertEqual(person_bnd_box[1], [(60, 40), (430, 40), (430, 504), (60, 504)]) + self.assertEqual(face[0], 'face') + self.assertEqual(face[1], [(113, 40), (450, 40), (450, 403), (113, 403)]) + + +class TestCreateMLRW(unittest.TestCase): + + def test_a_write(self): + dir_name = os.path.abspath(os.path.dirname(__file__)) + libs_path = os.path.join(dir_name, '..', 'libs') + sys.path.insert(0, libs_path) + from create_ml_io import CreateMLWriter + + person = {'label': 'person', 'points': ((65, 45), (420, 45), (420, 512), (65, 512))} + face = {'label': 'face', 'points': ((245, 250), (350, 250), (350, 365), (245, 365))} + + expected_width = 105 # 350-245 -> create_ml_io.py ll 46 + expected_height = 115 # 365-250 -> create_ml_io.py ll 49 + expected_x = 297.5 # 245+105/2 -> create_ml_io.py ll 53 + expected_y = 307.5 # 250+115/2 > create_ml_io.py ll 54 + + shapes = [person, face] + output_file = dir_name + "/tests.json" + + writer = CreateMLWriter('tests', 'test.512.512.bmp', (512, 512, 1), shapes, output_file, + local_img_path='tests/test.512.512.bmp') + + writer.verified = True + writer.write() + + # check written json + with open(output_file, "r") as file: + input_data = file.read() + + import json + data_dict = json.loads(input_data)[0] + self.assertEqual(True, data_dict['verified'], 'verified tag not reflected') + self.assertEqual('test.512.512.bmp', data_dict['image'], 'filename not correct in .json') + self.assertEqual(2, len(data_dict['annotations']), 'output file contains to less annotations') + face = data_dict['annotations'][1] + self.assertEqual('face', face['label'], 'label name is wrong') + face_coords = face['coordinates'] + self.assertEqual(expected_width, face_coords['width'], 'calculated width is wrong') + self.assertEqual(expected_height, face_coords['height'], 'calculated height is wrong') + self.assertEqual(expected_x, face_coords['x'], 'calculated x is wrong') + self.assertEqual(expected_y, face_coords['y'], 'calculated y is wrong') + + def test_b_read(self): + dir_name = os.path.abspath(os.path.dirname(__file__)) + libs_path = os.path.join(dir_name, '..', 'libs') + sys.path.insert(0, libs_path) + from create_ml_io import CreateMLReader + + output_file = dir_name + "/tests.json" + reader = CreateMLReader(output_file, 'tests/test.512.512.bmp') + shapes = reader.get_shapes() + face = shapes[1] + + self.assertEqual(2, len(shapes), 'shape count is wrong') + self.assertEqual('face', face[0], 'label is wrong') + + face_coords = face[1] + x_min = face_coords[0][0] + x_max = face_coords[1][0] + y_min = face_coords[0][1] + y_max = face_coords[2][1] + + self.assertEqual(245, x_min, 'xmin is wrong') + self.assertEqual(350, x_max, 'xmax is wrong') + self.assertEqual(250, y_min, 'ymin is wrong') + self.assertEqual(365, y_max, 'ymax is wrong') + + +if __name__ == '__main__': + unittest.main() diff --git a/labelImg/tests/test_qt.py b/labelImg/tests/test_qt.py new file mode 100644 index 00000000..869094de --- /dev/null +++ b/labelImg/tests/test_qt.py @@ -0,0 +1,20 @@ + +from unittest import TestCase + +from labelImg import get_main_app + + +class TestMainWindow(TestCase): + + app = None + win = None + + def setUp(self): + self.app, self.win = get_main_app() + + def tearDown(self): + self.win.close() + self.app.quit() + + def test_noop(self): + pass diff --git a/labelImg/tests/test_settings.py b/labelImg/tests/test_settings.py new file mode 100644 index 00000000..4df06d5f --- /dev/null +++ b/labelImg/tests/test_settings.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +import os +import sys +import time +import unittest + +__author__ = 'TzuTaLin' + +dir_name = os.path.abspath(os.path.dirname(__file__)) +libs_path = os.path.join(dir_name, '..', 'libs') +sys.path.insert(0, libs_path) +from settings import Settings + +class TestSettings(unittest.TestCase): + + def test_basic(self): + settings = Settings() + settings['test0'] = 'hello' + settings['test1'] = 10 + settings['test2'] = [0, 2, 3] + self.assertEqual(settings.get('test3', 3), 3) + self.assertEqual(settings.save(), True) + + settings.load() + self.assertEqual(settings.get('test0'), 'hello') + self.assertEqual(settings.get('test1'), 10) + + settings.reset() + + + +if __name__ == '__main__': + unittest.main() diff --git a/labelImg/tests/test_stringBundle.py b/labelImg/tests/test_stringBundle.py new file mode 100644 index 00000000..0dea1044 --- /dev/null +++ b/labelImg/tests/test_stringBundle.py @@ -0,0 +1,29 @@ +import os +import sys +import unittest +import resources +from stringBundle import StringBundle + +class TestStringBundle(unittest.TestCase): + + def test_loadDefaultBundle_withoutError(self): + str_bundle = StringBundle.get_bundle('en') + self.assertEqual(str_bundle.get_string("openDir"), 'Open Dir', 'Fail to load the default bundle') + + def test_fallback_withoutError(self): + str_bundle = StringBundle.get_bundle('zh-TW') + self.assertEqual(str_bundle.get_string("openDir"), u'\u958B\u555F\u76EE\u9304', 'Fail to load the zh-TW bundle') + + def test_setInvaleLocaleToEnv_printErrorMsg(self): + prev_lc = os.environ['LC_ALL'] + prev_lang = os.environ['LANG'] + os.environ['LC_ALL'] = 'UTF-8' + os.environ['LANG'] = 'UTF-8' + str_bundle = StringBundle.get_bundle() + self.assertEqual(str_bundle.get_string("openDir"), 'Open Dir', 'Fail to load the default bundle') + os.environ['LC_ALL'] = prev_lc + os.environ['LANG'] = prev_lang + + +if __name__ == '__main__': + unittest.main() diff --git a/labelImg/tests/test_utils.py b/labelImg/tests/test_utils.py new file mode 100644 index 00000000..a1971efc --- /dev/null +++ b/labelImg/tests/test_utils.py @@ -0,0 +1,22 @@ +import os +import sys +import unittest +from libs.utils import Struct, new_action, new_icon, add_actions, format_shortcut, generate_color_by_text, natural_sort + +class TestUtils(unittest.TestCase): + + def test_generateColorByGivingUniceText_noError(self): + res = generate_color_by_text(u'\u958B\u555F\u76EE\u9304') + self.assertTrue(res.green() >= 0) + self.assertTrue(res.red() >= 0) + self.assertTrue(res.blue() >= 0) + + def test_nautalSort_noError(self): + l1 = ['f1', 'f11', 'f3'] + expected_l1 = ['f1', 'f3', 'f11'] + natural_sort(l1) + for idx, val in enumerate(l1): + self.assertTrue(val == expected_l1[idx]) + +if __name__ == '__main__': + unittest.main() diff --git a/labelImg/tests/臉書.jpg b/labelImg/tests/臉書.jpg new file mode 100644 index 00000000..f6003096 Binary files /dev/null and b/labelImg/tests/臉書.jpg differ diff --git a/labelImg/tools/README.md b/labelImg/tools/README.md new file mode 100644 index 00000000..05fbdbb0 --- /dev/null +++ b/labelImg/tools/README.md @@ -0,0 +1,87 @@ +# Additional tools + +## Convert the label files to CSV + +### Introduction +To train the images on [Google Cloud AutoML](https://cloud.google.com/automl), we should prepare the specific csv files follow [this format](https://cloud.google.com/vision/automl/object-detection/docs/csv-format). + +`label_to_csv.py` can convert the `txt` or `xml` label files to csv file. The labels files should strictly follow to below structure. + +### Structures +* Images + To train the object detection tasks, all the images should upload to the cloud storage and access it by its name. All the images should stay in the **same buckets** in cloud storage. Also, different classes should have their own folder as below. + ``` + (on the cloud storage) + | -- class1 + | | -- class1_01.jpg + | | -- class1_02.jpg + | | ... + | -- class2 + | | -- class2_01.jpg + | | -- class2_02.jpg + | | ... + | ... + ``` + Note, URI of the `class1_01.jpg` is `gs:///class1/class1_01.jpg` +* Labels + There are four types of training data - `TRAINING`, `VALIDATION`, `TEST` and `UNASSIGNED`. To assign different categories, we should create four directories. + Inside each folder, users should create the class folders with the same name in cloud storage (see below structure). + ``` + labels (on PC) + | -- TRAINING + | | -- class1 + | | | -- class1_01.txt (or .xml) + | | | ... + | | -- class2 + | | | -- class2_01.txt (or .xml) + | | | ... + | | ... + | -- VALIDATION + | | -- class1 + | | | -- class1_02.txt (or .xml) + | | | ... + | | -- class2 + | | | -- class2_02.txt (or .xml) + | | | ... + | | ... + | -- TEST + | | (same as TRAINING and VALIDATION) + | -- UNASSIGNED + | | (same as TRAINING and VALIDATION) + ``` + +### Usage + +To see the argument of `label_to_csv.py`, +```commandline +python label_to_csv.py -h +``` + +```commandline +usage: label_to_csv.py [-h] -p PREFIX -l LOCATION -m MODE [-o OUTPUT] + [-c CLASSES] + +optional arguments: + -h, --help show this help message and exit + -p PREFIX, --prefix PREFIX + Bucket of the cloud storage path + -l LOCATION, --location LOCATION + Parent directory of the label files + -m MODE, --mode MODE 'xml' for converting from xml and 'txt' for converting + from txt + -o OUTPUT, --output OUTPUT + Output name of csv file + -c CLASSES, --classes CLASSES + Label classes path +``` + +For example, if mine bucket name is **test**, the location of the label directory is **/User/test/labels**, the mode I choose from is **txt**, the output name and the class path is same as default. +```commandline +python label_to_csv.py \ +-p test\ +-l /User/test/labels \ +-m txt +``` + +The output file is `res.csv` by default. Afterwards, upload the csv file to the cloud storage and you can start training! + diff --git a/labelImg/tools/label_to_csv.py b/labelImg/tools/label_to_csv.py new file mode 100755 index 00000000..f3b72bab --- /dev/null +++ b/labelImg/tools/label_to_csv.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Name: label_to_csv.py +Author: Justin Ruan +Contact: justin900429@gmail.com +Time: 2021.02.06 +""" + +import os +import argparse +import codecs + +import pandas as pd + + +def txt2csv(location, training_dir, path_prefix): + # Return list + temp_res = [] + + # Run through all the files + for file in os.listdir(location): + # Check the file name ends with txt + # and not class.txt + if (not file.endswith(".txt")) | \ + (file == "classes.txt"): + continue + + # Get the file name + file_whole_name = f"{location}/{file}" + + # Read in txt as csv + df_txt = pd.read_csv(file_whole_name, sep=" ", header=None) + + # Create data for each labels + for index, row in df_txt.iterrows(): + # Temp array for csv, initialized by the training types + temp_csv = [str(training_dir)] + + # gs://prefix/name/{image_name} + cloud_path = f"{path_prefix}/{os.path.splitext(file)[0]}.jpg" + temp_csv.append(cloud_path) + + # Class label + temp_csv.append(class_labels[int(row[0])]) + + # Add the upper left coordinate + x_min = min(max(0.0, row[1] - row[3] / 2), 1.0) + y_min = min(max(0.0, row[2] - row[4] / 2), 1.0) + temp_csv.extend([x_min, y_min]) + + # Add the lower left coordinate (not necessary, left blank) + temp_csv.extend(["", ""]) + + # Add the lower right coordinate + x_max = min(max(0.0, row[1] + row[3] / 2), 1.0) + y_max = min(max(0.0, row[2] + row[4] / 2), 1.0) + temp_csv.extend([x_max, y_max]) + + # Add the upper right coordinate (not necessary, left blank) + temp_csv.extend(["", ""]) + + # Append to the res + temp_res.append(temp_csv) + + return temp_res + + +def xml2csv(location, training_dir, path_prefix): + # To parse the xml files + import xml.etree.ElementTree as ET + + # Return list + temp_res = [] + + # Run through all the files + for file in os.listdir(location): + # Check the file name ends with xml + if not file.endswith(".xml"): + continue + + # Get the file name + file_whole_name = f"{location}/{file}" + + # Open the xml name + tree = ET.parse(file_whole_name) + root = tree.getroot() + + # Get the width, height of images + # to normalize the bounding boxes + size = root.find("size") + width, height = float(size.find("width").text), float(size.find("height").text) + + # Find all the bounding objects + for label_object in root.findall("object"): + # Temp array for csv, initialized by the training types + temp_csv = [str(training_dir)] + + # gs://prefix/name/{image_name} + cloud_path = f"{path_prefix}/{os.path.splitext(file)[0]}.jpg" + temp_csv.append(cloud_path) + + # Class label + temp_csv.append(label_object.find("name").text) + + # Bounding box coordinate + bounding_box = label_object.find("bndbox") + + # Add the upper left coordinate + x_min = float(bounding_box.find("xmin").text) / width + y_min = float(bounding_box.find("ymin").text) / height + temp_csv.extend([x_min, y_min]) + + # Add the lower left coordinate (not necessary, left blank) + temp_csv.extend(["", ""]) + + # Add the lower right coordinate + x_max = float(bounding_box.find("xmax").text) / width + y_max = float(bounding_box.find("ymax").text) / height + temp_csv.extend([x_max, y_max]) + + # Add the upper right coordinate (not necessary, left blank) + temp_csv.extend(["", ""]) + + # Append to the res + temp_res.append(temp_csv) + + return temp_res + + +if __name__ == "__main__": + # Add the argument parse + arg_p = argparse.ArgumentParser() + arg_p.add_argument("-p", "--prefix", + required=True, + type=str, + help="Bucket of the cloud storage path") + arg_p.add_argument("-l", "--location", + type=str, + required=True, + help="Location of the label files") + arg_p.add_argument("-m", "--mode", + type=str, + required=True, + help="'xml' for converting from xml and 'txt' for converting from txt") + arg_p.add_argument("-o", "--output", + type=str, + default="res.csv", + help="Output name of csv file") + arg_p.add_argument("-c", "--classes", + type=str, + default=os.path.join("..", "data", "predefined_classes.txt"), + help="Label classes path") + args = vars(arg_p.parse_args()) + + # Class labels + class_labels = [] + + # Load in the defined classes + if os.path.exists(args["classes"]) is True: + with codecs.open(args["classes"], 'r', 'utf8') as f: + for line in f: + line = line.strip() + class_labels.append(line) + else: # Exit if errors occurred + print(f"File: {args['classes']} not exists") + exit(1) + + # Prefix of the cloud storage + ori_prefix = f"gs://{args['prefix']}" + + # Array for final csv file + res = [] + # Get all the file in dir + for training_type_dir in os.listdir(args["location"]): + # Get the dirname + dir_name = f"{args['location']}/{training_type_dir}" + + # Check whether is dir + if not os.path.isdir(dir_name): + continue + # Process the files + + for class_type_dir in os.listdir(dir_name): + + # Check whether is dir + if not os.path.isdir(dir_name): + continue + + prefix = f"{ori_prefix}/{class_type_dir}" + + # Convert the chosen extension to csv + if args["mode"] == "txt": + res.extend(txt2csv(f"{dir_name}/{class_type_dir}", + training_type_dir, + prefix)) + elif args["mode"] == "xml": + res.extend(xml2csv(f"{dir_name}/{class_type_dir}", + training_type_dir, + prefix)) + else: + print("Wrong argument for convert mode.\n" + "'xml' for converting from xml to csv\n" + "'txt' for converting from txt to csv") + exit(1) + + # Write to the result csv + res_csv = pd.DataFrame(res, + columns=["set", "path", "label", + "x_min", "y_min", + "x_max", "y_min", + "x_max", "y_max", + "x_min", "y_max"]) + res_csv.to_csv("res.csv", index=False, header=False)