Repository: wkentaro/labelme Branch: main Commit: 89a615e5d777 Files: 162 Total size: 1.1 MB Directory structure: gitextract_ghzsofy7/ ├── .claude/ │ └── CLAUDE.md ├── .git-blame-ignore-revs ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── 1.bug_report.yml │ │ └── config.yml │ └── workflows/ │ ├── ci.yml │ ├── cla.yml │ └── discord-notify.yml ├── .gitignore ├── .gitmodules ├── .python-version ├── CITATION.cff ├── CLA.md ├── LICENSE ├── Makefile ├── README.md ├── examples/ │ ├── bbox_detection/ │ │ ├── README.md │ │ ├── data_annotated/ │ │ │ ├── 2011_000003.json │ │ │ ├── 2011_000006.json │ │ │ └── 2011_000025.json │ │ ├── data_dataset_voc/ │ │ │ ├── Annotations/ │ │ │ │ ├── 2011_000003.xml │ │ │ │ ├── 2011_000006.xml │ │ │ │ └── 2011_000025.xml │ │ │ └── class_names.txt │ │ ├── labelme2voc.py │ │ └── labels.txt │ ├── classification/ │ │ ├── README.md │ │ ├── data_annotated/ │ │ │ ├── 0001.json │ │ │ └── 0002.json │ │ └── flags.txt │ ├── instance_segmentation/ │ │ ├── README.md │ │ ├── data_annotated/ │ │ │ ├── 2011_000003.json │ │ │ ├── 2011_000006.json │ │ │ └── 2011_000025.json │ │ ├── data_dataset_coco/ │ │ │ └── annotations.json │ │ ├── data_dataset_voc/ │ │ │ ├── SegmentationClassNpy/ │ │ │ │ ├── 2011_000003.npy │ │ │ │ ├── 2011_000006.npy │ │ │ │ └── 2011_000025.npy │ │ │ ├── SegmentationObjectNpy/ │ │ │ │ ├── 2011_000003.npy │ │ │ │ ├── 2011_000006.npy │ │ │ │ └── 2011_000025.npy │ │ │ └── class_names.txt │ │ ├── labelme2coco.py │ │ ├── labelme2voc.py │ │ └── labels.txt │ ├── primitives/ │ │ └── primitives.json │ ├── semantic_segmentation/ │ │ ├── README.md │ │ ├── data_annotated/ │ │ │ ├── 2011_000003.json │ │ │ ├── 2011_000006.json │ │ │ └── 2011_000025.json │ │ ├── data_dataset_voc/ │ │ │ ├── SegmentationClassNpy/ │ │ │ │ ├── 2011_000003.npy │ │ │ │ ├── 2011_000006.npy │ │ │ │ └── 2011_000025.npy │ │ │ └── class_names.txt │ │ └── labels.txt │ ├── tutorial/ │ │ ├── README.md │ │ ├── apc2016_obj3/ │ │ │ └── label_names.txt │ │ ├── apc2016_obj3.json │ │ ├── draw_json.py │ │ ├── draw_label_png.py │ │ ├── export_json.py │ │ └── load_label_png.py │ └── video_annotation/ │ ├── README.md │ ├── data_annotated/ │ │ ├── 00000100.json │ │ ├── 00000101.json │ │ ├── 00000102.json │ │ ├── 00000103.json │ │ └── 00000104.json │ ├── data_dataset_voc/ │ │ ├── SegmentationClass/ │ │ │ ├── 00000100.npy │ │ │ ├── 00000101.npy │ │ │ ├── 00000102.npy │ │ │ ├── 00000103.npy │ │ │ └── 00000104.npy │ │ └── class_names.txt │ └── labels.txt ├── labelme/ │ ├── __init__.py │ ├── __main__.py │ ├── _automation/ │ │ ├── __init__.py │ │ ├── _osam_session.py │ │ ├── bbox_from_text.py │ │ └── polygon_from_mask.py │ ├── _label_file.py │ ├── app.py │ ├── config/ │ │ ├── __init__.py │ │ └── default_config.yaml │ ├── shape.py │ ├── testing.py │ ├── translate/ │ │ ├── de_DE.qm │ │ ├── de_DE.ts │ │ ├── es_ES.qm │ │ ├── es_ES.ts │ │ ├── fa_IR.qm │ │ ├── fa_IR.ts │ │ ├── fr_FR.qm │ │ ├── fr_FR.ts │ │ ├── hu_HU.qm │ │ ├── hu_HU.ts │ │ ├── it_IT.qm │ │ ├── it_IT.ts │ │ ├── ja_JP.qm │ │ ├── ja_JP.ts │ │ ├── ko_KR.qm │ │ ├── ko_KR.ts │ │ ├── nl_NL.qm │ │ ├── nl_NL.ts │ │ ├── pl_PL.qm │ │ ├── pl_PL.ts │ │ ├── pt_BR.qm │ │ ├── pt_BR.ts │ │ ├── th_TH.qm │ │ ├── th_TH.ts │ │ ├── tr_TR.qm │ │ ├── tr_TR.ts │ │ ├── vi_VN.qm │ │ ├── vi_VN.ts │ │ ├── zh_CN.qm │ │ ├── zh_CN.ts │ │ ├── zh_TW.qm │ │ └── zh_TW.ts │ ├── utils/ │ │ ├── __init__.py │ │ ├── _io.py │ │ ├── image.py │ │ ├── qt.py │ │ └── shape.py │ └── widgets/ │ ├── __init__.py │ ├── _ai_assisted_annotation_widget.py │ ├── _ai_text_to_annotation_widget.py │ ├── _info_button.py │ ├── _status.py │ ├── brightness_contrast_dialog.py │ ├── canvas.py │ ├── download.py │ ├── file_dialog_preview.py │ ├── label_dialog.py │ ├── label_list_widget.py │ ├── tool_bar.py │ ├── unique_label_qlist_widget.py │ └── zoom_widget.py ├── pyproject.toml ├── tests/ │ ├── conftest.py │ ├── e2e/ │ │ ├── __init__.py │ │ ├── annotation_test.py │ │ ├── config_test.py │ │ ├── conftest.py │ │ ├── file_loading_test.py │ │ ├── navigation_test.py │ │ └── smoke_test.py │ └── unit/ │ ├── __init__.py │ ├── _label_file_test.py │ ├── config_test.py │ ├── load_image_file_test.py │ ├── shape_contains_point_test.py │ ├── shape_test.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── image_test.py │ │ ├── qt_test.py │ │ ├── shape_test.py │ │ └── util.py │ └── widgets/ │ ├── __init__.py │ ├── canvas_test.py │ ├── label_dialog_test.py │ └── label_list_widget_test.py └── tools/ └── update_translate.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/CLAUDE.md ================================================ # Project Rules - use `uv` to run Python commands (e.g., `uv run python`, `uv run pytest`). - use `make test` to run tests. - use `make update_translate` to update translation files (`.ts` and `.qm`). ================================================ FILE: .git-blame-ignore-revs ================================================ # Format code with black 5c9808446a179176c721bd7a59d621b41c95898c # Mypy check # type: ignore 8a9cb1918d35c01abc804b40acb61f34bf8d4fc7 1f7ece25e26ed4e421a2dff3782217967f0443aa e63d1b8374f7b706a91c51e7ca9478a092a81bbb 6ba57e4a7467f73a6f388716c477639826186db6 4c0ef54052a7fd15715be8e8a2f5ba39fba4efaa 1b2f32ce683938d1ede0ecf956cd59e85cce0fba e83f3d68a69fe0a9a33c744fa428efb7a2a176b2 80f5a31723e6be3ae614e68a4aeaef8abec2162c 5e75e6cddf913e51ae413e58233c456bffba72ef 6a44111aded17402d03715f8b5d816a3ba19ba2f 34cff8833a442cfcea231e9bc5a719351f92427d cbb6d829725f531bdb3cb30bcb13213604a4fe31 9998f0042bcf5287625dbcee8e900c026474a0fe 39c9e2a979ebe52575e0e14be32ec5793e93e9fd 28dd12c1d641fe8b0b528eda9cab75a7fe0de7f6 50719a6268d9948e2bd08034e3ea7308d13c1bea 4aba96809ca188f1a13f353a843cdb09c631ad22 ================================================ FILE: .github/ISSUE_TEMPLATE/1.bug_report.yml ================================================ name: Bug Report description: Create a bug report labels: 'bug' body: - type: markdown attributes: value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible. - type: markdown attributes: value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions ["Q&A / Help" section](https://github.com/wkentaro/labelme/discussions/categories/q-a-help). - type: textarea attributes: label: Provide environment information description: Please run `which python; python --version; python -m pip list | grep labelme` in the root directory of your project and paste the results. validations: required: true - type: input attributes: label: What OS are you using? description: 'Please specify the exact version. For example: macOS 12.4, Ubuntu 20.04.4' validations: required: true - type: textarea attributes: label: Describe the Bug description: A clear and concise description of what the bug is. validations: required: true - type: textarea attributes: label: Expected Behavior description: A clear and concise description of what you expected to happen. - type: textarea attributes: label: To Reproduce description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. - type: markdown attributes: value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. - type: markdown attributes: value: Contributors should be able to follow the steps provided in order to reproduce the bug. - type: markdown attributes: value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance! ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Ideas / Feature request url: https://github.com/wkentaro/labelme/discussions/categories/ideas-feature-requests about: Share ideas for new features - name: Q&A / Help url: https://github.com/wkentaro/labelme/discussions/categories/q-a-help about: Ask the community for help - name: Show and tell url: https://github.com/wkentaro/labelme/discussions/categories/show-and-tell about: Show off something you've made ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: push: branches: - main pull_request: jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: "3.10" - uses: awalsh128/cache-apt-pkgs-action@v1 with: packages: qttools5-dev version: 1.0 - run: make setup - run: | make check build: runs-on: ${{ matrix.os }} strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: "3.10" - shell: bash run: | make setup - uses: awalsh128/cache-apt-pkgs-action@v1 if: matrix.os == 'ubuntu-latest' with: packages: xvfb libqt5widgets5 version: 1.0 - name: Test shell: bash if: matrix.os == 'ubuntu-latest' env: MPLBACKEND: 'agg' run: | Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & export DISPLAY=:99 make test - name: Run examples shell: bash if: matrix.os != 'windows-latest' env: MPLBACKEND: agg run: | labelme --help labelme --version (cd examples/primitives && ../tutorial/export_json.py primitives.json && rm -rf primitives) (cd examples/tutorial && rm -rf apc2016_obj3_json && ./export_json.py apc2016_obj3.json && python load_label_png.py && git checkout -- .) (cd examples/semantic_segmentation && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) (cd examples/instance_segmentation && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) (cd examples/video_annotation && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) uv pip install 'lxml<5.0.0' # for bbox_detection/labelme2voc.py (cd examples/bbox_detection && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) uv pip install cython && uv pip install pycocotools # for instance_segmentation/labelme2coco.py (cd examples/instance_segmentation && rm -rf data_dataset_coco && ./labelme2coco.py data_annotated data_dataset_coco --labels labels.txt && git checkout -- .) - name: Build wheel and install from it shell: bash run: | uv build uv pip install dist/labelme-*.whl ================================================ FILE: .github/workflows/cla.yml ================================================ name: CLA Assistant on: pull_request_target: types: [opened, synchronize] issue_comment: types: [created] jobs: cla: runs-on: ubuntu-latest steps: - name: CLA Assistant uses: contributor-assistant/github-action@v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} with: path-to-signatures: "signatures/version1/cla.json" path-to-document: "https://github.com/wkentaro/labelme/blob/main/CLA.md" branch: "cla-signatures" allowlist: wkentaro,bot* ================================================ FILE: .github/workflows/discord-notify.yml ================================================ name: discord-notify on: pull_request: types: [closed] branches: [main] jobs: notify: if: | github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'feature') runs-on: ubuntu-latest steps: - name: Send to Discord env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} run: | if [[ -z "$DISCORD_WEBHOOK" ]]; then echo "Error: DISCORD_WEBHOOK secret not configured" exit 1 fi TYPE="🎉 **New feature merged!** Try it in the next release" COLOR=3066993 MILESTONE="${{ github.event.pull_request.milestone.title }}" if [[ -z "$MILESTONE" ]]; then MILESTONE="None" fi TITLE_ESCAPED=$(echo "$PR_TITLE" | jq -Rs '.[:-1]') BODY_ESCAPED=$(echo "$PR_BODY" | jq -Rs '.[:-1] | if . == "" then "No description provided" elif length > 2000 then .[0:1997] + "..." else . end') curl -fsS -X POST "$DISCORD_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{ \"content\": \"$TYPE\", \"embeds\": [{ \"title\": $TITLE_ESCAPED, \"url\": \"${{ github.event.pull_request.html_url }}\", \"color\": $COLOR, \"fields\": [ { \"name\": \"Pull request\", \"value\": \"[#${{ github.event.pull_request.number }}](${{ github.event.pull_request.html_url }})\", \"inline\": true }, { \"name\": \"Milestone\", \"value\": \"$MILESTONE\", \"inline\": true }, { \"name\": \"💬 Feedback\", \"value\": \"[Share your thoughts](https://github.com/wkentaro/labelme/discussions)\", \"inline\": true } ], \"footer\": { \"text\": $BODY_ESCAPED } }] }" ================================================ FILE: .gitignore ================================================ /.cache/ /.pytest_cache/ /build/ /dist/ /*.egg-info/ *.py[cdo] .DS_Store .idea/ ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .python-version ================================================ 3.10 ================================================ FILE: CITATION.cff ================================================ cff-version: 1.2.0 message: "If you use this software, please cite it as below." authors: - family-names: "Wada" given-names: "Kentaro" orcid: "https://orcid.org/0000-0002-6347-5156" title: "Labelme: Image Polygonal Annotation with Python" doi: 10.5281/zenodo.5711226 url: "https://github.com/wkentaro/labelme" license: GPL-3 ================================================ FILE: CLA.md ================================================ # Contributor License Agreement (CLA) *Version 1.0 — March 2026* By submitting a pull request or otherwise contributing to this repository, you agree to the following terms: 1. **License Grant**: You grant Kentaro Wada a perpetual, worldwide, non-exclusive, irrevocable, royalty-free license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your contribution under any license terms Kentaro Wada chooses, including terms different from those of this project's open-source license. 2. **Representations**: You represent that (a) you are legally entitled to grant the above license, and (b) your contribution is your original work or you have the right to submit it under these terms. **How to sign:** When you open a pull request, the CLA Assistant bot will ask you to sign by commenting `I have read the CLA Document and I hereby sign the CLA`. Signing is required before your PR can be merged. ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: Makefile ================================================ ifneq ($(OS),Windows_NT) # On Unix-based systems, use ANSI codes BLUE = \033[36m BOLD_BLUE = \033[1;36m BOLD_GREEN = \033[1;32m RED = \033[31m YELLOW = \033[33m BOLD = \033[1m NC = \033[0m endif escape = $(subst $$,\$$,$(subst ",\",$(subst ',\',$(1)))) define exec @echo "$(BOLD_BLUE)$(call escape,$(1))$(NC)" @$(1) endef help: @echo "$(BOLD_GREEN)Available targets:$(NC)" @grep -E '^[a-zA-Z_-].+:.*?# .*$$' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS = ":.*?# "}; \ {printf " $(BOLD_BLUE)%-20s$(NC) %s\n", $$1, $$2}' PACKAGE_NAME:=labelme setup: # Setup the development environment $(call exec,uv sync) format: # Format code $(call exec,uv run ruff format) $(call exec,uv run ruff check --fix) lint: $(call exec,uv run ruff format --check) $(call exec,uv run ruff check) $(call exec,uv run ty check --no-progress) check_translate: update_translate $(call exec,git diff --exit-code labelme/translate) @if grep -r 'type="unfinished"' labelme/translate/*.ts; then \ echo "$(RED)Error: unfinished translations found$(NC)"; \ exit 1; \ fi check: lint check_translate # Run checks test: # Run tests $(call exec,uv run pytest -v tests/) update_translate: $(call exec,uv run --no-sync tools/update_translate.py) ================================================ FILE: README.md ================================================


labelme

Image annotation with Python.


## Description Labelme is a graphical image annotation tool inspired by . It is written in Python and uses Qt for its graphical interface. > Looking for a simple install without Python or Qt? Get the standalone app at **[labelme.io](https://labelme.io)**. VOC dataset example of instance segmentation. Other examples (semantic segmentation, bbox detection, and classification). Various primitives (polygon, rectangle, circle, line, and point).

Multi-language support (English, 中文, 日本語, 한국어, Deutsch, Français, and more).

## Features - [x] Image annotation for polygon, rectangle, circle, line and point ([tutorial](examples/tutorial)) - [x] Image flag annotation for classification and cleaning ([#166](https://github.com/wkentaro/labelme/pull/166)) - [x] Video annotation ([video annotation](examples/video_annotation)) - [x] GUI customization (predefined labels / flags, auto-saving, label validation, etc) ([#144](https://github.com/wkentaro/labelme/pull/144)) - [x] Exporting VOC-format dataset for [semantic segmentation](examples/semantic_segmentation), [instance segmentation](examples/instance_segmentation) - [x] Exporting COCO-format dataset for [instance segmentation](examples/instance_segmentation) - [x] AI-assisted point-to-polygon/mask annotation by SAM, EfficientSAM models - [x] AI text-to-annotation by YOLO-world, SAM3 models **🌏 Available in 16 languages** - English · 日本語 · 한국어 · 简体中文 · 繁體中文 · Deutsch · Français · Español · Italiano · Português · Nederlands · Magyar · Tiếng Việt · Türkçe · Polski · فارسی (`LANG=ja_JP.UTF-8 labelme`) ## Installation There are 3 options to install labelme: ### Option 1: Using pip For more detail, check ["Install Labelme using Terminal"](https://www.labelme.io/docs/install-labelme-terminal) ```bash pip install labelme # To install the latest version from GitHub: # pip install git+https://github.com/wkentaro/labelme.git ``` ### Option 2: Using standalone executable (Easiest) If you're willing to invest in the convenience of simple installation without any dependencies (Python, Qt), you can download the standalone executable from ["Install Labelme as App"](https://www.labelme.io/docs/install-labelme-app). It's a one-time payment for lifetime access, and it helps us to maintain this project. ### Option 3: Using a package manager in each Linux distribution In some Linux distributions, you can install labelme via their package managers (e.g., apt, pacman). The following systems are currently available: [![Packaging status](https://repology.org/badge/vertical-allrepos/labelme.svg)](https://repology.org/project/labelme/versions) ## Usage Run `labelme --help` for detail. The annotations are saved as a [JSON](http://www.json.org/) file. ```bash labelme # just open gui # tutorial (single image example) cd examples/tutorial labelme apc2016_obj3.jpg # specify image file labelme apc2016_obj3.jpg --output annotations/ # save annotation JSON files to a directory labelme apc2016_obj3.jpg --with-image-data # include image data in JSON file labelme apc2016_obj3.jpg \ --labels highland_6539_self_stick_notes,mead_index_cards,kong_air_dog_squeakair_tennis_ball # specify label list # semantic segmentation example cd examples/semantic_segmentation labelme data_annotated/ # Open directory to annotate all images in it labelme data_annotated/ --labels labels.txt # specify label list with a file ``` ### Command Line Arguments - `--output` specifies the location that annotations will be written to. If the location ends with .json, a single annotation will be written to this file. Only one image can be annotated if a location is specified with .json. If the location does not end with .json, the program will assume it is a directory. Annotations will be stored in this directory with a name that corresponds to the image that the annotation was made on. - The first time you run labelme, it will create a config file at `~/.labelmerc`. Add only the settings you want to override. For all available options and their defaults, see [`default_config.yaml`](labelme/config/default_config.yaml). If you would prefer to use a config file from another location, you can specify this file with the `--config` flag. - Without the `--nosortlabels` flag, the program will list labels in alphabetical order. When the program is run with this flag, it will display labels in the order that they are provided. - Flags are assigned to an entire image. [Example](examples/classification) - Labels are assigned to a single polygon. [Example](examples/bbox_detection) ### FAQ - **How to convert JSON file to numpy array?** See [examples/tutorial](examples/tutorial#convert-to-dataset). - **How to load label PNG file?** See [examples/tutorial](examples/tutorial#how-to-load-label-png-file). - **How to get annotations for semantic segmentation?** See [examples/semantic_segmentation](examples/semantic_segmentation). - **How to get annotations for instance segmentation?** See [examples/instance_segmentation](examples/instance_segmentation). ## Examples * [Image Classification](examples/classification) * [Bounding Box Detection](examples/bbox_detection) * [Semantic Segmentation](examples/semantic_segmentation) * [Instance Segmentation](examples/instance_segmentation) * [Video Annotation](examples/video_annotation) ## How to build standalone executable ```bash LABELME_PATH=./labelme OSAM_PATH=$(python -c 'import os, osam; print(os.path.dirname(osam.__file__))') pip install 'numpy<2.0' # numpy>=2.0 causes build errors (see #1532) pyinstaller labelme/labelme/__main__.py \ --name=Labelme \ --windowed \ --noconfirm \ --specpath=build \ --add-data=$(OSAM_PATH)/_models/yoloworld/clip/bpe_simple_vocab_16e6.txt.gz:osam/_models/yoloworld/clip \ --add-data=$(LABELME_PATH)/config/default_config.yaml:labelme/config \ --add-data=$(LABELME_PATH)/icons/*:labelme/icons \ --add-data=$(LABELME_PATH)/translate/*:translate \ --icon=$(LABELME_PATH)/icons/icon-256.png \ --onedir ``` ## Acknowledgement This repo is the fork of [mpitid/pylabelme](https://github.com/mpitid/pylabelme). ================================================ FILE: examples/bbox_detection/README.md ================================================ # Bounding Box Detection Example ## Usage ```bash labelme data_annotated --labels labels.txt ``` ![](.readme/annotation.jpg) ## Convert to VOC-format Dataset ```bash # It generates: # - data_dataset_voc/JPEGImages # - data_dataset_voc/Annotations # - data_dataset_voc/AnnotationsVisualization ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt ``` Fig1. JPEG image (left), Bounding box annotation visualization (right). ================================================ FILE: examples/bbox_detection/data_annotated/2011_000003.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "person", "points": [ [ 191.0, 107.36900369003689 ], [ 313.0, 329.36900369003695 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} }, { "label": "person", "points": [ [ 365.0, 83.0 ], [ 500.0, 333.0 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} } ], "imagePath": "2011_000003.jpg", "imageData": null, "imageHeight": 338, "imageWidth": 500 } ================================================ FILE: examples/bbox_detection/data_annotated/2011_000006.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "person", "points": [ [ 91.0, 107.0 ], [ 240.0, 330.0 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} }, { "label": "person", "points": [ [ 178.0, 110.0 ], [ 298.0, 282.0 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} }, { "label": "person", "points": [ [ 254.38461538461536, 115.38461538461539 ], [ 369.38461538461536, 292.38461538461536 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} }, { "label": "person", "points": [ [ 395.0, 81.0 ], [ 447.0, 117.0 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} } ], "imagePath": "2011_000006.jpg", "imageData": null, "imageHeight": 375, "imageWidth": 500 } ================================================ FILE: examples/bbox_detection/data_annotated/2011_000025.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "bus", "points": [ [ 84.0, 20.384615384615387 ], [ 435.0, 373.38461538461536 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} }, { "label": "bus", "points": [ [ 1.0, 99.0 ], [ 107.0, 282.0 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} }, { "label": "car", "points": [ [ 409.0, 167.0 ], [ 500.0, 266.0 ] ], "group_id": null, "shape_type": "rectangle", "flags": {} } ], "imagePath": "2011_000025.jpg", "imageData": null, "imageHeight": 375, "imageWidth": 500 } ================================================ FILE: examples/bbox_detection/data_dataset_voc/Annotations/2011_000003.xml ================================================ 2011_000003.jpg 338 500 3 person 191.0 107.36900369003689 313.0 329.36900369003695 person 365.0 83.0 500.0 333.0 ================================================ FILE: examples/bbox_detection/data_dataset_voc/Annotations/2011_000006.xml ================================================ 2011_000006.jpg 375 500 3 person 91.0 107.0 240.0 330.0 person 178.0 110.0 298.0 282.0 person 254.38461538461536 115.38461538461539 369.38461538461536 292.38461538461536 person 395.0 81.0 447.0 117.0 ================================================ FILE: examples/bbox_detection/data_dataset_voc/Annotations/2011_000025.xml ================================================ 2011_000025.jpg 375 500 3 bus 84.0 20.384615384615387 435.0 373.38461538461536 bus 1.0 99.0 107.0 282.0 car 409.0 167.0 500.0 266.0 ================================================ FILE: examples/bbox_detection/data_dataset_voc/class_names.txt ================================================ _background_ aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person potted plant sheep sofa train tv/monitor ================================================ FILE: examples/bbox_detection/labelme2voc.py ================================================ #!/usr/bin/env python import argparse import glob import os import os.path as osp import sys import imgviz import labelme try: import lxml.builder # type: ignore import lxml.etree # type: ignore except ImportError: print("Please install lxml:\n\n pip install lxml\n") sys.exit(1) def main(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("input_dir", help="input annotated directory") parser.add_argument("output_dir", help="output dataset directory") parser.add_argument("--labels", help="labels file", required=True) parser.add_argument("--noviz", help="no visualization", action="store_true") args = parser.parse_args() if osp.exists(args.output_dir): print("Output directory already exists:", args.output_dir) sys.exit(1) os.makedirs(args.output_dir) os.makedirs(osp.join(args.output_dir, "JPEGImages")) os.makedirs(osp.join(args.output_dir, "Annotations")) if not args.noviz: os.makedirs(osp.join(args.output_dir, "AnnotationsVisualization")) print("Creating dataset:", args.output_dir) class_names = [] class_name_to_id = {} for i, line in enumerate(open(args.labels).readlines()): class_id = i - 1 # starts with -1 class_name = line.strip() class_name_to_id[class_name] = class_id if class_id == -1: assert class_name == "__ignore__" continue elif class_id == 0: assert class_name == "_background_" class_names.append(class_name) class_names = tuple(class_names) print("class_names:", class_names) out_class_names_file = osp.join(args.output_dir, "class_names.txt") with open(out_class_names_file, "w") as f: f.writelines("\n".join(class_names)) print("Saved class_names:", out_class_names_file) for filename in glob.glob(osp.join(args.input_dir, "*.json")): print("Generating dataset from:", filename) label_file = labelme.LabelFile(filename=filename) base = osp.splitext(osp.basename(filename))[0] out_img_file = osp.join(args.output_dir, "JPEGImages", f"{base}.jpg") out_xml_file = osp.join(args.output_dir, "Annotations", f"{base}.xml") if not args.noviz: out_viz_file = osp.join( args.output_dir, "AnnotationsVisualization", f"{base}.jpg" ) img = labelme.utils.img_data_to_arr(label_file.imageData) imgviz.io.imsave(out_img_file, img) maker = lxml.builder.ElementMaker() xml = maker.annotation( maker.folder(), maker.filename(f"{base}.jpg"), maker.database(), # e.g., The VOC2007 Database maker.annotation(), # e.g., Pascal VOC2007 maker.image(), # e.g., flickr maker.size( maker.height(str(img.shape[0])), maker.width(str(img.shape[1])), maker.depth(str(img.shape[2])), ), maker.segmented(), ) bboxes: list[list[float]] = [] labels = [] for shape in label_file.shapes: if shape["shape_type"] != "rectangle": print( "Skipping shape: label={label}, shape_type={shape_type}".format( **shape ) ) continue class_name = shape["label"] class_id = class_names.index(class_name) (xmin, ymin), (xmax, ymax) = shape["points"] # swap if min is larger than max. xmin, xmax = sorted([xmin, xmax]) ymin, ymax = sorted([ymin, ymax]) bboxes.append([ymin, xmin, ymax, xmax]) labels.append(class_id) xml.append( maker.object( maker.name(shape["label"]), maker.pose(), maker.truncated(), maker.difficult(), maker.bndbox( maker.xmin(str(xmin)), maker.ymin(str(ymin)), maker.xmax(str(xmax)), maker.ymax(str(ymax)), ), ) ) if not args.noviz: captions: list[str] = [class_names[label] for label in labels] viz = imgviz.instances2rgb( image=img, labels=labels, bboxes=bboxes, captions=captions, font_size=15, ) imgviz.io.imsave(out_viz_file, viz) with open(out_xml_file, "wb") as f: f.write(lxml.etree.tostring(xml, pretty_print=True)) if __name__ == "__main__": main() ================================================ FILE: examples/bbox_detection/labels.txt ================================================ __ignore__ _background_ aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person potted plant sheep sofa train tv/monitor ================================================ FILE: examples/classification/README.md ================================================ # Classification Example ## Usage ```bash labelme data_annotated --flags flags.txt ``` ================================================ FILE: examples/classification/data_annotated/0001.json ================================================ { "version": "4.0.0", "flags": { "__ignore__": false, "cat": true, "dog": false }, "shapes": [], "imagePath": "0001.jpg", "imageData": null, "imageHeight": 480, "imageWidth": 640 } ================================================ FILE: examples/classification/data_annotated/0002.json ================================================ { "version": "4.0.0", "flags": { "__ignore__": false, "cat": false, "dog": true }, "shapes": [], "imagePath": "0002.jpg", "imageData": null, "imageHeight": 480, "imageWidth": 640 } ================================================ FILE: examples/classification/flags.txt ================================================ __ignore__ cat dog ================================================ FILE: examples/instance_segmentation/README.md ================================================ # Instance Segmentation Example ## Annotation ```bash labelme data_annotated --labels labels.txt --validatelabel exact --config '{shift_auto_shape_color: -2}' labelme data_annotated --labels labels.txt --labelflags '{.*: [occluded, truncated], person: [male]}' ``` ![](.readme/annotation.jpg) ## Convert to VOC-format Dataset ```bash # It generates: # - data_dataset_voc/JPEGImages # - data_dataset_voc/SegmentationClass # - data_dataset_voc/SegmentationClassNpy # - data_dataset_voc/SegmentationClassVisualization # - data_dataset_voc/SegmentationObject # - data_dataset_voc/SegmentationObjectNpy # - data_dataset_voc/SegmentationObjectVisualization ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt ``` Fig 1. JPEG image (left), JPEG class label visualization (center), JPEG instance label visualization (right) Note that the label file contains only very low label values (ex. `0, 4, 14`), and `255` indicates the `__ignore__` label value (`-1` in the npy file). You can see the label PNG file by following. ```bash ../tutorial/draw_label_png.py data_dataset_voc/SegmentationClass/2011_000003.png # left ../tutorial/draw_label_png.py data_dataset_voc/SegmentationObject/2011_000003.png # right ``` ## Convert to COCO-format Dataset ```bash # It generates: # - data_dataset_coco/JPEGImages # - data_dataset_coco/annotations.json ./labelme2coco.py data_annotated data_dataset_coco --labels labels.txt ``` ================================================ FILE: examples/instance_segmentation/data_annotated/2011_000003.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "person", "points": [ [ 250.8142292490119, 107.33596837944665 ], [ 229.8142292490119, 119.33596837944665 ], [ 221.8142292490119, 135.33596837944665 ], [ 223.8142292490119, 148.33596837944665 ], [ 217.8142292490119, 161.33596837944665 ], [ 202.8142292490119, 168.33596837944665 ], [ 192.8142292490119, 200.33596837944665 ], [ 194.8142292490119, 222.33596837944665 ], [ 199.8142292490119, 227.33596837944665 ], [ 191.8142292490119, 234.33596837944665 ], [ 197.8142292490119, 264.3359683794467 ], [ 213.8142292490119, 295.3359683794467 ], [ 214.8142292490119, 320.3359683794467 ], [ 221.8142292490119, 327.3359683794467 ], [ 235.8142292490119, 326.3359683794467 ], [ 240.8142292490119, 323.3359683794467 ], [ 235.8142292490119, 298.3359683794467 ], [ 238.8142292490119, 287.3359683794467 ], [ 234.8142292490119, 268.3359683794467 ], [ 257.81422924901193, 258.3359683794467 ], [ 264.81422924901193, 264.3359683794467 ], [ 256.81422924901193, 273.3359683794467 ], [ 259.81422924901193, 282.3359683794467 ], [ 284.81422924901193, 288.3359683794467 ], [ 297.81422924901193, 278.3359683794467 ], [ 288.81422924901193, 270.3359683794467 ], [ 281.81422924901193, 270.3359683794467 ], [ 283.81422924901193, 264.3359683794467 ], [ 292.81422924901193, 261.3359683794467 ], [ 308.81422924901193, 236.33596837944665 ], [ 313.81422924901193, 217.33596837944665 ], [ 309.81422924901193, 208.33596837944665 ], [ 312.81422924901193, 202.33596837944665 ], [ 308.81422924901193, 185.33596837944665 ], [ 291.81422924901193, 173.33596837944665 ], [ 269.81422924901193, 159.33596837944665 ], [ 261.81422924901193, 154.33596837944665 ], [ 264.81422924901193, 142.33596837944665 ], [ 273.81422924901193, 137.33596837944665 ], [ 278.81422924901193, 130.33596837944665 ], [ 270.81422924901193, 121.33596837944665 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 482.81422924901193, 87.18098682963114 ], [ 468.81422924901193, 92.18098682963114 ], [ 460.81422924901193, 112.18098682963114 ], [ 460.81422924901193, 129.18098682963114 ], [ 444.81422924901193, 139.18098682963114 ], [ 419.81422924901193, 155.18098682963114 ], [ 410.81422924901193, 165.18098682963114 ], [ 403.81422924901193, 170.18098682963114 ], [ 394.81422924901193, 172.18098682963114 ], [ 386.81422924901193, 170.18098682963114 ], [ 386.81422924901193, 186.18098682963114 ], [ 392.81422924901193, 184.18098682963114 ], [ 410.81422924901193, 189.18098682963114 ], [ 414.81422924901193, 194.18098682963114 ], [ 437.81422924901193, 191.18098682963114 ], [ 434.81422924901193, 206.18098682963114 ], [ 390.81422924901193, 197.18098682963114 ], [ 386.81422924901193, 197.18098682963114 ], [ 387.81422924901193, 210.18098682963114 ], [ 381.81422924901193, 214.18098682963114 ], [ 372.81422924901193, 214.18098682963114 ], [ 372.81422924901193, 218.18098682963114 ], [ 400.81422924901193, 272.18098682963114 ], [ 389.81422924901193, 274.18098682963114 ], [ 389.81422924901193, 276.18098682963114 ], [ 403.81422924901193, 284.18098682963114 ], [ 444.81422924901193, 285.18098682963114 ], [ 443.81422924901193, 261.18098682963114 ], [ 426.81422924901193, 246.18098682963114 ], [ 462.81422924901193, 258.18098682963114 ], [ 474.81422924901193, 272.18098682963114 ], [ 477.81422924901193, 282.18098682963114 ], [ 473.81422924901193, 291.18098682963114 ], [ 471.81422924901193, 298.18098682963114 ], [ 472.81422924901193, 319.18098682963114 ], [ 480.81422924901193, 334.18098682963114 ], [ 494.81422924901193, 337.18098682963114 ], [ 498.81422924901193, 331.18098682963114 ], [ 494.81422924901193, 310.18098682963114 ], [ 499.81422924901193, 299.18098682963114 ], [ 499.81422924901193, 92.18098682963114 ] ], "group_id": 0, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 370.81422924901193, 170.33596837944665 ], [ 366.81422924901193, 173.33596837944665 ], [ 365.81422924901193, 182.33596837944665 ], [ 368.81422924901193, 185.33596837944665 ] ], "group_id": 0, "shape_type": "polygon", "flags": {} }, { "label": "bottle", "points": [ [ 374.81422924901193, 159.33596837944665 ], [ 369.81422924901193, 170.33596837944665 ], [ 369.81422924901193, 210.33596837944665 ], [ 375.81422924901193, 212.33596837944665 ], [ 387.81422924901193, 209.33596837944665 ], [ 385.81422924901193, 185.33596837944665 ], [ 385.81422924901193, 168.33596837944665 ], [ 385.81422924901193, 165.33596837944665 ], [ 382.81422924901193, 159.33596837944665 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "__ignore__", "points": [ [ 338.81422924901193, 266.3359683794467 ], [ 313.81422924901193, 269.3359683794467 ], [ 297.81422924901193, 277.3359683794467 ], [ 282.81422924901193, 288.3359683794467 ], [ 273.81422924901193, 302.3359683794467 ], [ 272.81422924901193, 320.3359683794467 ], [ 279.81422924901193, 337.3359683794467 ], [ 428.81422924901193, 337.3359683794467 ], [ 432.81422924901193, 316.3359683794467 ], [ 423.81422924901193, 296.3359683794467 ], [ 403.81422924901193, 283.3359683794467 ], [ 370.81422924901193, 270.3359683794467 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "2011_000003.jpg", "imageData": null, "imageHeight": 338, "imageWidth": 500 } ================================================ FILE: examples/instance_segmentation/data_annotated/2011_000006.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "person", "points": [ [ 204.936170212766, 108.56382978723406 ], [ 183.936170212766, 141.56382978723406 ], [ 166.936170212766, 150.56382978723406 ], [ 108.93617021276599, 203.56382978723406 ], [ 92.93617021276599, 228.56382978723406 ], [ 95.93617021276599, 244.56382978723406 ], [ 105.93617021276599, 244.56382978723406 ], [ 116.93617021276599, 223.56382978723406 ], [ 163.936170212766, 187.56382978723406 ], [ 147.936170212766, 212.56382978723406 ], [ 117.93617021276599, 222.56382978723406 ], [ 108.93617021276599, 243.56382978723406 ], [ 100.93617021276599, 325.56382978723406 ], [ 135.936170212766, 329.56382978723406 ], [ 148.936170212766, 319.56382978723406 ], [ 150.936170212766, 295.56382978723406 ], [ 169.936170212766, 272.56382978723406 ], [ 171.936170212766, 249.56382978723406 ], [ 178.936170212766, 246.56382978723406 ], [ 186.936170212766, 225.56382978723406 ], [ 214.936170212766, 219.56382978723406 ], [ 242.936170212766, 157.56382978723406 ], [ 228.936170212766, 146.56382978723406 ], [ 228.936170212766, 125.56382978723406 ], [ 216.936170212766, 112.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 271.936170212766, 109.56382978723406 ], [ 249.936170212766, 110.56382978723406 ], [ 244.936170212766, 150.56382978723406 ], [ 215.936170212766, 219.56382978723406 ], [ 208.936170212766, 245.56382978723406 ], [ 214.936170212766, 220.56382978723406 ], [ 188.936170212766, 227.56382978723406 ], [ 170.936170212766, 246.56382978723406 ], [ 170.936170212766, 275.56382978723406 ], [ 221.936170212766, 278.56382978723406 ], [ 233.936170212766, 259.56382978723406 ], [ 246.936170212766, 253.56382978723406 ], [ 245.936170212766, 256.56382978723406 ], [ 242.936170212766, 251.56382978723406 ], [ 262.936170212766, 256.56382978723406 ], [ 304.936170212766, 226.56382978723406 ], [ 297.936170212766, 199.56382978723406 ], [ 308.936170212766, 164.56382978723406 ], [ 296.936170212766, 148.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 308.936170212766, 115.56382978723406 ], [ 298.936170212766, 145.56382978723406 ], [ 309.936170212766, 166.56382978723406 ], [ 297.936170212766, 200.56382978723406 ], [ 305.936170212766, 228.56382978723406 ], [ 262.936170212766, 258.56382978723406 ], [ 252.936170212766, 284.56382978723406 ], [ 272.936170212766, 291.56382978723406 ], [ 281.936170212766, 250.56382978723406 ], [ 326.936170212766, 235.56382978723406 ], [ 351.936170212766, 239.56382978723406 ], [ 365.936170212766, 223.56382978723406 ], [ 371.936170212766, 187.56382978723406 ], [ 353.936170212766, 168.56382978723406 ], [ 344.936170212766, 143.56382978723406 ], [ 336.936170212766, 115.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "chair", "points": [ [ 309.7054009819968, 242.94844517184941 ], [ 282.7054009819968, 251.94844517184941 ], [ 271.7054009819968, 287.9484451718494 ], [ 175.70540098199677, 275.9484451718494 ], [ 149.70540098199677, 296.9484451718494 ], [ 151.70540098199677, 319.9484451718494 ], [ 160.70540098199677, 328.9484451718494 ], [ 165.54250204582655, 375.38461538461536 ], [ 486.7054009819968, 373.9484451718494 ], [ 498.7054009819968, 336.9484451718494 ], [ 498.7054009819968, 202.94844517184941 ], [ 454.7054009819968, 193.94844517184941 ], [ 435.7054009819968, 212.94844517184941 ], [ 368.7054009819968, 224.94844517184941 ], [ 351.7054009819968, 241.94844517184941 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 425.936170212766, 82.56382978723406 ], [ 404.936170212766, 109.56382978723406 ], [ 400.936170212766, 114.56382978723406 ], [ 437.936170212766, 114.56382978723406 ], [ 448.936170212766, 102.56382978723406 ], [ 446.936170212766, 91.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "__ignore__", "points": [ [ 457.936170212766, 85.56382978723406 ], [ 439.936170212766, 117.56382978723406 ], [ 477.936170212766, 117.56382978723406 ], [ 474.936170212766, 87.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 183.936170212766, 140.56382978723406 ], [ 125.93617021276599, 140.56382978723406 ], [ 110.93617021276599, 187.56382978723406 ], [ 22.936170212765987, 199.56382978723406 ], [ 18.936170212765987, 218.56382978723406 ], [ 22.936170212765987, 234.56382978723406 ], [ 93.93617021276599, 239.56382978723406 ], [ 91.93617021276599, 229.56382978723406 ], [ 110.93617021276599, 203.56382978723406 ] ], "group_id": 0, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 103.93617021276599, 290.56382978723406 ], [ 58.93617021276599, 303.56382978723406 ], [ 97.93617021276599, 311.56382978723406 ] ], "group_id": 0, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 348.936170212766, 146.56382978723406 ], [ 472.936170212766, 149.56382978723406 ], [ 477.936170212766, 162.56382978723406 ], [ 471.936170212766, 196.56382978723406 ], [ 453.936170212766, 192.56382978723406 ], [ 434.936170212766, 213.56382978723406 ], [ 368.936170212766, 226.56382978723406 ], [ 375.936170212766, 187.56382978723406 ], [ 353.936170212766, 164.56382978723406 ] ], "group_id": 0, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 246.936170212766, 252.56382978723406 ], [ 219.936170212766, 277.56382978723406 ], [ 254.936170212766, 287.56382978723406 ], [ 261.936170212766, 256.56382978723406 ] ], "group_id": 0, "shape_type": "polygon", "flags": {} } ], "imagePath": "2011_000006.jpg", "imageData": null, "imageHeight": 375, "imageWidth": 500 } ================================================ FILE: examples/instance_segmentation/data_annotated/2011_000025.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "bus", "points": [ [ 260.936170212766, 23.33306055646483 ], [ 193.936170212766, 20.33306055646483 ], [ 124.93617021276599, 40.33306055646483 ], [ 89.93617021276599, 102.33306055646483 ], [ 81.93617021276599, 151.33306055646483 ], [ 108.93617021276599, 146.33306055646483 ], [ 88.93617021276599, 245.33306055646483 ], [ 89.93617021276599, 323.33306055646483 ], [ 116.93617021276599, 368.33306055646483 ], [ 158.936170212766, 369.33306055646483 ], [ 165.936170212766, 338.33306055646483 ], [ 347.936170212766, 336.33306055646483 ], [ 349.936170212766, 370.33306055646483 ], [ 391.936170212766, 374.33306055646483 ], [ 403.936170212766, 336.33306055646483 ], [ 425.936170212766, 333.33306055646483 ], [ 421.936170212766, 282.33306055646483 ], [ 428.936170212766, 253.33306055646483 ], [ 428.936170212766, 237.33306055646483 ], [ 409.936170212766, 221.33306055646483 ], [ 409.936170212766, 151.33306055646483 ], [ 430.936170212766, 144.33306055646483 ], [ 433.936170212766, 113.33306055646483 ], [ 431.936170212766, 97.33306055646483 ], [ 408.936170212766, 91.33306055646483 ], [ 395.936170212766, 51.33306055646483 ], [ 338.936170212766, 26.33306055646483 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "bus", "points": [ [ 88.93617021276599, 115.56382978723406 ], [ 0.9361702127659877, 96.56382978723406 ], [ 0.0, 251.968085106388 ], [ 0.9361702127659877, 265.56382978723406 ], [ 27.936170212765987, 265.56382978723406 ], [ 29.936170212765987, 283.56382978723406 ], [ 63.93617021276599, 281.56382978723406 ], [ 89.93617021276599, 252.56382978723406 ], [ 100.93617021276599, 183.56382978723406 ], [ 108.93617021276599, 145.56382978723406 ], [ 81.93617021276599, 151.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "car", "points": [ [ 413.936170212766, 168.94844517184944 ], [ 497.936170212766, 168.94844517184944 ], [ 497.936170212766, 256.94844517184936 ], [ 431.936170212766, 258.94844517184936 ], [ 430.936170212766, 236.94844517184944 ], [ 408.936170212766, 218.94844517184944 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "2011_000025.jpg", "imageData": null, "imageHeight": 375, "imageWidth": 500 } ================================================ FILE: examples/instance_segmentation/data_dataset_coco/annotations.json ================================================ {"info": {"description": null, "url": null, "version": null, "year": 2020, "contributor": null, "date_created": "2020-01-26 05:46:30.244442"}, "licenses": [{"url": null, "id": 0, "name": null}], "images": [{"license": 0, "url": null, "file_name": "JPEGImages/2011_000003.jpg", "height": 338, "width": 500, "date_captured": null, "id": 0}, {"license": 0, "url": null, "file_name": "JPEGImages/2011_000025.jpg", "height": 375, "width": 500, "date_captured": null, "id": 1}, {"license": 0, "url": null, "file_name": "JPEGImages/2011_000006.jpg", "height": 375, "width": 500, "date_captured": null, "id": 2}], "type": "instances", "annotations": [{"id": 0, "image_id": 0, "category_id": 15, "segmentation": [[250.8142292490119, 107.33596837944665, 229.8142292490119, 119.33596837944665, 221.8142292490119, 135.33596837944665, 223.8142292490119, 148.33596837944665, 217.8142292490119, 161.33596837944665, 202.8142292490119, 168.33596837944665, 192.8142292490119, 200.33596837944665, 194.8142292490119, 222.33596837944665, 199.8142292490119, 227.33596837944665, 191.8142292490119, 234.33596837944665, 197.8142292490119, 264.3359683794467, 213.8142292490119, 295.3359683794467, 214.8142292490119, 320.3359683794467, 221.8142292490119, 327.3359683794467, 235.8142292490119, 326.3359683794467, 240.8142292490119, 323.3359683794467, 235.8142292490119, 298.3359683794467, 238.8142292490119, 287.3359683794467, 234.8142292490119, 268.3359683794467, 257.81422924901193, 258.3359683794467, 264.81422924901193, 264.3359683794467, 256.81422924901193, 273.3359683794467, 259.81422924901193, 282.3359683794467, 284.81422924901193, 288.3359683794467, 297.81422924901193, 278.3359683794467, 288.81422924901193, 270.3359683794467, 281.81422924901193, 270.3359683794467, 283.81422924901193, 264.3359683794467, 292.81422924901193, 261.3359683794467, 308.81422924901193, 236.33596837944665, 313.81422924901193, 217.33596837944665, 309.81422924901193, 208.33596837944665, 312.81422924901193, 202.33596837944665, 308.81422924901193, 185.33596837944665, 291.81422924901193, 173.33596837944665, 269.81422924901193, 159.33596837944665, 261.81422924901193, 154.33596837944665, 264.81422924901193, 142.33596837944665, 273.81422924901193, 137.33596837944665, 278.81422924901193, 130.33596837944665, 270.81422924901193, 121.33596837944665]], "area": 15689.0, "bbox": [191.0, 107.0, 123.0, 221.0], "iscrowd": 0}, {"id": 1, "image_id": 0, "category_id": 15, "segmentation": [[482.81422924901193, 87.18098682963114, 468.81422924901193, 92.18098682963114, 460.81422924901193, 112.18098682963114, 460.81422924901193, 129.18098682963114, 444.81422924901193, 139.18098682963114, 419.81422924901193, 155.18098682963114, 410.81422924901193, 165.18098682963114, 403.81422924901193, 170.18098682963114, 394.81422924901193, 172.18098682963114, 386.81422924901193, 170.18098682963114, 386.81422924901193, 186.18098682963114, 392.81422924901193, 184.18098682963114, 410.81422924901193, 189.18098682963114, 414.81422924901193, 194.18098682963114, 437.81422924901193, 191.18098682963114, 434.81422924901193, 206.18098682963114, 390.81422924901193, 197.18098682963114, 386.81422924901193, 197.18098682963114, 387.81422924901193, 210.18098682963114, 381.81422924901193, 214.18098682963114, 372.81422924901193, 214.18098682963114, 372.81422924901193, 218.18098682963114, 400.81422924901193, 272.18098682963114, 389.81422924901193, 274.18098682963114, 389.81422924901193, 276.18098682963114, 403.81422924901193, 284.18098682963114, 444.81422924901193, 285.18098682963114, 443.81422924901193, 261.18098682963114, 426.81422924901193, 246.18098682963114, 462.81422924901193, 258.18098682963114, 474.81422924901193, 272.18098682963114, 477.81422924901193, 282.18098682963114, 473.81422924901193, 291.18098682963114, 471.81422924901193, 298.18098682963114, 472.81422924901193, 319.18098682963114, 480.81422924901193, 334.18098682963114, 494.81422924901193, 337.18098682963114, 498.81422924901193, 331.18098682963114, 494.81422924901193, 310.18098682963114, 499.81422924901193, 299.18098682963114, 499.81422924901193, 92.18098682963114], [370.81422924901193, 170.33596837944665, 366.81422924901193, 173.33596837944665, 365.81422924901193, 182.33596837944665, 368.81422924901193, 185.33596837944665]], "area": 17254.0, "bbox": [365.0, 87.0, 135.0, 251.0], "iscrowd": 0}, {"id": 2, "image_id": 0, "category_id": 5, "segmentation": [[374.81422924901193, 159.33596837944665, 369.81422924901193, 170.33596837944665, 369.81422924901193, 210.33596837944665, 375.81422924901193, 212.33596837944665, 387.81422924901193, 209.33596837944665, 385.81422924901193, 185.33596837944665, 385.81422924901193, 168.33596837944665, 385.81422924901193, 165.33596837944665, 382.81422924901193, 159.33596837944665]], "area": 873.0, "bbox": [369.0, 159.0, 19.0, 54.0], "iscrowd": 0}, {"id": 3, "image_id": 1, "category_id": 6, "segmentation": [[260.936170212766, 23.33306055646483, 193.936170212766, 20.33306055646483, 124.93617021276599, 40.33306055646483, 89.93617021276599, 102.33306055646483, 81.93617021276599, 151.33306055646483, 108.93617021276599, 146.33306055646483, 88.93617021276599, 245.33306055646483, 89.93617021276599, 323.33306055646483, 116.93617021276599, 368.33306055646483, 158.936170212766, 369.33306055646483, 165.936170212766, 338.33306055646483, 347.936170212766, 336.33306055646483, 349.936170212766, 370.33306055646483, 391.936170212766, 374.33306055646483, 403.936170212766, 336.33306055646483, 425.936170212766, 333.33306055646483, 421.936170212766, 282.33306055646483, 428.936170212766, 253.33306055646483, 428.936170212766, 237.33306055646483, 409.936170212766, 221.33306055646483, 409.936170212766, 151.33306055646483, 430.936170212766, 144.33306055646483, 433.936170212766, 113.33306055646483, 431.936170212766, 97.33306055646483, 408.936170212766, 91.33306055646483, 395.936170212766, 51.33306055646483, 338.936170212766, 26.33306055646483]], "area": 102701.0, "bbox": [81.0, 20.0, 353.0, 355.0], "iscrowd": 0}, {"id": 4, "image_id": 1, "category_id": 6, "segmentation": [[88.93617021276599, 115.56382978723406, 0.9361702127659877, 96.56382978723406, 0.0, 251.968085106388, 0.9361702127659877, 265.56382978723406, 27.936170212765987, 265.56382978723406, 29.936170212765987, 283.56382978723406, 63.93617021276599, 281.56382978723406, 89.93617021276599, 252.56382978723406, 100.93617021276599, 183.56382978723406, 108.93617021276599, 145.56382978723406, 81.93617021276599, 151.56382978723406]], "area": 15781.0, "bbox": [0.0, 96.0, 109.0, 188.0], "iscrowd": 0}, {"id": 5, "image_id": 1, "category_id": 7, "segmentation": [[413.936170212766, 168.94844517184944, 497.936170212766, 168.94844517184944, 497.936170212766, 256.94844517184936, 431.936170212766, 258.94844517184936, 430.936170212766, 236.94844517184944, 408.936170212766, 218.94844517184944]], "area": 7256.0, "bbox": [408.0, 168.0, 90.0, 91.0], "iscrowd": 0}, {"id": 6, "image_id": 2, "category_id": 15, "segmentation": [[204.936170212766, 108.56382978723406, 183.936170212766, 141.56382978723406, 166.936170212766, 150.56382978723406, 108.93617021276599, 203.56382978723406, 92.93617021276599, 228.56382978723406, 95.93617021276599, 244.56382978723406, 105.93617021276599, 244.56382978723406, 116.93617021276599, 223.56382978723406, 163.936170212766, 187.56382978723406, 147.936170212766, 212.56382978723406, 117.93617021276599, 222.56382978723406, 108.93617021276599, 243.56382978723406, 100.93617021276599, 325.56382978723406, 135.936170212766, 329.56382978723406, 148.936170212766, 319.56382978723406, 150.936170212766, 295.56382978723406, 169.936170212766, 272.56382978723406, 171.936170212766, 249.56382978723406, 178.936170212766, 246.56382978723406, 186.936170212766, 225.56382978723406, 214.936170212766, 219.56382978723406, 242.936170212766, 157.56382978723406, 228.936170212766, 146.56382978723406, 228.936170212766, 125.56382978723406, 216.936170212766, 112.56382978723406]], "area": 15203.0, "bbox": [92.0, 108.0, 151.0, 222.0], "iscrowd": 0}, {"id": 7, "image_id": 2, "category_id": 15, "segmentation": [[271.936170212766, 109.56382978723406, 249.936170212766, 110.56382978723406, 244.936170212766, 150.56382978723406, 215.936170212766, 219.56382978723406, 208.936170212766, 245.56382978723406, 214.936170212766, 220.56382978723406, 188.936170212766, 227.56382978723406, 170.936170212766, 246.56382978723406, 170.936170212766, 275.56382978723406, 221.936170212766, 278.56382978723406, 233.936170212766, 259.56382978723406, 246.936170212766, 253.56382978723406, 245.936170212766, 256.56382978723406, 242.936170212766, 251.56382978723406, 262.936170212766, 256.56382978723406, 304.936170212766, 226.56382978723406, 297.936170212766, 199.56382978723406, 308.936170212766, 164.56382978723406, 296.936170212766, 148.56382978723406]], "area": 11735.0, "bbox": [170.0, 109.0, 139.0, 170.0], "iscrowd": 0}, {"id": 8, "image_id": 2, "category_id": 15, "segmentation": [[308.936170212766, 115.56382978723406, 298.936170212766, 145.56382978723406, 309.936170212766, 166.56382978723406, 297.936170212766, 200.56382978723406, 305.936170212766, 228.56382978723406, 262.936170212766, 258.56382978723406, 252.936170212766, 284.56382978723406, 272.936170212766, 291.56382978723406, 281.936170212766, 250.56382978723406, 326.936170212766, 235.56382978723406, 351.936170212766, 239.56382978723406, 365.936170212766, 223.56382978723406, 371.936170212766, 187.56382978723406, 353.936170212766, 168.56382978723406, 344.936170212766, 143.56382978723406, 336.936170212766, 115.56382978723406]], "area": 7597.0, "bbox": [252.0, 115.0, 120.0, 177.0], "iscrowd": 0}, {"id": 9, "image_id": 2, "category_id": 9, "segmentation": [[309.7054009819968, 242.94844517184941, 282.7054009819968, 251.94844517184941, 271.7054009819968, 287.9484451718494, 175.70540098199677, 275.9484451718494, 149.70540098199677, 296.9484451718494, 151.70540098199677, 319.9484451718494, 160.70540098199677, 328.9484451718494, 165.54250204582655, 375.38461538461536, 486.7054009819968, 373.9484451718494, 498.7054009819968, 336.9484451718494, 498.7054009819968, 202.94844517184941, 454.7054009819968, 193.94844517184941, 435.7054009819968, 212.94844517184941, 368.7054009819968, 224.94844517184941, 351.7054009819968, 241.94844517184941]], "area": 44532.0, "bbox": [149.0, 193.0, 350.0, 182.0], "iscrowd": 0}, {"id": 10, "image_id": 2, "category_id": 15, "segmentation": [[425.936170212766, 82.56382978723406, 404.936170212766, 109.56382978723406, 400.936170212766, 114.56382978723406, 437.936170212766, 114.56382978723406, 448.936170212766, 102.56382978723406, 446.936170212766, 91.56382978723406]], "area": 996.0, "bbox": [400.0, 82.0, 49.0, 33.0], "iscrowd": 0}, {"id": 11, "image_id": 2, "category_id": 18, "segmentation": [[183.936170212766, 140.56382978723406, 125.93617021276599, 140.56382978723406, 110.93617021276599, 187.56382978723406, 22.936170212765987, 199.56382978723406, 18.936170212765987, 218.56382978723406, 22.936170212765987, 234.56382978723406, 93.93617021276599, 239.56382978723406, 91.93617021276599, 229.56382978723406, 110.93617021276599, 203.56382978723406], [103.93617021276599, 290.56382978723406, 58.93617021276599, 303.56382978723406, 97.93617021276599, 311.56382978723406], [348.936170212766, 146.56382978723406, 472.936170212766, 149.56382978723406, 477.936170212766, 162.56382978723406, 471.936170212766, 196.56382978723406, 453.936170212766, 192.56382978723406, 434.936170212766, 213.56382978723406, 368.936170212766, 226.56382978723406, 375.936170212766, 187.56382978723406, 353.936170212766, 164.56382978723406], [246.936170212766, 252.56382978723406, 219.936170212766, 277.56382978723406, 254.936170212766, 287.56382978723406, 261.936170212766, 256.56382978723406]], "area": 14001.0, "bbox": [18.0, 140.0, 460.0, 172.0], "iscrowd": 0}], "categories": [{"supercategory": null, "id": 0, "name": "_background_"}, {"supercategory": null, "id": 1, "name": "aeroplane"}, {"supercategory": null, "id": 2, "name": "bicycle"}, {"supercategory": null, "id": 3, "name": "bird"}, {"supercategory": null, "id": 4, "name": "boat"}, {"supercategory": null, "id": 5, "name": "bottle"}, {"supercategory": null, "id": 6, "name": "bus"}, {"supercategory": null, "id": 7, "name": "car"}, {"supercategory": null, "id": 8, "name": "cat"}, {"supercategory": null, "id": 9, "name": "chair"}, {"supercategory": null, "id": 10, "name": "cow"}, {"supercategory": null, "id": 11, "name": "diningtable"}, {"supercategory": null, "id": 12, "name": "dog"}, {"supercategory": null, "id": 13, "name": "horse"}, {"supercategory": null, "id": 14, "name": "motorbike"}, {"supercategory": null, "id": 15, "name": "person"}, {"supercategory": null, "id": 16, "name": "potted plant"}, {"supercategory": null, "id": 17, "name": "sheep"}, {"supercategory": null, "id": 18, "name": "sofa"}, {"supercategory": null, "id": 19, "name": "train"}, {"supercategory": null, "id": 20, "name": "tv/monitor"}]} ================================================ FILE: examples/instance_segmentation/data_dataset_voc/class_names.txt ================================================ _background_ aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person potted plant sheep sofa train tv/monitor ================================================ FILE: examples/instance_segmentation/labelme2coco.py ================================================ #!/usr/bin/env python import argparse import collections import datetime import glob import json import os import os.path as osp import sys import uuid import imgviz import numpy as np import labelme try: import pycocotools.mask # type: ignore except ImportError: print("Please install pycocotools:\n\n pip install pycocotools\n") sys.exit(1) def main(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("input_dir", help="input annotated directory") parser.add_argument("output_dir", help="output dataset directory") parser.add_argument("--labels", help="labels file", required=True) parser.add_argument("--noviz", help="no visualization", action="store_true") args = parser.parse_args() if osp.exists(args.output_dir): print("Output directory already exists:", args.output_dir) sys.exit(1) os.makedirs(args.output_dir) os.makedirs(osp.join(args.output_dir, "JPEGImages")) if not args.noviz: os.makedirs(osp.join(args.output_dir, "Visualization")) print("Creating dataset:", args.output_dir) now = datetime.datetime.now() data = dict( info=dict( description=None, url=None, version=None, year=now.year, contributor=None, date_created=now.strftime("%Y-%m-%d %H:%M:%S.%f"), ), licenses=[ dict( url=None, id=0, name=None, ) ], type="instances", ) data["images"] = [] # license, url, file_name, height, width, date_captured, id data["categories"] = [] # supercategory, id, name data[ "annotations" ] = [] # segmentation, area, iscrowd, image_id, bbox, category_id, id class_name_to_id = {} for i, line in enumerate(open(args.labels).readlines()): class_id = i - 1 # starts with -1 class_name = line.strip() if class_id == -1: assert class_name == "__ignore__" continue class_name_to_id[class_name] = class_id data["categories"].append( dict( supercategory=None, id=class_id, name=class_name, ) ) out_ann_file = osp.join(args.output_dir, "annotations.json") label_files = glob.glob(osp.join(args.input_dir, "*.json")) for image_id, filename in enumerate(label_files): print("Generating dataset from:", filename) label_file = labelme.LabelFile(filename=filename) base = osp.splitext(osp.basename(filename))[0] out_img_file = osp.join(args.output_dir, "JPEGImages", f"{base}.jpg") img = labelme.utils.img_data_to_arr(label_file.imageData) if img.ndim == 3 and img.shape[2] == 4: img = imgviz.rgba2rgb(img) imgviz.io.imsave(out_img_file, img) data["images"].append( dict( license=0, url=None, file_name=osp.relpath(out_img_file, osp.dirname(out_ann_file)), height=img.shape[0], width=img.shape[1], date_captured=None, id=image_id, ) ) masks = {} # for area segmentations = collections.defaultdict(list) # for segmentation for shape in label_file.shapes: points: list[list[int | float]] = shape["points"] label = shape["label"] group_id = shape.get("group_id") shape_type = shape.get("shape_type", "polygon") mask = labelme.utils.shape_to_mask(img.shape[:2], points, shape_type) if group_id is None: group_id = uuid.uuid1() instance = (label, group_id) if instance in masks: masks[instance] = masks[instance] | mask else: masks[instance] = mask points_coco: list[int | float] if shape_type == "rectangle": (x1, y1), (x2, y2) = points x1, x2 = sorted([x1, x2]) y1, y2 = sorted([y1, y2]) points_coco = [x1, y1, x2, y1, x2, y2, x1, y2] if shape_type == "circle": (x1, y1), (x2, y2) = points r = np.linalg.norm([x2 - x1, y2 - y1]) # r(1-cos(a/2)) N>pi/arccos(1-x/r) # x: tolerance of the gap between the arc and the line segment n_points_circle = max(int(np.pi / np.arccos(1 - 1 / r)), 12) i = np.arange(n_points_circle) x = x1 + r * np.sin(2 * np.pi / n_points_circle * i) y = y1 + r * np.cos(2 * np.pi / n_points_circle * i) points_coco = np.stack((x, y), axis=1).flatten().tolist() else: points_coco = np.asarray(points).flatten().tolist() segmentations[instance].append(points_coco) segmentations = dict(segmentations) for instance, mask in masks.items(): cls_name, group_id = instance if cls_name not in class_name_to_id: continue cls_id = class_name_to_id[cls_name] mask = np.asfortranarray(mask.astype(np.uint8)) mask = pycocotools.mask.encode(mask) area = float(pycocotools.mask.area(mask)) bbox = pycocotools.mask.toBbox(mask).flatten().tolist() data["annotations"].append( dict( id=len(data["annotations"]), image_id=image_id, category_id=cls_id, segmentation=segmentations[instance], area=area, bbox=bbox, iscrowd=0, ) ) if not args.noviz: viz = img if masks: labels, captions, masks = zip( *[ (class_name_to_id[cnm], cnm, msk) for (cnm, gid), msk in masks.items() if cnm in class_name_to_id ] ) viz = imgviz.instances2rgb( image=img, labels=labels, masks=masks, captions=captions, font_size=15, line_width=2, ) out_viz_file = osp.join(args.output_dir, "Visualization", f"{base}.jpg") imgviz.io.imsave(out_viz_file, viz) with open(out_ann_file, "w") as f: json.dump(data, f) if __name__ == "__main__": main() ================================================ FILE: examples/instance_segmentation/labelme2voc.py ================================================ #!/usr/bin/env python import argparse import glob import os import os.path as osp import sys import imgviz import numpy as np import labelme def main(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("input_dir", help="Input annotated directory") parser.add_argument("output_dir", help="Output dataset directory") parser.add_argument( "--labels", help="Labels file or comma separated text", required=True ) parser.add_argument( "--noobject", help="Flag not to generate object label", action="store_true" ) parser.add_argument( "--nonpy", help="Flag not to generate .npy files", action="store_true" ) parser.add_argument( "--noviz", help="Flag to disable visualization", action="store_true" ) args = parser.parse_args() if osp.exists(args.output_dir): print("Output directory already exists:", args.output_dir) sys.exit(1) os.makedirs(args.output_dir) os.makedirs(osp.join(args.output_dir, "JPEGImages")) os.makedirs(osp.join(args.output_dir, "SegmentationClass")) if not args.nonpy: os.makedirs(osp.join(args.output_dir, "SegmentationClassNpy")) if not args.noviz: os.makedirs(osp.join(args.output_dir, "SegmentationClassVisualization")) if not args.noobject: os.makedirs(osp.join(args.output_dir, "SegmentationObject")) if not args.nonpy: os.makedirs(osp.join(args.output_dir, "SegmentationObjectNpy")) if not args.noviz: os.makedirs(osp.join(args.output_dir, "SegmentationObjectVisualization")) print("Creating dataset:", args.output_dir) if osp.exists(args.labels): with open(args.labels) as f: labels = [label.strip() for label in f if label] else: labels = [label.strip() for label in args.labels.split(",")] class_names: list[str] = [] class_name_to_id = {} for i, label in enumerate(labels): class_id = i - 1 # starts with -1 class_name = label.strip() class_name_to_id[class_name] = class_id if class_id == -1: assert class_name == "__ignore__" continue elif class_id == 0: assert class_name == "_background_" class_names.append(class_name) print("class_names:", class_names) out_class_names_file = osp.join(args.output_dir, "class_names.txt") with open(out_class_names_file, "w") as f: f.writelines("\n".join(class_names)) print("Saved class_names:", out_class_names_file) for filename in sorted(glob.glob(osp.join(args.input_dir, "*.json"))): print("Generating dataset from:", filename) label_file = labelme.LabelFile(filename=filename) base = osp.splitext(osp.basename(filename))[0] out_img_file = osp.join(args.output_dir, "JPEGImages", f"{base}.jpg") out_clsp_file = osp.join(args.output_dir, "SegmentationClass", f"{base}.png") if not args.nonpy: out_cls_file = osp.join( args.output_dir, "SegmentationClassNpy", f"{base}.npy" ) if not args.noviz: out_clsv_file = osp.join( args.output_dir, "SegmentationClassVisualization", f"{base}.jpg", ) if not args.noobject: out_insp_file = osp.join( args.output_dir, "SegmentationObject", f"{base}.png" ) if not args.nonpy: out_ins_file = osp.join( args.output_dir, "SegmentationObjectNpy", f"{base}.npy" ) if not args.noviz: out_insv_file = osp.join( args.output_dir, "SegmentationObjectVisualization", f"{base}.jpg", ) img = labelme.utils.img_data_to_arr(label_file.imageData) imgviz.io.imsave(out_img_file, img) cls, ins = labelme.utils.shapes_to_label( img_shape=img.shape, shapes=label_file.shapes, label_name_to_value=class_name_to_id, ) ins[cls == -1] = 0 # ignore it. # class label labelme.utils.lblsave(out_clsp_file, cls) if not args.nonpy: np.save(out_cls_file, cls) if not args.noviz: clsv = imgviz.label2rgb( cls, imgviz.rgb2gray(img), label_names=class_names, font_size=15, loc="rb", ) imgviz.io.imsave(out_clsv_file, clsv) if not args.noobject: # instance label labelme.utils.lblsave(out_insp_file, ins) if not args.nonpy: np.save(out_ins_file, ins) if not args.noviz: instance_ids = np.unique(ins) instance_names = [str(i) for i in range(max(instance_ids) + 1)] insv = imgviz.label2rgb( ins, imgviz.rgb2gray(img), label_names=instance_names, font_size=15, loc="rb", ) imgviz.io.imsave(out_insv_file, insv) if __name__ == "__main__": main() ================================================ FILE: examples/instance_segmentation/labels.txt ================================================ __ignore__ _background_ aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person potted plant sheep sofa train tv/monitor ================================================ FILE: examples/primitives/primitives.json ================================================ { "version": "5.7.0", "flags": {}, "shapes": [ { "label": "rectangle", "points": [ [ 32.0, 35.0 ], [ 132.0, 135.0 ] ], "group_id": null, "description": null, "shape_type": "rectangle", "flags": {}, "mask": null }, { "label": "circle", "points": [ [ 195.0, 84.0 ], [ 225.0, 125.0 ] ], "group_id": null, "description": null, "shape_type": "circle", "flags": {}, "mask": null }, { "label": "rectangle", "points": [ [ 391.0, 33.0 ], [ 542.0, 135.0 ] ], "group_id": null, "description": null, "shape_type": "rectangle", "flags": {}, "mask": null }, { "label": "polygon", "points": [ [ 69.0, 318.0 ], [ 45.0, 403.0 ], [ 173.0, 406.0 ], [ 198.0, 321.0 ] ], "group_id": null, "description": null, "shape_type": "polygon", "flags": {}, "mask": null }, { "label": "line", "points": [ [ 188.0, 178.0 ], [ 160.0, 224.0 ] ], "group_id": null, "description": null, "shape_type": "line", "flags": {}, "mask": null }, { "label": "point", "points": [ [ 345.0, 174.0 ] ], "group_id": null, "description": null, "shape_type": "point", "flags": {}, "mask": null }, { "label": "line_strip", "points": [ [ 440.53703703703707, 181.46296296296293 ], [ 402.53703703703707, 274.46296296296293 ], [ 544.5370370370371, 275.46296296296293 ] ], "group_id": null, "description": null, "shape_type": "linestrip", "flags": {}, "mask": null }, { "label": "octagon", "points": [ [ 417.39759036144574, 305.0 ], [ 516.3975903614457, 404.0 ] ], "group_id": null, "description": "", "shape_type": "mask", "flags": {}, "mask": "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAAAAABVicqIAAABd0lEQVR4nN3aUYrDMAwE0Bnd/86zdMt2aWI7TqwRJPooFGS9SE5DwSHOBV8furLoQq48CDfflY6005SJ9JOUhBxlaB2Z6VRryPRtoatlztzdGDLMIcYMk4Shw0SiyzCV6DBMJppM5BvgGEkxsCuz6QQWJUZXkBXhKcsuktkIe0hqsI24dgS+TtpIdiNsIcYIX2nuEd+2wzoubhFnI7BuPL8RayO4+y2Mz4CiYFowj4v/iLkRPGDj8R5SFEwLVeOiFeBTNh6vVsK/7SgaF+0En7LxeBDCCkSPGRdCFQjsoapbWGZBdT9GVSDeUOWzS8b6+kPsER/OjfhCv5+FT2FVIK7QF6L7dqINort2ogoEO8Q5r/CVVgPRHTtRE9H9OlEHsbUSrsJdJLEVdRFXRN9fCQ0Qj4ENktOK7CemODzMzGhGc9dN+wHzgiP3mwU4feh/ntFCnUlGq1XofqVkJkerBQ6ztLZ8JlELaydTdXnldLL1zzMvPAt+AJy8O66XXMiCAAAAAElFTkSuQmCC" } ], "imagePath": "primitives.jpg", "imageData": null, "imageHeight": 450, "imageWidth": 560 } ================================================ FILE: examples/semantic_segmentation/README.md ================================================ # Semantic Segmentation Example ## Annotation ```bash labelme data_annotated --labels labels.txt --validatelabel exact --config '{shift_auto_shape_color: -2}' ``` ![](.readme/annotation.jpg) ## Convert to VOC-format Dataset ```bash # It generates: # - data_dataset_voc/JPEGImages # - data_dataset_voc/SegmentationClass # - data_dataset_voc/SegmentationClassNpy # - data_dataset_voc/SegmentationClassVisualization ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt --noobject ``` Fig 1. JPEG image (left), PNG label (center), JPEG label visualization (right) Note that the label file contains only very low label values (ex. `0, 4, 14`), and `255` indicates the `__ignore__` label value (`-1` in the npy file). You can see the label PNG file by following. ```bash ../tutorial/draw_label_png.py data_dataset_voc/SegmentationClass/2011_000003.png ``` ================================================ FILE: examples/semantic_segmentation/data_annotated/2011_000003.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "person", "points": [ [ 250.8142292490119, 106.96696468940974 ], [ 229.8142292490119, 118.96696468940974 ], [ 221.8142292490119, 134.96696468940974 ], [ 223.8142292490119, 147.96696468940974 ], [ 217.8142292490119, 160.96696468940974 ], [ 202.8142292490119, 167.96696468940974 ], [ 192.8142292490119, 199.96696468940974 ], [ 194.8142292490119, 221.96696468940974 ], [ 199.8142292490119, 226.96696468940974 ], [ 191.8142292490119, 233.96696468940974 ], [ 197.8142292490119, 263.9669646894098 ], [ 213.8142292490119, 294.9669646894098 ], [ 214.8142292490119, 319.9669646894098 ], [ 221.8142292490119, 326.9669646894098 ], [ 235.8142292490119, 325.9669646894098 ], [ 240.8142292490119, 322.9669646894098 ], [ 235.8142292490119, 297.9669646894098 ], [ 238.8142292490119, 286.9669646894098 ], [ 234.8142292490119, 267.9669646894098 ], [ 257.81422924901193, 257.9669646894098 ], [ 264.81422924901193, 263.9669646894098 ], [ 256.81422924901193, 272.9669646894098 ], [ 259.81422924901193, 281.9669646894098 ], [ 284.81422924901193, 287.9669646894098 ], [ 297.81422924901193, 277.9669646894098 ], [ 288.81422924901193, 269.9669646894098 ], [ 281.81422924901193, 269.9669646894098 ], [ 283.81422924901193, 263.9669646894098 ], [ 292.81422924901193, 260.9669646894098 ], [ 308.81422924901193, 235.96696468940974 ], [ 313.81422924901193, 216.96696468940974 ], [ 309.81422924901193, 207.96696468940974 ], [ 312.81422924901193, 201.96696468940974 ], [ 308.81422924901193, 184.96696468940974 ], [ 291.81422924901193, 172.96696468940974 ], [ 269.81422924901193, 158.96696468940974 ], [ 261.81422924901193, 153.96696468940974 ], [ 264.81422924901193, 141.96696468940974 ], [ 273.81422924901193, 136.96696468940974 ], [ 278.81422924901193, 129.96696468940974 ], [ 270.81422924901193, 120.96696468940974 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 482.81422924901193, 85.33596837944665 ], [ 468.81422924901193, 90.33596837944665 ], [ 460.81422924901193, 110.33596837944665 ], [ 460.81422924901193, 127.33596837944665 ], [ 444.81422924901193, 137.33596837944665 ], [ 419.81422924901193, 153.33596837944665 ], [ 410.81422924901193, 163.33596837944665 ], [ 403.81422924901193, 168.33596837944665 ], [ 394.81422924901193, 170.33596837944665 ], [ 386.81422924901193, 168.33596837944665 ], [ 386.81422924901193, 184.33596837944665 ], [ 392.81422924901193, 182.33596837944665 ], [ 410.81422924901193, 187.33596837944665 ], [ 414.81422924901193, 192.33596837944665 ], [ 437.81422924901193, 189.33596837944665 ], [ 434.81422924901193, 204.33596837944665 ], [ 390.81422924901193, 195.33596837944665 ], [ 386.81422924901193, 195.33596837944665 ], [ 387.81422924901193, 208.33596837944665 ], [ 381.81422924901193, 212.33596837944665 ], [ 372.81422924901193, 212.33596837944665 ], [ 372.81422924901193, 216.33596837944665 ], [ 400.81422924901193, 270.3359683794467 ], [ 389.81422924901193, 272.3359683794467 ], [ 389.81422924901193, 274.3359683794467 ], [ 403.81422924901193, 282.3359683794467 ], [ 444.81422924901193, 283.3359683794467 ], [ 443.81422924901193, 259.3359683794467 ], [ 426.81422924901193, 244.33596837944665 ], [ 462.81422924901193, 256.3359683794467 ], [ 474.81422924901193, 270.3359683794467 ], [ 477.81422924901193, 280.3359683794467 ], [ 473.81422924901193, 289.3359683794467 ], [ 471.81422924901193, 296.3359683794467 ], [ 472.81422924901193, 317.3359683794467 ], [ 480.81422924901193, 332.3359683794467 ], [ 494.81422924901193, 335.3359683794467 ], [ 498.81422924901193, 329.3359683794467 ], [ 494.81422924901193, 308.3359683794467 ], [ 499.81422924901193, 297.3359683794467 ], [ 499.81422924901193, 90.33596837944665 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 370.81422924901193, 170.33596837944665 ], [ 366.81422924901193, 173.33596837944665 ], [ 365.81422924901193, 182.33596837944665 ], [ 368.81422924901193, 185.33596837944665 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "bottle", "points": [ [ 374.81422924901193, 159.33596837944665 ], [ 369.81422924901193, 170.33596837944665 ], [ 369.81422924901193, 210.33596837944665 ], [ 375.81422924901193, 212.33596837944665 ], [ 387.81422924901193, 209.33596837944665 ], [ 385.81422924901193, 185.33596837944665 ], [ 385.81422924901193, 168.33596837944665 ], [ 385.81422924901193, 165.33596837944665 ], [ 382.81422924901193, 159.33596837944665 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "__ignore__", "points": [ [ 338.81422924901193, 266.3359683794467 ], [ 313.81422924901193, 269.3359683794467 ], [ 297.81422924901193, 277.3359683794467 ], [ 282.81422924901193, 288.3359683794467 ], [ 273.81422924901193, 302.3359683794467 ], [ 272.81422924901193, 320.3359683794467 ], [ 279.81422924901193, 337.3359683794467 ], [ 428.81422924901193, 337.3359683794467 ], [ 432.81422924901193, 316.3359683794467 ], [ 423.81422924901193, 296.3359683794467 ], [ 403.81422924901193, 283.3359683794467 ], [ 370.81422924901193, 270.3359683794467 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "2011_000003.jpg", "imageData": null, "imageHeight": 338, "imageWidth": 500 } ================================================ FILE: examples/semantic_segmentation/data_annotated/2011_000006.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "person", "points": [ [ 204.936170212766, 108.56382978723406 ], [ 183.936170212766, 141.56382978723406 ], [ 166.936170212766, 150.56382978723406 ], [ 108.93617021276599, 203.56382978723406 ], [ 92.93617021276599, 228.56382978723406 ], [ 95.93617021276599, 244.56382978723406 ], [ 105.93617021276599, 244.56382978723406 ], [ 116.93617021276599, 223.56382978723406 ], [ 163.936170212766, 187.56382978723406 ], [ 147.936170212766, 212.56382978723406 ], [ 117.93617021276599, 222.56382978723406 ], [ 108.93617021276599, 243.56382978723406 ], [ 100.93617021276599, 325.56382978723406 ], [ 135.936170212766, 329.56382978723406 ], [ 148.936170212766, 319.56382978723406 ], [ 150.936170212766, 295.56382978723406 ], [ 169.936170212766, 272.56382978723406 ], [ 171.936170212766, 249.56382978723406 ], [ 178.936170212766, 246.56382978723406 ], [ 186.936170212766, 225.56382978723406 ], [ 214.936170212766, 219.56382978723406 ], [ 242.936170212766, 157.56382978723406 ], [ 228.936170212766, 146.56382978723406 ], [ 228.936170212766, 125.56382978723406 ], [ 216.936170212766, 112.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 271.936170212766, 109.56382978723406 ], [ 249.936170212766, 110.56382978723406 ], [ 244.936170212766, 150.56382978723406 ], [ 215.936170212766, 219.56382978723406 ], [ 208.936170212766, 245.56382978723406 ], [ 214.936170212766, 220.56382978723406 ], [ 188.936170212766, 227.56382978723406 ], [ 170.936170212766, 246.56382978723406 ], [ 170.936170212766, 275.56382978723406 ], [ 221.936170212766, 278.56382978723406 ], [ 233.936170212766, 259.56382978723406 ], [ 246.936170212766, 253.56382978723406 ], [ 245.936170212766, 256.56382978723406 ], [ 242.936170212766, 251.56382978723406 ], [ 262.936170212766, 256.56382978723406 ], [ 304.936170212766, 226.56382978723406 ], [ 297.936170212766, 199.56382978723406 ], [ 308.936170212766, 164.56382978723406 ], [ 296.936170212766, 148.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 308.936170212766, 115.56382978723406 ], [ 298.936170212766, 145.56382978723406 ], [ 309.936170212766, 166.56382978723406 ], [ 297.936170212766, 200.56382978723406 ], [ 305.936170212766, 228.56382978723406 ], [ 262.936170212766, 258.56382978723406 ], [ 252.936170212766, 284.56382978723406 ], [ 272.936170212766, 291.56382978723406 ], [ 281.936170212766, 250.56382978723406 ], [ 326.936170212766, 235.56382978723406 ], [ 351.936170212766, 239.56382978723406 ], [ 365.936170212766, 223.56382978723406 ], [ 371.936170212766, 187.56382978723406 ], [ 353.936170212766, 168.56382978723406 ], [ 344.936170212766, 143.56382978723406 ], [ 336.936170212766, 115.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "chair", "points": [ [ 308.936170212766, 243.33306055646483 ], [ 281.936170212766, 252.33306055646483 ], [ 270.936170212766, 288.33306055646483 ], [ 174.936170212766, 276.33306055646483 ], [ 148.936170212766, 297.33306055646483 ], [ 150.936170212766, 320.33306055646483 ], [ 159.936170212766, 329.33306055646483 ], [ 164.77327127659578, 375.7692307692308 ], [ 485.936170212766, 374.33306055646483 ], [ 497.936170212766, 337.33306055646483 ], [ 497.936170212766, 203.33306055646483 ], [ 453.936170212766, 194.33306055646483 ], [ 434.936170212766, 213.33306055646483 ], [ 367.936170212766, 225.33306055646483 ], [ 350.936170212766, 242.33306055646483 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "person", "points": [ [ 425.936170212766, 82.56382978723406 ], [ 404.936170212766, 109.56382978723406 ], [ 400.936170212766, 114.56382978723406 ], [ 437.936170212766, 114.56382978723406 ], [ 448.936170212766, 102.56382978723406 ], [ 446.936170212766, 91.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "__ignore__", "points": [ [ 457.936170212766, 85.56382978723406 ], [ 439.936170212766, 117.56382978723406 ], [ 477.936170212766, 117.56382978723406 ], [ 474.936170212766, 87.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 183.936170212766, 140.56382978723406 ], [ 125.93617021276599, 140.56382978723406 ], [ 110.93617021276599, 187.56382978723406 ], [ 22.936170212765987, 199.56382978723406 ], [ 18.936170212765987, 218.56382978723406 ], [ 22.936170212765987, 234.56382978723406 ], [ 93.93617021276599, 239.56382978723406 ], [ 91.93617021276599, 229.56382978723406 ], [ 110.93617021276599, 203.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 103.93617021276599, 290.56382978723406 ], [ 58.93617021276599, 303.56382978723406 ], [ 97.93617021276599, 311.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 348.936170212766, 146.56382978723406 ], [ 472.936170212766, 149.56382978723406 ], [ 477.936170212766, 162.56382978723406 ], [ 471.936170212766, 196.56382978723406 ], [ 453.936170212766, 192.56382978723406 ], [ 434.936170212766, 213.56382978723406 ], [ 368.936170212766, 226.56382978723406 ], [ 375.936170212766, 187.56382978723406 ], [ 353.936170212766, 164.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "sofa", "points": [ [ 246.936170212766, 252.56382978723406 ], [ 219.936170212766, 277.56382978723406 ], [ 254.936170212766, 287.56382978723406 ], [ 261.936170212766, 256.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "2011_000006.jpg", "imageData": null, "imageHeight": 375, "imageWidth": 500 } ================================================ FILE: examples/semantic_segmentation/data_annotated/2011_000025.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "bus", "points": [ [ 260.936170212766, 22.948445171849443 ], [ 193.936170212766, 19.948445171849443 ], [ 124.93617021276599, 39.94844517184944 ], [ 89.93617021276599, 101.94844517184944 ], [ 81.93617021276599, 150.94844517184944 ], [ 108.93617021276599, 145.94844517184944 ], [ 88.93617021276599, 244.94844517184944 ], [ 89.93617021276599, 322.94844517184936 ], [ 116.93617021276599, 367.94844517184936 ], [ 158.936170212766, 368.94844517184936 ], [ 165.936170212766, 337.94844517184936 ], [ 347.936170212766, 335.94844517184936 ], [ 349.936170212766, 369.94844517184936 ], [ 391.936170212766, 373.94844517184936 ], [ 403.936170212766, 335.94844517184936 ], [ 425.936170212766, 332.94844517184936 ], [ 421.936170212766, 281.94844517184936 ], [ 428.936170212766, 252.94844517184944 ], [ 428.936170212766, 236.94844517184944 ], [ 409.936170212766, 220.94844517184944 ], [ 409.936170212766, 150.94844517184944 ], [ 430.936170212766, 143.94844517184944 ], [ 433.936170212766, 112.94844517184944 ], [ 431.936170212766, 96.94844517184944 ], [ 408.936170212766, 90.94844517184944 ], [ 395.936170212766, 50.94844517184944 ], [ 338.936170212766, 25.948445171849443 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "bus", "points": [ [ 88.93617021276599, 115.56382978723406 ], [ 0.9361702127659877, 96.56382978723406 ], [ 0.0, 251.968085106388 ], [ 0.9361702127659877, 265.56382978723406 ], [ 27.936170212765987, 265.56382978723406 ], [ 29.936170212765987, 283.56382978723406 ], [ 63.93617021276599, 281.56382978723406 ], [ 89.93617021276599, 252.56382978723406 ], [ 100.93617021276599, 183.56382978723406 ], [ 108.93617021276599, 145.56382978723406 ], [ 81.93617021276599, 151.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "car", "points": [ [ 413.936170212766, 168.56382978723406 ], [ 497.936170212766, 168.56382978723406 ], [ 497.936170212766, 256.56382978723406 ], [ 431.936170212766, 258.56382978723406 ], [ 430.936170212766, 236.56382978723406 ], [ 408.936170212766, 218.56382978723406 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "2011_000025.jpg", "imageData": null, "imageHeight": 375, "imageWidth": 500 } ================================================ FILE: examples/semantic_segmentation/data_dataset_voc/class_names.txt ================================================ _background_ aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person potted plant sheep sofa train tv/monitor ================================================ FILE: examples/semantic_segmentation/labels.txt ================================================ __ignore__ _background_ aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person potted plant sheep sofa train tv/monitor ================================================ FILE: examples/tutorial/README.md ================================================ # Tutorial (Single Image Example) ## Annotation ```bash labelme apc2016_obj3.jpg ``` ![](.readme/annotation.jpg) ## Visualization To view the json file quickly, you can use utility script: ```bash ./draw_json.py apc2016_obj3.json ``` ## Convert to Dataset To convert the json to set of image and label, you can run following: ```bash ./export_json.py apc2016_obj3.json ``` It generates standard files from the JSON file. - [img.png](apc2016_obj3/img.png): Image file. - [label.png](apc2016_obj3/label.png): uint8 label file. - [label_viz.png](apc2016_obj3/label_viz.png): Visualization of `label.png`. - [label_names.txt](apc2016_obj3/label_names.txt): Label names for values in `label.png`. ## How to load label PNG file? Note that loading `label.png` is a bit difficult (`scipy.misc.imread`, `skimage.io.imread` may not work correctly), and please use `PIL.Image.open` to avoid unexpected behavior: ```python # see load_label_png.py also. >>> import numpy as np >>> import PIL.Image >>> label_png = 'apc2016_obj3/label.png' >>> lbl = np.asarray(PIL.Image.open(label_png)) >>> print(lbl.dtype) dtype('uint8') >>> np.unique(lbl) array([0, 1, 2, 3], dtype=uint8) >>> lbl.shape (907, 1210) ``` Also, you can see the label PNG file by: ```bash ./draw_label_png.py apc2016_obj3/label.png ``` ================================================ FILE: examples/tutorial/apc2016_obj3/label_names.txt ================================================ _background_ highland_6539_self_stick_notes kong_air_dog_squeakair_tennis_ball mead_index_cards shelf ================================================ FILE: examples/tutorial/apc2016_obj3.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "shelf", "points": [ [ 7.942307692307736, 80.76150251617551 ], [ 171.94230769230774, 714.7615025161755 ], [ 968.9423076923077, 733.7615025161755 ], [ 1181.9423076923076, 110.76150251617551 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "highland_6539_self_stick_notes", "points": [ [ 430.16339869281046, 516.2450980392157 ], [ 390.9477124183006, 606.4411764705883 ], [ 398.7908496732026, 697.2908496732026 ], [ 522.3202614379085, 711.6699346405229 ], [ 762.18954248366, 721.4738562091503 ], [ 777.2222222222222, 634.5457516339869 ], [ 761.5359477124183, 537.8137254901961 ], [ 634.0849673202614, 518.8594771241831 ], [ 573.3006535947712, 512.9771241830066 ], [ 534.0849673202614, 513.6307189542484 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "mead_index_cards", "points": [ [ 447.156862745098, 394.6764705882353 ], [ 410.55555555555554, 480.95098039215685 ], [ 415.1307189542483, 522.781045751634 ], [ 422.9738562091503, 522.781045751634 ], [ 427.5490196078431, 515.5915032679738 ], [ 570.032679738562, 512.3235294117648 ], [ 732.1241830065359, 528.0098039215686 ], [ 733.4313725490196, 492.71568627450984 ], [ 724.9346405228757, 407.0947712418301 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "kong_air_dog_squeakair_tennis_ball", "points": [ [ 419.05228758169926, 266.5718954248366 ], [ 343.235294117647, 303.1732026143791 ], [ 511.2091503267974, 318.859477124183 ], [ 519.7058823529411, 334.5457516339869 ], [ 533.4313725490196, 339.7745098039216 ], [ 551.7320261437908, 349.57843137254906 ], [ 573.3006535947712, 354.15359477124184 ], [ 592.2549019607843, 353.5 ], [ 604.0196078431372, 345.0032679738562 ], [ 613.1699346405228, 341.7352941176471 ], [ 687.0261437908497, 365.91830065359477 ], [ 696.1764705882352, 358.07516339869284 ], [ 727.5490196078431, 329.3169934640523 ], [ 677.2222222222222, 306.44117647058823 ], [ 651.078431372549, 273.76143790849676 ], [ 632.1241830065359, 272.4542483660131 ], [ 612.516339869281, 248.27124183006538 ], [ 596.8300653594771, 241.08169934640526 ], [ 577.2222222222222, 234.54575163398695 ], [ 563.4967320261437, 234.54575163398695 ], [ 545.1960784313725, 235.19934640522877 ], [ 534.7385620915032, 242.3888888888889 ], [ 526.2418300653594, 247.61764705882354 ], [ 513.1699346405228, 258.7287581699347 ], [ 507.28758169934633, 266.5718954248366 ], [ 502.0588235294117, 272.4542483660131 ], [ 496.17647058823525, 273.1078431372549 ], [ 474.6078431372549, 273.1078431372549 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "apc2016_obj3.json", "imageData": "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", "imageHeight": 907, "imageWidth": 1210 } ================================================ FILE: examples/tutorial/draw_json.py ================================================ #!/usr/bin/env python import argparse import imgviz import matplotlib.pyplot as plt from labelme import utils from labelme._label_file import LabelFile def main(): parser = argparse.ArgumentParser() parser.add_argument("json_file") args = parser.parse_args() label_file = LabelFile(args.json_file) img = utils.img_data_to_arr(label_file.imageData) label_name_to_value = {"_background_": 0} for shape in sorted(label_file.shapes, key=lambda x: x["label"]): label_name = shape["label"] if label_name in label_name_to_value: label_value = label_name_to_value[label_name] else: label_value = len(label_name_to_value) label_name_to_value[label_name] = label_value lbl, _ = utils.shapes_to_label(img.shape, label_file.shapes, label_name_to_value) label_names: list[str] = [""] * (max(label_name_to_value.values()) + 1) for name, value in label_name_to_value.items(): label_names[value] = name lbl_viz = imgviz.label2rgb( lbl, imgviz.asgray(img), label_names=label_names, font_size=30, loc="rb", ) plt.subplot(121) plt.imshow(img) plt.subplot(122) plt.imshow(lbl_viz) plt.show() if __name__ == "__main__": main() ================================================ FILE: examples/tutorial/draw_label_png.py ================================================ #!/usr/bin/env python import argparse import os import imgviz import matplotlib.pyplot as plt import numpy as np from loguru import logger def main(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("label_png", help="label PNG file") parser.add_argument( "--labels", help="labels list (comma separated text or file)", default=None, ) parser.add_argument("--image", help="image file", default=None) args = parser.parse_args() if args.labels is not None: if os.path.exists(args.labels): with open(args.labels) as f: label_names = [label.strip() for label in f] else: label_names = args.labels.split(",") else: label_names = None if args.image is not None: image = imgviz.io.imread(args.image) else: image = None label = imgviz.io.imread(args.label_png) label = label.astype(np.int32) label[label == 255] = -1 unique_label_values = np.unique(label) logger.info(f"Label image shape: {label.shape}") logger.info(f"Label values: {unique_label_values.tolist()}") if label_names is not None: logger.info( "Label names: {}".format( [ f"{label_value}:{label_names[label_value]}" for label_value in unique_label_values ] ) ) if args.image: num_cols = 2 else: num_cols = 1 plt.figure(figsize=(num_cols * 6, 5)) plt.subplot(1, num_cols, 1) plt.title(args.label_png) label_viz = imgviz.label2rgb( label=label, label_names=label_names, font_size=label.shape[1] // 30 ) plt.imshow(label_viz) if image is not None: plt.subplot(1, num_cols, 2) label_viz_with_overlay = imgviz.label2rgb( label=label, image=image, label_names=label_names, font_size=label.shape[1] // 30, ) plt.title(f"{args.label_png}\n{args.image}") plt.imshow(label_viz_with_overlay) plt.tight_layout() plt.show() if __name__ == "__main__": main() ================================================ FILE: examples/tutorial/export_json.py ================================================ #!/usr/bin/env python import argparse import os import os.path as osp import imgviz import numpy as np import PIL.Image from loguru import logger from numpy.typing import NDArray from labelme import utils from labelme._label_file import LabelFile def main(): parser = argparse.ArgumentParser() parser.add_argument("json_file") parser.add_argument("-o", "--out", default=None) args = parser.parse_args() json_file = args.json_file if args.out is None: out_dir = osp.splitext(osp.basename(json_file))[0] out_dir = osp.join(osp.dirname(json_file), out_dir) else: out_dir = args.out os.makedirs(out_dir, exist_ok=True) label_file: LabelFile = LabelFile(filename=json_file) image: NDArray[np.uint8] = utils.img_data_to_arr(label_file.imageData) label_name_to_value: dict[str, int] = {"_background_": 0} for shape in sorted(label_file.shapes, key=lambda x: x["label"]): label_name = shape["label"] if label_name in label_name_to_value: label_value = label_name_to_value[label_name] else: label_value = len(label_name_to_value) label_name_to_value[label_name] = label_value lbl, _ = utils.shapes_to_label(image.shape, label_file.shapes, label_name_to_value) label_names: list[str] = [""] * (max(label_name_to_value.values()) + 1) for name, value in label_name_to_value.items(): label_names[value] = name lbl_viz = imgviz.label2rgb( lbl, imgviz.asgray(image), label_names=label_names, loc="rb" ) PIL.Image.fromarray(image).save(osp.join(out_dir, "img.png")) utils.lblsave(osp.join(out_dir, "label.png"), lbl) PIL.Image.fromarray(lbl_viz).save(osp.join(out_dir, "label_viz.png")) with open(osp.join(out_dir, "label_names.txt"), "w") as f: for lbl_name in label_names: f.write(f"{lbl_name}\n") logger.info(f"Saved to: {out_dir}") if __name__ == "__main__": main() ================================================ FILE: examples/tutorial/load_label_png.py ================================================ #!/usr/bin/env python import os.path as osp import numpy as np import PIL.Image here = osp.dirname(osp.abspath(__file__)) def main(): label_png = osp.join(here, "apc2016_obj3/label.png") print("Loading:", label_png) print() lbl = np.asarray(PIL.Image.open(label_png)) labels = np.unique(lbl) label_names_txt = osp.join(here, "apc2016_obj3/label_names.txt") label_names = [name.strip() for name in open(label_names_txt)] print("# of labels:", len(labels)) print("# of label_names:", len(label_names)) if len(labels) != len(label_names): print("Number of unique labels and label_names must be same.") quit(1) print() print("label: label_name") for label, label_name in zip(labels, label_names): print(f"{label}: {label_name}") if __name__ == "__main__": main() ================================================ FILE: examples/video_annotation/README.md ================================================ # Video Annotation Example ## Annotation ```bash labelme data_annotated --labels labels.txt --keep-prev --config '{shift_auto_shape_color: -2}' ``` *Fig 1. Video annotation example. A frame (left), The next frame (right).* *Fig 2. Visualization of video semantic segmentation.* ## How to Convert a Video File to Images for Annotation? ```bash pip install video-cli video-toimg your_video.mp4 # this creates your_video/ directory ls your_video/ labelme your_video/ ``` ================================================ FILE: examples/video_annotation/data_annotated/00000100.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "track", "points": [ [ 634.0, 203.25925925925924 ], [ 604.0, 274.25925925925924 ], [ 603.0, 339.25925925925924 ], [ 622.0, 362.25925925925924 ], [ 639.0, 362.25925925925924 ], [ 649.0, 353.25925925925924 ], [ 682.0, 382.25925925925924 ], [ 733.0, 389.25925925925924 ], [ 748.0, 363.25925925925924 ], [ 827.0, 358.25925925925924 ], [ 829.0, 249.25925925925924 ], [ 800.0, 193.25925925925924 ], [ 775.0, 184.25925925925924 ], [ 740.0, 198.25925925925924 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "track", "points": [ [ 860.0, 190.0 ], [ 997.0, 186.0 ], [ 998.0, 305.0 ], [ 924.0, 320.0 ], [ 905.0, 352.0 ], [ 877.0, 353.0 ], [ 869.0, 245.0 ], [ 879.0, 222.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "car", "points": [ [ 924.0, 321.0 ], [ 905.0, 352.0 ], [ 909.0, 388.0 ], [ 936.0, 404.0 ], [ 959.0, 411.0 ], [ 966.0, 431.0 ], [ 1000.0, 432.0 ], [ 1000.0, 306.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "00000100.jpg", "imageData": null, "imageHeight": 563, "imageWidth": 1000 } ================================================ FILE: examples/video_annotation/data_annotated/00000101.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "track", "points": [ [ 614.7407407407408, 203.2592592592593 ], [ 584.7407407407408, 274.2592592592593 ], [ 583.7407407407408, 339.2592592592593 ], [ 602.7407407407408, 362.2592592592593 ], [ 619.7407407407408, 362.2592592592593 ], [ 629.7407407407408, 353.2592592592593 ], [ 662.7407407407408, 382.2592592592593 ], [ 713.7407407407408, 389.2592592592593 ], [ 728.7407407407408, 363.2592592592593 ], [ 827.7407407407408, 357.2592592592593 ], [ 825.7407407407408, 248.2592592592593 ], [ 801.7407407407408, 199.2592592592593 ], [ 757.7407407407408, 193.2592592592593 ], [ 720.7407407407408, 198.2592592592593 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "track", "points": [ [ 860.0, 190.0 ], [ 997.0, 186.0 ], [ 998.0, 305.0 ], [ 924.0, 320.0 ], [ 905.0, 352.0 ], [ 877.0, 353.0 ], [ 869.0, 245.0 ], [ 879.0, 222.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "car", "points": [ [ 924.0, 321.0 ], [ 905.0, 352.0 ], [ 909.0, 388.0 ], [ 936.0, 404.0 ], [ 959.0, 411.0 ], [ 966.0, 431.0 ], [ 1000.0, 432.0 ], [ 1000.0, 306.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "00000101.jpg", "imageData": null, "imageHeight": 563, "imageWidth": 1000 } ================================================ FILE: examples/video_annotation/data_annotated/00000102.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "track", "points": [ [ 591.5185185185185, 202.51851851851853 ], [ 561.5185185185185, 273.51851851851853 ], [ 560.5185185185185, 338.51851851851853 ], [ 579.5185185185185, 361.51851851851853 ], [ 596.5185185185185, 361.51851851851853 ], [ 606.5185185185185, 352.51851851851853 ], [ 639.5185185185185, 381.51851851851853 ], [ 690.5185185185185, 388.51851851851853 ], [ 705.5185185185185, 362.51851851851853 ], [ 825.5185185185185, 356.51851851851853 ], [ 821.5185185185185, 241.51851851851853 ], [ 800.5185185185185, 197.51851851851853 ], [ 734.5185185185185, 192.51851851851853 ], [ 697.5185185185185, 197.51851851851853 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "track", "points": [ [ 860.0, 190.0 ], [ 997.0, 186.0 ], [ 998.0, 305.0 ], [ 924.0, 320.0 ], [ 905.0, 352.0 ], [ 877.0, 353.0 ], [ 869.0, 245.0 ], [ 879.0, 222.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "car", "points": [ [ 924.0, 321.0 ], [ 905.0, 352.0 ], [ 909.0, 388.0 ], [ 936.0, 404.0 ], [ 959.0, 411.0 ], [ 966.0, 431.0 ], [ 1000.0, 432.0 ], [ 1000.0, 306.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "00000102.jpg", "imageData": null, "imageHeight": 563, "imageWidth": 1000 } ================================================ FILE: examples/video_annotation/data_annotated/00000103.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "track", "points": [ [ 573.0, 202.55555555555554 ], [ 543.0, 273.55555555555554 ], [ 542.0, 338.55555555555554 ], [ 561.0, 361.55555555555554 ], [ 578.0, 361.55555555555554 ], [ 588.0, 352.55555555555554 ], [ 621.0, 381.55555555555554 ], [ 672.0, 388.55555555555554 ], [ 687.0, 362.55555555555554 ], [ 829.0, 349.55555555555554 ], [ 821.0, 231.55555555555554 ], [ 801.0, 194.55555555555554 ], [ 716.0, 192.55555555555554 ], [ 679.0, 197.55555555555554 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "track", "points": [ [ 860.0, 190.0 ], [ 997.0, 186.0 ], [ 998.0, 305.0 ], [ 924.0, 320.0 ], [ 905.0, 352.0 ], [ 877.0, 353.0 ], [ 869.0, 245.0 ], [ 879.0, 222.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "car", "points": [ [ 924.0, 321.0 ], [ 905.0, 352.0 ], [ 909.0, 388.0 ], [ 936.0, 404.0 ], [ 959.0, 411.0 ], [ 966.0, 431.0 ], [ 1000.0, 432.0 ], [ 1000.0, 306.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "00000103.jpg", "imageData": null, "imageHeight": 563, "imageWidth": 1000 } ================================================ FILE: examples/video_annotation/data_annotated/00000104.json ================================================ { "version": "4.0.0", "flags": {}, "shapes": [ { "label": "track", "points": [ [ 555.2592592592594, 200.25925925925924 ], [ 527.2592592592594, 276.25925925925924 ], [ 523.2592592592594, 341.25925925925924 ], [ 527.2592592592594, 360.25925925925924 ], [ 562.2592592592594, 364.25925925925924 ], [ 572.2592592592594, 355.25925925925924 ], [ 605.2592592592594, 384.25925925925924 ], [ 656.2592592592594, 391.25925925925924 ], [ 671.2592592592594, 365.25925925925924 ], [ 824.2592592592594, 353.25925925925924 ], [ 825.2592592592594, 237.25925925925924 ], [ 800.2592592592594, 201.25925925925924 ], [ 700.2592592592594, 195.25925925925924 ], [ 663.2592592592594, 200.25925925925924 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "track", "points": [ [ 860.0, 190.0 ], [ 997.0, 186.0 ], [ 998.0, 305.0 ], [ 924.0, 320.0 ], [ 905.0, 352.0 ], [ 874.0, 354.0 ], [ 869.0, 245.0 ], [ 879.0, 222.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} }, { "label": "car", "points": [ [ 924.0, 321.0 ], [ 905.0, 352.0 ], [ 909.0, 388.0 ], [ 936.0, 404.0 ], [ 959.0, 411.0 ], [ 966.0, 431.0 ], [ 1000.0, 432.0 ], [ 1000.0, 306.0 ] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "00000104.jpg", "imageData": null, "imageHeight": 563, "imageWidth": 1000 } ================================================ FILE: examples/video_annotation/data_dataset_voc/class_names.txt ================================================ _background_ car track ================================================ FILE: examples/video_annotation/labels.txt ================================================ __ignore__ _background_ car track ================================================ FILE: labelme/__init__.py ================================================ import importlib.metadata __appname__ = "Labelme" # Semantic Versioning 2.0.0: https://semver.org/ # 1. MAJOR version when you make incompatible API changes; # 2. MINOR version when you add functionality in a backwards-compatible manner; # 3. PATCH version when you make backwards-compatible bug fixes. # e.g., 1.0.0a0, 1.0.0a1, 1.0.0b0, 1.0.0rc0, 1.0.0, 1.0.0.post0 __version__ = importlib.metadata.version("labelme") # XXX: has to be imported before PyQt5 to load dlls in order on Windows # https://github.com/wkentaro/labelme/issues/1564 import onnxruntime from labelme import utils from labelme._label_file import LabelFile ================================================ FILE: labelme/__main__.py ================================================ import argparse import codecs import contextlib import io import os import os.path as osp import sys import traceback import warnings from pathlib import Path from typing import AnyStr import yaml from loguru import logger from PyQt5 import QtCore from PyQt5 import QtWidgets from labelme import __appname__ from labelme import __version__ from labelme.app import MainWindow from labelme.config import get_user_config_file from labelme.utils import newIcon class _LoggerIO(io.StringIO): def write(self, s: AnyStr) -> int: assert isinstance(s, str) if stripped_s := s.strip(): logger.debug(stripped_s) return len(s) def flush(self) -> None: pass def writable(self) -> bool: return True def readable(self) -> bool: return False def seekable(self) -> bool: return False @property def closed(self) -> bool: return False def _setup_loguru(logger_level: str) -> None: try: logger.remove(handler_id=0) except ValueError: pass if sys.stderr: logger.add(sys.stderr, level=logger_level) cache_dir: str if os.name == "nt": cache_dir = os.path.join(os.environ["LOCALAPPDATA"], "labelme") else: cache_dir = os.path.expanduser("~/.cache/labelme") os.makedirs(cache_dir, exist_ok=True) log_file = os.path.join(cache_dir, "labelme.log") logger.add( log_file, colorize=True, level="DEBUG", rotation="10 MB", retention="30 days", compression="gz", enqueue=True, backtrace=True, diagnose=True, ) def _handle_exception(exc_type, exc_value, exc_traceback): if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) sys.exit(0) traceback_str: str = "".join( traceback.format_exception(exc_type, exc_value, exc_traceback) ) logger.critical(traceback_str) traceback_html: str = traceback_str.replace("\n", "
").replace(" ", " ") QtWidgets.QMessageBox.critical( None, "Error", f"An unexpected error occurred. The application will close.

Please report issues following the Troubleshoot.

{traceback_html}", # noqa: E501 ) if app := QtWidgets.QApplication.instance(): app.quit() sys.exit(1) def main(): parser = argparse.ArgumentParser() parser.add_argument("--version", "-V", action="store_true", help="show version") parser.add_argument("--reset-config", action="store_true", help="reset qt config") parser.add_argument( "--logger-level", default="debug", choices=["debug", "info", "warning", "fatal", "error"], help="logger level", ) parser.add_argument("path", nargs="?", help="image file, label file, or directory") parser.add_argument( "--output", help="output directory for saving annotation JSON files", ) default_config_file = get_user_config_file() parser.add_argument( "--config", dest="config", help=f"config file or yaml-format string (default: {default_config_file})", default=default_config_file, ) # config for the gui parser.add_argument( "--nodata", dest="_deprecated_nodata", action="store_true", help=argparse.SUPPRESS, default=argparse.SUPPRESS, ) parser.add_argument( "--with-image-data", dest="with_image_data", action="store_true", help="store image data in JSON file", default=argparse.SUPPRESS, ) parser.add_argument( "--no-auto-save", dest="auto_save", action="store_false", help="disable auto save", default=argparse.SUPPRESS, ) parser.add_argument( "--autosave", dest="_deprecated_autosave", action="store_true", help=argparse.SUPPRESS, default=argparse.SUPPRESS, ) parser.add_argument( "--no-sort-labels", "--nosortlabels", # deprecated dest="sort_labels", action="store_false", help="stop sorting labels", default=argparse.SUPPRESS, ) parser.add_argument( "--flags", help="comma separated list of flags OR file containing flags", default=argparse.SUPPRESS, ) parser.add_argument( "--label-flags", "--labelflags", # deprecated dest="label_flags", help=r"yaml string of label specific flags OR file containing json " r"string of label specific flags (ex. {person-\d+: [male, tall], " r"dog-\d+: [black, brown, white], .*: [occluded]})", # NOQA default=argparse.SUPPRESS, ) parser.add_argument( "--labels", help="comma separated list of labels OR file containing labels", default=argparse.SUPPRESS, ) parser.add_argument( "--validate-label", "--validatelabel", # deprecated dest="validate_label", choices=["exact"], help="label validation types", default=argparse.SUPPRESS, ) parser.add_argument( "--keep-prev", action="store_true", help="keep annotation of previous frame", default=argparse.SUPPRESS, ) parser.add_argument( "--epsilon", type=float, help="epsilon to find nearest vertex on canvas", default=argparse.SUPPRESS, ) args = parser.parse_args() if hasattr(args, "_deprecated_nodata"): warnings.warn( "--nodata is deprecated and will be removed in a future version. " "Image data is no longer stored by default. " "Use --with-image-data to store it.", FutureWarning, stacklevel=1, ) del args._deprecated_nodata if hasattr(args, "_deprecated_autosave"): warnings.warn( "--autosave is deprecated and will be removed in a future version. " "Auto save is now enabled by default. Use --no-autosave to disable it.", FutureWarning, stacklevel=1, ) del args._deprecated_autosave if args.version: print(f"{__appname__} {__version__}") sys.exit(0) _setup_loguru(logger_level=args.logger_level.upper()) logger.info("Starting {} {}", __appname__, __version__) sys.excepthook = _handle_exception if hasattr(args, "flags"): if os.path.isfile(args.flags): with codecs.open(args.flags, "r", encoding="utf-8") as f: args.flags = [line.strip() for line in f if line.strip()] else: args.flags = [line for line in args.flags.split(",") if line] if hasattr(args, "labels"): if os.path.isfile(args.labels): with codecs.open(args.labels, "r", encoding="utf-8") as f: args.labels = [line.strip() for line in f if line.strip()] else: args.labels = [line for line in args.labels.split(",") if line] if hasattr(args, "label_flags"): if os.path.isfile(args.label_flags): with codecs.open(args.label_flags, "r", encoding="utf-8") as f: args.label_flags = yaml.safe_load(f) else: args.label_flags = yaml.safe_load(args.label_flags) config_from_args = args.__dict__ config_from_args.pop("version") reset_config = config_from_args.pop("reset_config") filename = config_from_args.pop("path") output = config_from_args.pop("output") config_overrides: dict config_file: Path | None config_str: str = config_from_args.pop("config") if isinstance(config_loaded := yaml.safe_load(config_str), dict): config_overrides = config_loaded config_file = None else: config_overrides = {} config_file = Path(config_str) if not config_file.is_file(): logger.error( "Config file does not exist: {!r}", str(config_file.absolute()) ) sys.exit(1) del config_str config_overrides.update(config_from_args) output_dir = None if output is not None: if output.endswith(".json"): parser.error( f"--output expects a directory path, but '{output}' looks like a file." " Remove the .json extension or provide a directory path." ) output_dir = output translator = QtCore.QTranslator() translator.load( QtCore.QLocale.system().name(), f"{osp.dirname(osp.abspath(__file__))}/translate", ) app = QtWidgets.QApplication(sys.argv) app.setStyle("Fusion") # for consistent appearance across platforms # Force light mode to avoid dark mode UI issues (e.g., invisible icons) app.setPalette(QtWidgets.QStyleFactory.create("Fusion").standardPalette()) app.setApplicationName(__appname__) app.setWindowIcon(newIcon("icon")) app.installTranslator(translator) win = MainWindow( config_file=config_file, config_overrides=config_overrides, filename=filename, output_dir=output_dir, ) if reset_config: logger.info(f"Resetting Qt config: {win.settings.fileName()}") win.settings.clear() sys.exit(0) with contextlib.redirect_stderr(new_target=_LoggerIO()): win.show() win.raise_() sys.exit(app.exec_()) # this main block is required to generate executable by pyinstaller if __name__ == "__main__": main() ================================================ FILE: labelme/_automation/__init__.py ================================================ from ._osam_session import OsamSession ================================================ FILE: labelme/_automation/_osam_session.py ================================================ from __future__ import annotations import collections import numpy as np import osam from loguru import logger from numpy.typing import NDArray class OsamSession: _model_name: str _model: osam.types.Model | None _embedding_cache: collections.deque[tuple[str, osam.types.ImageEmbedding]] def __init__( self, model_name: str = "sam2:latest", embedding_cache_size: int = 3, ) -> None: logger.debug("Initializing OsamSession with model_name={!r}", model_name) self._model_name = model_name self._model = None self._embedding_cache = collections.deque(maxlen=embedding_cache_size) logger.debug("Initialized OsamSession with model_name={!r}", model_name) @property def model_name(self) -> str: return self._model_name def run( self, image: NDArray[np.uint8], image_id: str, points: NDArray[np.floating] | None = None, point_labels: NDArray[np.intp] | None = None, texts: list[str] | None = None, ) -> osam.types.GenerateResponse: image_embedding: osam.types.ImageEmbedding | None try: image_embedding = self._get_or_compute_embedding( image=image, image_id=image_id ) except NotImplementedError: image_embedding = None prompt: osam.types.Prompt if points is not None and point_labels is not None: prompt = osam.types.Prompt( points=points, point_labels=point_labels, ) elif texts is not None: prompt = osam.types.Prompt( texts=texts, iou_threshold=1.0, score_threshold=0.01, max_annotations=1000, ) else: raise ValueError( "Either points and point_labels, or texts must be provided." ) model: osam.types.Model = self._get_or_load_model() return model.generate( request=osam.types.GenerateRequest( model=model.name, image=image, image_embedding=image_embedding, prompt=prompt, ) ) def _get_or_compute_embedding( self, image: NDArray[np.uint8], image_id: str ) -> osam.types.ImageEmbedding: for key, embedding in self._embedding_cache: if key == image_id: return embedding model: osam.types.Model = self._get_or_load_model() logger.debug("Computing embedding for cache_key={!r}", image_id) embedding: osam.types.ImageEmbedding = model.encode_image(image=image) self._embedding_cache.append((image_id, embedding)) logger.debug("Cached embedding for cache_key={!r}", image_id) return embedding def _get_or_load_model(self) -> osam.types.Model: if self._model is None: logger.debug("Loading model with name={!r}", self._model_name) self._model = osam.apis.get_model_type_by_name(self._model_name)() logger.debug("Loaded model with name={!r}", self._model_name) return self._model ================================================ FILE: labelme/_automation/bbox_from_text.py ================================================ import json import time from typing import Literal import numpy as np import osam from loguru import logger from numpy.typing import NDArray from PyQt5 import QtCore from labelme.shape import Shape from ._osam_session import OsamSession from .polygon_from_mask import compute_polygon_from_mask def get_bboxes_from_texts( session: OsamSession, image: np.ndarray, image_id: str, texts: list[str] ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray | None]: logger.debug( f"Requesting with model={session.model_name!r}, " f"image={(image.shape, image.dtype)}, texts={texts!r}" ) t_start: float = time.time() response: osam.types.GenerateResponse = session.run( image=image, image_id=image_id, texts=texts, ) num_annotations: int = len(response.annotations) logger.debug( f"Response: num_annotations={num_annotations}, " f"elapsed_time={time.time() - t_start:.3f} [s]" ) boxes: NDArray[np.float32] = np.empty((num_annotations, 4), dtype=np.float32) scores: NDArray[np.float32] = np.empty((num_annotations,), dtype=np.float32) labels: NDArray[np.int32] = np.empty((num_annotations,), dtype=np.int32) for i, annotation in enumerate(response.annotations): if annotation.bounding_box is None: raise ValueError("Bounding box is missing in the annotation.") if annotation.text not in texts: raise ValueError( f"Unexpected text {annotation.text!r} found in the response." ) boxes[i] = [ annotation.bounding_box.xmin, annotation.bounding_box.ymin, annotation.bounding_box.xmax, annotation.bounding_box.ymax, ] scores[i] = annotation.score labels[i] = texts.index(annotation.text) masks: NDArray[np.bool_] | None = None if response.annotations and response.annotations[0].mask is not None: masks = np.array( [annotation.mask for annotation in response.annotations], dtype=np.bool_ ) return boxes, scores, labels, masks def nms_bboxes( boxes: np.ndarray, scores: np.ndarray, labels: np.ndarray, iou_threshold: float, score_threshold: float, max_num_detections: int, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: if len(boxes) == 0: return boxes, scores, labels, np.empty((0,), dtype=np.int32) num_classes: int = max(labels) + 1 scores_of_all_classes: NDArray[np.float32] = np.zeros( (len(boxes), num_classes), dtype=np.float32 ) for i, (score, label) in enumerate(zip(scores, labels)): scores_of_all_classes[i, label] = score logger.debug( "Running NMS: iou_threshold={}, score_threshold={}, max_num_detections={}", iou_threshold, score_threshold, max_num_detections, ) logger.debug(f"Input: num_boxes={len(boxes)}") boxes, scores, labels, indices = osam.apis.non_maximum_suppression( boxes=boxes, scores=scores_of_all_classes, iou_threshold=iou_threshold, score_threshold=score_threshold, max_num_detections=max_num_detections, ) logger.debug(f"Output: num_boxes={len(boxes)}") return boxes, scores, labels, indices def get_shapes_from_bboxes( boxes: np.ndarray, scores: np.ndarray, labels: np.ndarray, texts: list[str], masks: NDArray[np.bool_] | None, shape_type: Literal["rectangle", "polygon", "mask"], ) -> list[Shape]: shapes: list[Shape] = [] for i, (box, score, label) in enumerate(zip(boxes, scores, labels)): text: str = texts[label] xmin, ymin, xmax, ymax = box points: list[list[float]] mask: NDArray[np.bool_] | None = None if shape_type == "rectangle": points = [[xmin, ymin], [xmax, ymax]] elif shape_type == "polygon": if masks is None: points = [ [xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax], [xmin, ymin], ] else: points = compute_polygon_from_mask(mask=masks[i]).tolist() elif shape_type == "mask": xmin = int(xmin) ymin = int(ymin) xmax = int(xmax) ymax = int(ymax) points = [[xmin, ymin], [xmax, ymax]] if masks is None: mask = np.zeros((ymax - ymin, xmax - xmin), dtype=bool) else: mask = masks[i][ymin : ymax + 1, xmin : xmax + 1] else: raise ValueError(f"Unsupported shape_type: {shape_type!r}") shape = Shape( label=text, shape_type=shape_type, mask=mask, description=json.dumps(dict(score=score.item(), text=text)), ) for point in points: shape.addPoint(QtCore.QPointF(point[0], point[1])) shapes.append(shape) return shapes ================================================ FILE: labelme/_automation/polygon_from_mask.py ================================================ import imgviz import numpy as np import skimage from loguru import logger from numpy.typing import NDArray def _get_contour_length(contour: NDArray[np.float32]) -> float: contour_start: NDArray[np.float32] = contour contour_end: NDArray[np.float32] = np.r_[contour[1:], contour[0:1]] return np.linalg.norm(contour_end - contour_start, axis=1).sum() def compute_polygon_from_mask(mask: NDArray[np.bool_]) -> NDArray[np.float32]: contours: NDArray[np.float32] = skimage.measure.find_contours( np.pad(mask, pad_width=1) ) if len(contours) == 0: logger.warning("No contour found, so returning empty polygon.") return np.empty((0, 2), dtype=np.float32) contour: NDArray[np.float32] = max(contours, key=_get_contour_length) POLYGON_APPROX_TOLERANCE: float = 0.004 polygon: NDArray[np.float32] = skimage.measure.approximate_polygon( coords=contour, tolerance=np.ptp(contour, axis=0).max() * POLYGON_APPROX_TOLERANCE, ) polygon = np.clip(polygon, (0, 0), (mask.shape[0] - 1, mask.shape[1] - 1)) polygon = polygon[:-1] # drop last point that is duplicate of first point if 0: import PIL.Image image_pil = PIL.Image.fromarray(imgviz.gray2rgb(imgviz.bool2ubyte(mask))) imgviz.draw.line_(image_pil, yx=polygon, fill=(0, 255, 0)) for point in polygon: imgviz.draw.circle_(image_pil, center=point, diameter=10, fill=(0, 255, 0)) imgviz.io.imsave("contour.jpg", np.asarray(image_pil)) return polygon[:, ::-1] # yx -> xy ================================================ FILE: labelme/_label_file.py ================================================ from __future__ import annotations import base64 import contextlib import io import json import os.path as osp import time from pathlib import PureWindowsPath from typing import TypedDict import numpy as np import PIL.Image import tifffile from loguru import logger from numpy.typing import NDArray from labelme import __version__ from labelme import utils PIL.Image.MAX_IMAGE_PIXELS = None @contextlib.contextmanager def _open(name, mode): assert mode in ["r", "w"] encoding = "utf-8" yield open(name, mode, encoding=encoding) return class ShapeDict(TypedDict): label: str points: list[list[float]] shape_type: str flags: dict[str, bool] description: str group_id: int | None mask: NDArray[np.bool_] | None other_data: dict def _load_shape_json_obj(shape_json_obj: dict) -> ShapeDict: SHAPE_KEYS: set[str] = { "label", "points", "group_id", "shape_type", "flags", "description", "mask", } if "label" not in shape_json_obj: raise ValueError(f"label is required: {shape_json_obj}") if not isinstance(shape_json_obj["label"], str): raise TypeError(f"label must be str: {shape_json_obj['label']}") label: str = shape_json_obj["label"] if "points" not in shape_json_obj: raise ValueError(f"points is required: {shape_json_obj}") if not isinstance(shape_json_obj["points"], list): raise TypeError(f"points must be list: {shape_json_obj['points']}") if not shape_json_obj["points"]: raise ValueError(f"points must be non-empty: {shape_json_obj}") if not all( isinstance(point, list) and len(point) == 2 and all(isinstance(xy, int | float) for xy in point) for point in shape_json_obj["points"] ): raise ValueError(f"points must be list of [x, y]: {shape_json_obj['points']}") points: list[list[float]] = shape_json_obj["points"] if "shape_type" not in shape_json_obj: raise ValueError(f"shape_type is required: {shape_json_obj}") if not isinstance(shape_json_obj["shape_type"], str): raise TypeError(f"shape_type must be str: {shape_json_obj['shape_type']}") shape_type: str = shape_json_obj["shape_type"] flags: dict = {} if shape_json_obj.get("flags") is not None: if not isinstance(shape_json_obj["flags"], dict): raise TypeError(f"flags must be dict: {shape_json_obj['flags']}") if not all( isinstance(k, str) and isinstance(v, bool) for k, v in shape_json_obj["flags"].items() ): raise TypeError( f"flags must be dict of str to bool: {shape_json_obj['flags']}" ) flags = shape_json_obj["flags"] description: str = "" if shape_json_obj.get("description") is not None: if not isinstance(shape_json_obj["description"], str): raise TypeError(f"description must be str: {shape_json_obj['description']}") description = shape_json_obj["description"] group_id: int | None = None if shape_json_obj.get("group_id") is not None: if not isinstance(shape_json_obj["group_id"], int): raise TypeError(f"group_id must be int: {shape_json_obj['group_id']}") group_id = shape_json_obj["group_id"] mask: NDArray[np.bool_] | None = None if shape_json_obj.get("mask") is not None: if not isinstance(shape_json_obj["mask"], str): raise TypeError( f"mask must be base64-encoded PNG: {shape_json_obj['mask']}" ) mask = utils.img_b64_to_arr(shape_json_obj["mask"]).astype(bool) other_data = {k: v for k, v in shape_json_obj.items() if k not in SHAPE_KEYS} loaded: ShapeDict = ShapeDict( label=label, points=points, shape_type=shape_type, flags=flags, description=description, group_id=group_id, mask=mask, other_data=other_data, ) if set(loaded.keys()) != SHAPE_KEYS | {"other_data"}: raise RuntimeError( f"unexpected keys: {set(loaded.keys())} != {SHAPE_KEYS | {'other_data'}}" ) return loaded class LabelFileError(Exception): pass class LabelFile: shapes: list[ShapeDict] suffix = ".json" def __init__(self, filename: str | None = None): self.shapes: list[ShapeDict] = [] self.imagePath: str | None = None self.imageData: bytes | None = None if filename is not None: self.load(filename) self.filename: str | None = filename @staticmethod def load_image_file(filename): t0 = time.time() image_pil = _imread(filename=filename) oriented: PIL.Image.Image = utils.apply_exif_orientation(image_pil) ext = osp.splitext(filename)[1].lower() if oriented is image_pil and ext in (".jpg", ".jpeg", ".png"): # no encoding needed with open(filename, "rb") as f: imageData = f.read() else: with io.BytesIO() as f: format = "PNG" if "A" in oriented.mode else "JPEG" oriented.save(f, format=format, quality=95) f.seek(0) imageData = f.read() logger.debug( "Loaded image file: {!r} in {:.0f}ms", filename, (time.time() - t0) * 1000 ) return imageData def load(self, filename): keys = [ "version", "imageData", "imagePath", "shapes", # polygonal annotations "flags", # image level flags "imageHeight", "imageWidth", ] try: with _open(filename, "r") as f: data = json.load(f) # Normalize Windows-style backslash paths to POSIX forward slashes imagePath = PureWindowsPath(data["imagePath"]).as_posix() if data["imageData"] is not None: imageData = base64.b64decode(data["imageData"]) else: # relative path from label file to relative path from cwd imageData = self.load_image_file( osp.join(osp.dirname(filename), imagePath) ) flags = data.get("flags") or {} self._check_image_height_and_width( imageData, data.get("imageHeight"), data.get("imageWidth"), ) shapes: list[ShapeDict] = [ _load_shape_json_obj(shape_json_obj=s) for s in data["shapes"] ] except Exception as e: raise LabelFileError(e) otherData = {} for key, value in data.items(): if key not in keys: otherData[key] = value # Only replace data after everything is loaded. self.flags = flags self.shapes = shapes self.imagePath = imagePath self.imageData = imageData self.filename = filename self.otherData = otherData @staticmethod def _check_image_height_and_width(imageData, imageHeight, imageWidth): img_pil = utils.img_data_to_pil(imageData) actual_w, actual_h = img_pil.size if imageHeight is not None and actual_h != imageHeight: logger.error( "imageHeight does not match with imageData or imagePath, " "so getting imageHeight from actual image." ) imageHeight = actual_h if imageWidth is not None and actual_w != imageWidth: logger.error( "imageWidth does not match with imageData or imagePath, " "so getting imageWidth from actual image." ) imageWidth = actual_w return imageHeight, imageWidth def save( self, filename, shapes, imagePath, imageHeight, imageWidth, imageData=None, otherData=None, flags=None, ): if imageData is not None: imageHeight, imageWidth = self._check_image_height_and_width( imageData, imageHeight, imageWidth ) imageData = base64.b64encode(imageData).decode("utf-8") if otherData is None: otherData = {} if flags is None: flags = {} data = dict( version=__version__, flags=flags, shapes=shapes, imagePath=imagePath, imageData=imageData, imageHeight=imageHeight, imageWidth=imageWidth, ) for key, value in otherData.items(): assert key not in data data[key] = value try: with _open(filename, "w") as f: json.dump(data, f, ensure_ascii=False, indent=2) self.filename = filename except Exception as e: raise LabelFileError(e) @staticmethod def is_label_file(filename): return osp.splitext(filename)[1].lower() == LabelFile.suffix _DISPLAYABLE_MODES = {"1", "L", "P", "RGB", "RGBA", "LA", "PA"} def _imread(filename: str) -> PIL.Image.Image: ext: str = osp.splitext(filename)[1].lower() try: image_pil = PIL.Image.open(filename) if image_pil.mode not in _DISPLAYABLE_MODES: raise PIL.UnidentifiedImageError return image_pil except PIL.UnidentifiedImageError: if ext in (".tif", ".tiff"): return _imread_tiff(filename) raise def _imread_tiff(filename: str) -> PIL.Image.Image: img_arr: NDArray = tifffile.imread(filename) if img_arr.ndim == 2: img_arr_normalized = _normalize_to_uint8(img_arr) elif img_arr.ndim == 3: if img_arr.shape[2] >= 3: img_arr_normalized = np.stack( [_normalize_to_uint8(img_arr[:, :, i]) for i in range(3)], axis=2, ) else: img_arr_normalized = _normalize_to_uint8(img_arr[:, :, 0]) else: raise OSError(f"Unsupported image shape: {img_arr.shape}") return PIL.Image.fromarray(img_arr_normalized) def _normalize_to_uint8(arr: NDArray) -> NDArray[np.uint8]: arr = arr.astype(np.float64) min_val = np.nanmin(arr) max_val = np.nanmax(arr) if np.isnan(min_val) or np.isnan(max_val) or max_val - min_val == 0: return np.zeros(arr.shape, dtype=np.uint8) normalized = (arr - min_val) / (max_val - min_val) * 255 return np.clip(normalized, 0, 255).astype(np.uint8) ================================================ FILE: labelme/app.py ================================================ from __future__ import annotations import enum import functools import html import math import os import os.path as osp import platform import re import subprocess import time import webbrowser from collections.abc import Callable from pathlib import Path from typing import Literal from typing import NamedTuple import imgviz import natsort import numpy as np import osam from loguru import logger from numpy.typing import NDArray from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox from labelme import __appname__ from labelme import __version__ from labelme._automation import bbox_from_text from labelme._automation._osam_session import OsamSession from labelme._label_file import LabelFile from labelme._label_file import LabelFileError from labelme._label_file import ShapeDict from labelme.config import load_config from labelme.shape import Shape from labelme.widgets import AiAssistedAnnotationWidget from labelme.widgets import AiTextToAnnotationWidget from labelme.widgets import BrightnessContrastDialog from labelme.widgets import Canvas from labelme.widgets import FileDialogPreview from labelme.widgets import LabelDialog from labelme.widgets import LabelListWidget from labelme.widgets import LabelListWidgetItem from labelme.widgets import StatusStats from labelme.widgets import ToolBar from labelme.widgets import UniqueLabelQListWidget from labelme.widgets import ZoomWidget from labelme.widgets import download_ai_model from . import utils # handle high-dpi scaling issue # https://leomoon.com/journal/python/high-dpi-scaling-in-pyqt5 if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"): QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) if hasattr(QtCore.Qt, "AA_UseHighDpiPixmaps"): QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) LABEL_COLORMAP: NDArray[np.uint8] = imgviz.label_colormap() class _ZoomMode(enum.Enum): FIT_WINDOW = enum.auto() FIT_WIDTH = enum.auto() MANUAL_ZOOM = enum.auto() _AI_TEXT_TO_ANNOTATION_CREATE_MODE_TO_SHAPE_TYPE: dict[ str, Literal["mask", "polygon", "rectangle"] ] = { "ai_mask": "mask", "ai_polygon": "polygon", "polygon": "polygon", "rectangle": "rectangle", } class _StatusBarWidgets(NamedTuple): message: QtWidgets.QLabel stats: StatusStats class _CanvasWidgets(NamedTuple): canvas: Canvas zoom_widget: ZoomWidget scroll_bars: dict[Qt.Orientation, QtWidgets.QScrollBar] class _DockWidgets(NamedTuple): flag_dock: QtWidgets.QDockWidget flag_list: QtWidgets.QListWidget shape_dock: QtWidgets.QDockWidget label_list: LabelListWidget label_dock: QtWidgets.QDockWidget unique_label_list: UniqueLabelQListWidget file_dock: QtWidgets.QDockWidget file_search: QtWidgets.QLineEdit file_list: QtWidgets.QListWidget class _Actions(NamedTuple): about: QtWidgets.QAction save: QtWidgets.QAction save_as: QtWidgets.QAction save_auto: QtWidgets.QAction save_with_image_data: QtWidgets.QAction change_output_dir: QtWidgets.QAction open: QtWidgets.QAction close: QtWidgets.QAction delete_file: QtWidgets.QAction toggle_keep_prev_mode: QtWidgets.QAction toggle_keep_prev_brightness_contrast: QtWidgets.QAction delete: QtWidgets.QAction edit: QtWidgets.QAction duplicate: QtWidgets.QAction copy: QtWidgets.QAction paste: QtWidgets.QAction undo_last_point: QtWidgets.QAction undo: QtWidgets.QAction remove_point: QtWidgets.QAction create_mode: QtWidgets.QAction edit_mode: QtWidgets.QAction create_rectangle_mode: QtWidgets.QAction create_circle_mode: QtWidgets.QAction create_line_mode: QtWidgets.QAction create_point_mode: QtWidgets.QAction create_line_strip_mode: QtWidgets.QAction create_ai_polygon_mode: QtWidgets.QAction create_ai_mask_mode: QtWidgets.QAction open_next_img: QtWidgets.QAction open_prev_img: QtWidgets.QAction keep_prev_scale: QtWidgets.QAction fit_window: QtWidgets.QAction fit_width: QtWidgets.QAction brightness_contrast: QtWidgets.QAction zoom_in: QtWidgets.QAction zoom_out: QtWidgets.QAction zoom_org: QtWidgets.QAction reset_layout: QtWidgets.QAction fill_drawing: QtWidgets.QAction hide_all: QtWidgets.QAction show_all: QtWidgets.QAction toggle_all: QtWidgets.QAction open_dir: QtWidgets.QAction zoom_widget_action: QtWidgets.QWidgetAction draw: list[tuple[str, QtWidgets.QAction]] zoom: tuple[ZoomWidget | QtWidgets.QAction, ...] on_load_active: tuple[QtWidgets.QAction, ...] on_shapes_present: tuple[QtWidgets.QAction, ...] context_menu: tuple[QtWidgets.QAction, ...] edit_menu: tuple[QtWidgets.QAction | None, ...] class _Menus(NamedTuple): file: QtWidgets.QMenu edit: QtWidgets.QMenu view: QtWidgets.QMenu help: QtWidgets.QMenu recent_files: QtWidgets.QMenu label_list: QtWidgets.QMenu class MainWindow(QtWidgets.QMainWindow): _config_file: Path | None _config: dict _text_osam_session: OsamSession | None = None _is_changed: bool = False _copied_shapes: list[Shape] _zoom_mode: _ZoomMode _prev_opened_dir: str | None _canvas_widgets: _CanvasWidgets _status_bar: _StatusBarWidgets _docks: _DockWidgets _actions: _Actions _menus: _Menus _scalers: dict[_ZoomMode, Callable[[], float]] _label_dialog: LabelDialog _ai_annotation: AiAssistedAnnotationWidget _ai_text: AiTextToAnnotationWidget _output_dir: str | None _filename: str | None _image: QtGui.QImage _label_file: LabelFile | None _image_path: str | None _recent_files: list[str] _max_recent: int _other_data: dict | None _zoom_values: dict[str, tuple[_ZoomMode, int]] _brightness_contrast_values: dict[str, tuple[int | None, int | None]] _scroll_values: dict[Qt.Orientation, dict[str, float]] _default_state: QtCore.QByteArray def __init__( self, config_file: Path | None = None, config_overrides: dict | None = None, filename: str | None = None, output_dir: str | None = None, ) -> None: super().__init__() self.setWindowTitle(__appname__) self._config_file, self._config = self._load_config( config_file=config_file, config_overrides=config_overrides ) # set default shape colors Shape.line_color = QtGui.QColor(*self._config["shape"]["line_color"]) Shape.fill_color = QtGui.QColor(*self._config["shape"]["fill_color"]) Shape.select_line_color = QtGui.QColor( *self._config["shape"]["select_line_color"] ) Shape.select_fill_color = QtGui.QColor( *self._config["shape"]["select_fill_color"] ) Shape.vertex_fill_color = QtGui.QColor( *self._config["shape"]["vertex_fill_color"] ) Shape.hvertex_fill_color = QtGui.QColor( *self._config["shape"]["hvertex_fill_color"] ) # Set point size from config file Shape.point_size = self._config["shape"]["point_size"] self._copied_shapes = [] self._label_dialog = LabelDialog( parent=self, labels=self._config["labels"], sort_labels=self._config["sort_labels"], show_text_field=self._config["show_label_text_field"], completion=self._config["label_completion"], fit_to_content=self._config["fit_to_content"], flags=self._config["label_flags"], ) self._prev_opened_dir = None self._docks = self._setup_dock_widgets() self.setAcceptDrops(True) self._canvas_widgets = self._setup_canvas() self._actions = self._setup_actions() self._scalers = { _ZoomMode.FIT_WINDOW: self.scaleFitWindow, _ZoomMode.FIT_WIDTH: self.scaleFitWidth, _ZoomMode.MANUAL_ZOOM: lambda: 1, } self._menus = self._setup_menus() self._ai_annotation = AiAssistedAnnotationWidget( default_model=self._config["ai"]["default"], on_model_changed=self._canvas_widgets.canvas.set_ai_model_name, parent=self, ) self._ai_annotation.setEnabled(False) self._ai_text = AiTextToAnnotationWidget( on_submit=self._submit_ai_prompt, parent=self ) self._ai_text.setEnabled(False) self._setup_toolbars() self._status_bar = self._setup_status_bar() self._setup_app_state(output_dir=output_dir, filename=filename) self.updateFileMenu() self._canvas_widgets.zoom_widget.valueChanged.connect(self._paint_canvas) self.populateModeActions() def _setup_actions(self) -> _Actions: action = functools.partial(utils.newAction, self) shortcuts = self._config["shortcuts"] about = action( text=f"&About {__appname__}", slot=functools.partial( QMessageBox.about, self, f"About {__appname__}", f"""

{__appname__}

Image Polygonal Annotation with Python

Version: {__version__}

Author: Kentaro Wada

Homepage | Documentation | Troubleshooting

GitHub | Twitter/X

""", ), ) save = action( text=self.tr("&Save\n"), slot=self.saveFile, shortcut=shortcuts["save"], icon="floppy-disk.svg", tip=self.tr("Save labels to file"), enabled=False, ) save_as = action( text=self.tr("&Save As"), slot=self.saveFileAs, shortcut=shortcuts["save_as"], icon="floppy-disk.svg", tip=self.tr("Save labels to a different file"), enabled=False, ) save_auto = action( text=self.tr("Save &Automatically"), tip=self.tr("Save automatically"), checkable=True, enabled=True, ) save_auto.setChecked(self._config["auto_save"]) save_with_image_data = action( text=self.tr("Save With Image Data"), slot=self.enableSaveImageWithData, tip=self.tr("Save image data in label file"), checkable=True, checked=self._config["with_image_data"], ) change_output_dir = action( text=self.tr("&Change Output Dir"), slot=self.changeOutputDirDialog, shortcut=shortcuts["save_to"], icon="folders.svg", tip=self.tr("Change where annotations are loaded/saved"), ) open_ = action( text=self.tr("&Open\n"), slot=self._open_file_with_dialog, shortcut=shortcuts["open"], icon="folder-open.svg", tip=self.tr("Open image or label file"), ) open_dir = action( text=self.tr("Open Dir"), slot=self._open_dir_with_dialog, shortcut=shortcuts["open_dir"], icon="folder-open.svg", tip=self.tr("Open Dir"), ) close = action( text=self.tr("&Close"), slot=self.closeFile, shortcut=shortcuts["close"], icon="x-circle.svg", tip=self.tr("Close current file"), ) delete_file = action( text=self.tr("&Delete File"), slot=self.deleteFile, shortcut=shortcuts["delete_file"], icon="file-x.svg", tip=self.tr("Delete current label file"), enabled=False, ) toggle_keep_prev_mode = action( text=self.tr("Keep Previous Annotation"), slot=self.toggleKeepPrevMode, shortcut=shortcuts["toggle_keep_prev_mode"], icon=None, tip=self.tr('Toggle "keep previous annotation" mode'), checkable=True, ) toggle_keep_prev_mode.setChecked(self._config["keep_prev"]) toggle_keep_prev_brightness_contrast = action( text=self.tr("Keep Previous Brightness/Contrast"), slot=lambda: self._config.__setitem__( "keep_prev_brightness_contrast", not self._config["keep_prev_brightness_contrast"], ), checkable=True, checked=self._config["keep_prev_brightness_contrast"], ) delete = action( self.tr("Delete Shapes"), self.deleteSelectedShape, shortcuts["delete_shape"], icon="trash.svg", tip=self.tr("Delete the selected shapes"), enabled=False, ) edit = action( self.tr("&Edit Label"), self._edit_label, shortcuts["edit_label"], icon="note-pencil.svg", tip=self.tr("Modify the label of the selected shape"), enabled=False, ) duplicate = action( self.tr("Duplicate Shapes"), self.duplicateSelectedShape, shortcuts["duplicate_shape"], icon="copy.svg", tip=self.tr("Create a duplicate of the selected shapes"), enabled=False, ) copy = action( self.tr("Copy Shapes"), self.copySelectedShape, shortcuts["copy_shape"], "copy_clipboard", self.tr("Copy selected shapes to clipboard"), enabled=False, ) paste = action( self.tr("Paste Shapes"), self.pasteSelectedShape, shortcuts["paste_shape"], "paste", self.tr("Paste copied shapes"), enabled=False, ) undo_last_point = action( self.tr("Undo last point"), self._canvas_widgets.canvas.undoLastPoint, shortcuts["undo_last_point"], icon="arrow-u-up-left.svg", tip=self.tr("Undo last drawn point"), enabled=False, ) undo = action( self.tr("Undo\n"), self.undoShapeEdit, shortcuts["undo"], icon="arrow-u-up-left.svg", tip=self.tr("Undo last add and edit of shape"), enabled=False, ) remove_point = action( text=self.tr("Remove Selected Point"), slot=self.removeSelectedPoint, shortcut=shortcuts["remove_selected_point"], icon="trash.svg", tip=self.tr("Remove selected point from polygon"), enabled=False, ) create_mode = action( text=self.tr("Create Polygons"), slot=lambda: self._switch_canvas_mode(edit=False, createMode="polygon"), shortcut=shortcuts["create_polygon"], icon="polygon.svg", tip=self.tr("Start drawing polygons"), enabled=False, ) edit_mode = action( self.tr("Edit Shapes"), lambda: self._switch_canvas_mode(edit=True), shortcuts["edit_shape"], icon="note-pencil.svg", tip=self.tr("Move and edit the selected shapes"), enabled=False, ) create_rectangle_mode = action( text=self.tr("Create Rectangle"), slot=lambda: self._switch_canvas_mode(edit=False, createMode="rectangle"), shortcut=shortcuts["create_rectangle"], icon="rectangle.svg", tip=self.tr("Start drawing rectangles"), enabled=False, ) create_circle_mode = action( text=self.tr("Create Circle"), slot=lambda: self._switch_canvas_mode(edit=False, createMode="circle"), shortcut=shortcuts["create_circle"], icon="circle.svg", tip=self.tr("Start drawing circles"), enabled=False, ) create_line_mode = action( text=self.tr("Create Line"), slot=lambda: self._switch_canvas_mode(edit=False, createMode="line"), shortcut=shortcuts["create_line"], icon="line-segment.svg", tip=self.tr("Start drawing lines"), enabled=False, ) create_point_mode = action( text=self.tr("Create Point"), slot=lambda: self._switch_canvas_mode(edit=False, createMode="point"), shortcut=shortcuts["create_point"], icon="circles-four.svg", tip=self.tr("Start drawing points"), enabled=False, ) create_line_strip_mode = action( text=self.tr("Create LineStrip"), slot=lambda: self._switch_canvas_mode(edit=False, createMode="linestrip"), shortcut=shortcuts["create_linestrip"], icon="line-segments.svg", tip=self.tr("Start drawing linestrip. Ctrl+LeftClick ends creation."), enabled=False, ) create_ai_polygon_mode = action( self.tr("Create AI-Polygon"), lambda: self._switch_canvas_mode(edit=False, createMode="ai_polygon"), None, "ai-polygon.svg", self.tr("Start drawing ai_polygon. Ctrl+LeftClick ends creation."), enabled=False, ) create_ai_mask_mode = action( self.tr("Create AI-Mask"), lambda: self._switch_canvas_mode(edit=False, createMode="ai_mask"), None, "ai-mask.svg", self.tr("Start drawing ai_mask. Ctrl+LeftClick ends creation."), enabled=False, ) open_next_img = action( text=self.tr("&Next Image"), slot=self._open_next_image, shortcut=shortcuts["open_next"], icon="arrow-fat-right.svg", tip=self.tr("Open next (hold Ctl+Shift to copy labels)"), enabled=False, ) open_prev_img = action( text=self.tr("&Prev Image"), slot=self._open_prev_image, shortcut=shortcuts["open_prev"], icon="arrow-fat-left.svg", tip=self.tr("Open prev (hold Ctl+Shift to copy labels)"), enabled=False, ) keep_prev_scale = action( self.tr("&Keep Previous Scale"), self.enableKeepPrevScale, tip=self.tr("Keep previous zoom scale"), checkable=True, checked=self._config["keep_prev_scale"], enabled=True, ) fit_window = action( self.tr("&Fit Window"), self.setFitWindow, shortcuts["fit_window"], icon="frame-corners.svg", tip=self.tr("Zoom follows window size"), checkable=True, enabled=False, ) fit_width = action( self.tr("Fit &Width"), self.setFitWidth, shortcuts["fit_width"], icon="frame-arrows-horizontal.svg", tip=self.tr("Zoom follows window width"), checkable=True, enabled=False, ) brightness_contrast = action( self.tr("&Brightness Contrast"), self.brightnessContrast, None, "brightness-contrast.svg", self.tr("Adjust brightness and contrast"), enabled=False, ) zoom_in = action( self.tr("Zoom &In"), lambda _: self._add_zoom(increment=1.1), shortcuts["zoom_in"], icon="magnifying-glass-minus.svg", tip=self.tr("Increase zoom level"), enabled=False, ) zoom_out = action( self.tr("&Zoom Out"), lambda _: self._add_zoom(increment=0.9), shortcuts["zoom_out"], icon="magnifying-glass-plus.svg", tip=self.tr("Decrease zoom level"), enabled=False, ) zoom_org = action( self.tr("&Original size"), self._set_zoom_to_original, shortcuts["zoom_to_original"], icon="image-square.svg", tip=self.tr("Zoom to original size"), enabled=False, ) reset_layout = action( text=self.tr("Reset Layout"), slot=self._reset_layout, icon="layout-duotone.svg", ) fill_drawing = action( self.tr("Fill Drawing Polygon"), self._canvas_widgets.canvas.setFillDrawing, None, icon="paint-bucket.svg", tip=self.tr("Fill polygon while drawing"), checkable=True, enabled=True, ) if self._config["canvas"]["fill_drawing"]: fill_drawing.trigger() hide_all = action( self.tr("&Hide\nShapes"), functools.partial(self.toggleShapes, False), shortcuts["hide_all_shapes"], icon="eye.svg", tip=self.tr("Hide all shapes"), enabled=False, ) show_all = action( self.tr("&Show\nShapes"), functools.partial(self.toggleShapes, True), shortcuts["show_all_shapes"], icon="eye.svg", tip=self.tr("Show all shapes"), enabled=False, ) toggle_all = action( self.tr("&Toggle\nShapes"), functools.partial(self.toggleShapes, None), shortcuts["toggle_all_shapes"], icon="eye.svg", tip=self.tr("Toggle all shapes"), enabled=False, ) zoom_widget_action = QtWidgets.QWidgetAction(self) zoom_box_layout = QtWidgets.QVBoxLayout() zoom_label = QtWidgets.QLabel(self.tr("Zoom")) zoom_label.setAlignment(Qt.AlignCenter) zoom_box_layout.addWidget(zoom_label) zoom_box_layout.addWidget(self._canvas_widgets.zoom_widget) zoom_widget_action.setDefaultWidget(QtWidgets.QWidget()) zoom_widget_action.defaultWidget().setLayout(zoom_box_layout) self._canvas_widgets.zoom_widget.setWhatsThis( str( self.tr( "Zoom in or out of the image. Also accessible with " "{} and {} from the canvas." ) ).format( utils.fmtShortcut(f"{shortcuts['zoom_in']},{shortcuts['zoom_out']}"), utils.fmtShortcut(self.tr("Ctrl+Wheel")), ) ) self._canvas_widgets.zoom_widget.setEnabled(False) self._zoom_mode = _ZoomMode.FIT_WINDOW fit_window.setChecked(Qt.Checked) self._canvas_widgets.canvas.vertexSelected.connect(remove_point.setEnabled) draw = [ ("polygon", create_mode), ("rectangle", create_rectangle_mode), ("circle", create_circle_mode), ("point", create_point_mode), ("line", create_line_mode), ("linestrip", create_line_strip_mode), ("ai_polygon", create_ai_polygon_mode), ("ai_mask", create_ai_mask_mode), ] zoom = ( self._canvas_widgets.zoom_widget, zoom_in, zoom_out, zoom_org, fit_window, fit_width, ) on_load_active = ( close, create_mode, create_rectangle_mode, create_circle_mode, create_line_mode, create_point_mode, create_line_strip_mode, create_ai_polygon_mode, create_ai_mask_mode, brightness_contrast, ) on_shapes_present = (save_as, hide_all, show_all, toggle_all) context_menu = ( *[draw_action for _, draw_action in draw], edit_mode, edit, duplicate, copy, paste, delete, undo, undo_last_point, remove_point, ) edit_menu = ( edit, duplicate, copy, paste, delete, None, undo, undo_last_point, None, remove_point, None, toggle_keep_prev_mode, ) return _Actions( about=about, save=save, save_as=save_as, save_auto=save_auto, save_with_image_data=save_with_image_data, change_output_dir=change_output_dir, open=open_, close=close, delete_file=delete_file, toggle_keep_prev_mode=toggle_keep_prev_mode, toggle_keep_prev_brightness_contrast=toggle_keep_prev_brightness_contrast, delete=delete, edit=edit, duplicate=duplicate, copy=copy, paste=paste, undo_last_point=undo_last_point, undo=undo, remove_point=remove_point, create_mode=create_mode, edit_mode=edit_mode, create_rectangle_mode=create_rectangle_mode, create_circle_mode=create_circle_mode, create_line_mode=create_line_mode, create_point_mode=create_point_mode, create_line_strip_mode=create_line_strip_mode, create_ai_polygon_mode=create_ai_polygon_mode, create_ai_mask_mode=create_ai_mask_mode, open_next_img=open_next_img, open_prev_img=open_prev_img, keep_prev_scale=keep_prev_scale, fit_window=fit_window, fit_width=fit_width, brightness_contrast=brightness_contrast, zoom_in=zoom_in, zoom_out=zoom_out, zoom_org=zoom_org, reset_layout=reset_layout, fill_drawing=fill_drawing, hide_all=hide_all, show_all=show_all, toggle_all=toggle_all, open_dir=open_dir, zoom_widget_action=zoom_widget_action, draw=draw, zoom=zoom, on_load_active=on_load_active, on_shapes_present=on_shapes_present, context_menu=context_menu, edit_menu=edit_menu, ) def _setup_menus(self) -> _Menus: action = functools.partial(utils.newAction, self) shortcuts = self._config["shortcuts"] quit_ = action( text=self.tr("&Quit"), slot=self.close, shortcut=shortcuts["quit"], icon=None, tip=self.tr("Quit application"), ) open_config = action( text=self.tr("Preferences…"), slot=self._open_config_file, shortcut="Ctrl+," if platform.system() == "Darwin" else "Ctrl+Shift+,", icon=None, tip=self.tr("Open config file in text editor"), ) open_config.setMenuRole(QtWidgets.QAction.PreferencesRole) help_ = action( self.tr("&Tutorial"), self.tutorial, icon="question.svg", tip=self.tr("Show tutorial page"), ) file_menu = self.menu(self.tr("&File")) edit_menu = self.menu(self.tr("&Edit")) view_menu = self.menu(self.tr("&View")) help_menu = self.menu(self.tr("&Help")) recent_files = QtWidgets.QMenu(self.tr("Open &Recent")) label_menu = QtWidgets.QMenu() utils.addActions(label_menu, (self._actions.edit, self._actions.delete)) self._docks.label_list.setContextMenuPolicy(Qt.CustomContextMenu) self._docks.label_list.customContextMenuRequested.connect(self.popLabelListMenu) utils.addActions( file_menu, ( self._actions.open, self._actions.open_next_img, self._actions.open_prev_img, self._actions.open_dir, recent_files, self._actions.save, self._actions.save_as, self._actions.save_auto, self._actions.change_output_dir, self._actions.save_with_image_data, self._actions.close, self._actions.delete_file, None, open_config, None, quit_, ), ) utils.addActions(help_menu, (help_, self._actions.about)) utils.addActions( view_menu, ( self._docks.flag_dock.toggleViewAction(), self._docks.label_dock.toggleViewAction(), self._docks.shape_dock.toggleViewAction(), self._docks.file_dock.toggleViewAction(), None, self._actions.reset_layout, None, self._actions.fill_drawing, None, self._actions.hide_all, self._actions.show_all, self._actions.toggle_all, None, self._actions.zoom_in, self._actions.zoom_out, self._actions.zoom_org, self._actions.keep_prev_scale, None, self._actions.fit_window, self._actions.fit_width, None, self._actions.brightness_contrast, self._actions.toggle_keep_prev_brightness_contrast, ), ) file_menu.aboutToShow.connect(self.updateFileMenu) utils.addActions( self._canvas_widgets.canvas.menus[0], self._actions.context_menu ) utils.addActions( self._canvas_widgets.canvas.menus[1], ( action("&Copy here", self.copyShape), action("&Move here", self.moveShape), ), ) return _Menus( file=file_menu, edit=edit_menu, view=view_menu, help=help_menu, recent_files=recent_files, label_list=label_menu, ) def _setup_toolbars(self) -> None: select_ai_model = QtWidgets.QWidgetAction(self) select_ai_model.setDefaultWidget(self._ai_annotation) ai_prompt_action = QtWidgets.QWidgetAction(self) ai_prompt_action.setDefaultWidget(self._ai_text) self.addToolBar( Qt.TopToolBarArea, ToolBar( title="Tools", actions=[ self._actions.open, self._actions.open_dir, self._actions.open_prev_img, self._actions.open_next_img, self._actions.save, self._actions.delete_file, None, self._actions.edit_mode, self._actions.duplicate, self._actions.delete, self._actions.undo, self._actions.brightness_contrast, None, self._actions.fit_window, self._actions.zoom_widget_action, None, select_ai_model, None, ai_prompt_action, ], font_base=self.font(), ), ) self.addToolBar( Qt.LeftToolBarArea, ToolBar( title="CreateShapeTools", actions=[a for _, a in self._actions.draw], orientation=Qt.Vertical, button_style=Qt.ToolButtonTextUnderIcon, font_base=self.font(), ), ) def _setup_app_state( self, *, output_dir: str | None, filename: str | None, ) -> None: self._output_dir = output_dir self._image = QtGui.QImage() self._label_file = None self._image_path = None self._max_recent = 7 self._other_data = None self._zoom_values = {} self._brightness_contrast_values = {} self._scroll_values = { Qt.Horizontal: {}, Qt.Vertical: {}, } if self._config["file_search"]: self._docks.file_search.setText(self._config["file_search"]) self._default_state = self.saveState() # # XXX: Could be completely declarative. # Restore application settings. self.settings = QtCore.QSettings("labelme", "labelme") # # Bump this when dock/toolbar layout changes to reset window state # for users upgrading from an older version. SETTINGS_VERSION: int = 1 if self.settings.value("settingsVersion", 0, type=int) != SETTINGS_VERSION: self._reset_layout() self.settings.setValue("settingsVersion", SETTINGS_VERSION) # self._recent_files = self.settings.value("recentFiles", []) or [] self.resize(self.settings.value("window/size", QtCore.QSize(900, 500))) self.move(self.settings.value("window/position", QtCore.QPoint(0, 0))) self.restoreState(self.settings.value("window/state", QtCore.QByteArray())) # Recover window position when the saved screen is no longer connected. if not any( s.availableGeometry().intersects(self.frameGeometry()) for s in QtWidgets.QApplication.screens() ) and (primary_screen := QtWidgets.QApplication.primaryScreen()): self.move(primary_screen.availableGeometry().topLeft()) if filename: if osp.isdir(filename): self._import_images_from_dir( root_dir=filename, pattern=self._docks.file_search.text() ) self._open_next_image() else: self._load_file(filename=filename) else: self._filename = None def _setup_status_bar(self) -> _StatusBarWidgets: message = QtWidgets.QLabel(self.tr("%s started.") % __appname__) stats = StatusStats() self.statusBar().addWidget(message, 1) self.statusBar().addWidget(stats, 0) self.statusBar().show() return _StatusBarWidgets(message=message, stats=stats) def _setup_canvas(self) -> _CanvasWidgets: zoom_widget = ZoomWidget() canvas = Canvas( epsilon=self._config["epsilon"], double_click=self._config["canvas"]["double_click"], num_backups=self._config["canvas"]["num_backups"], crosshair=self._config["canvas"]["crosshair"], ) canvas.zoomRequest.connect(self._zoom_requested) canvas.mouseMoved.connect(self._update_status_stats) canvas.statusUpdated.connect( lambda text: self._status_bar.message.setText(text) ) scroll_area = QtWidgets.QScrollArea() scroll_area.setWidget(canvas) scroll_area.setWidgetResizable(True) scroll_bars = { Qt.Vertical: scroll_area.verticalScrollBar(), Qt.Horizontal: scroll_area.horizontalScrollBar(), } canvas.scrollRequest.connect(self.scrollRequest) canvas.newShape.connect(self.newShape) canvas.shapeMoved.connect(self.setDirty) canvas.selectionChanged.connect(self.shapeSelectionChanged) canvas.drawingPolygon.connect(self.toggleDrawingSensitive) self.setCentralWidget(scroll_area) return _CanvasWidgets( canvas=canvas, zoom_widget=zoom_widget, scroll_bars=scroll_bars, ) def _setup_dock_widgets(self) -> _DockWidgets: flag_list = QtWidgets.QListWidget() flag = QtWidgets.QDockWidget(self.tr("Flags"), self) flag.setObjectName("Flags") if self._config["flags"]: self._load_flags( flags={k: False for k in self._config["flags"]}, widget=flag_list, ) flag.setWidget(flag_list) flag_list.itemChanged.connect(self.setDirty) label_list = LabelListWidget() label_list.itemSelectionChanged.connect(self._label_selection_changed) label_list.itemDoubleClicked.connect(self._edit_label) label_list.itemChanged.connect(self.labelItemChanged) label_list.itemDropped.connect(self.labelOrderChanged) shape = QtWidgets.QDockWidget(self.tr("Annotation List"), self) shape.setObjectName("Labels") shape.setWidget(label_list) unique_label_list = UniqueLabelQListWidget() unique_label_list.setToolTip( self.tr("Select label to start annotating for it. Press 'Esc' to deselect.") ) if self._config["labels"]: for lbl in self._config["labels"]: unique_label_list.add_label_item( label=lbl, color=self._get_rgb_by_label( label=lbl, unique_label_list=unique_label_list ), ) label = QtWidgets.QDockWidget(self.tr("Label List"), self) label.setObjectName("Label List") label.setWidget(unique_label_list) file_search = QtWidgets.QLineEdit() file_search.setPlaceholderText(self.tr("Search Filename")) file_search.textChanged.connect(self.fileSearchChanged) file_list = QtWidgets.QListWidget() file_list.itemSelectionChanged.connect(self.fileSelectionChanged) file_list_layout = QtWidgets.QVBoxLayout() file_list_layout.setContentsMargins(0, 0, 0, 0) file_list_layout.setSpacing(0) file_list_layout.addWidget(file_search) file_list_layout.addWidget(file_list) file = QtWidgets.QDockWidget(self.tr("File List"), self) file.setObjectName("Files") file_list_container = QtWidgets.QWidget() file_list_container.setLayout(file_list_layout) file.setWidget(file_list_container) for config_key, dock_widget in [ ("flag_dock", flag), ("label_dock", label), ("shape_dock", shape), ("file_dock", file), ]: features = QtWidgets.QDockWidget.DockWidgetFeatures() if self._config[config_key]["closable"]: features = features | QtWidgets.QDockWidget.DockWidgetClosable if self._config[config_key]["floatable"]: features = features | QtWidgets.QDockWidget.DockWidgetFloatable if self._config[config_key]["movable"]: features = features | QtWidgets.QDockWidget.DockWidgetMovable dock_widget.setFeatures(features) if self._config[config_key]["show"] is False: dock_widget.setVisible(False) self.addDockWidget(Qt.RightDockWidgetArea, dock_widget) return _DockWidgets( flag_dock=flag, flag_list=flag_list, shape_dock=shape, label_list=label_list, label_dock=label, unique_label_list=unique_label_list, file_dock=file, file_search=file_search, file_list=file_list, ) def _load_config( self, config_file: Path | None, config_overrides: dict | None ) -> tuple[Path | None, dict]: try: config = load_config( config_file=config_file, config_overrides=config_overrides or {} ) except ValueError as e: msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Warning) msg_box.setWindowTitle(self.tr("Configuration Errors")) msg_box.setText( self.tr( "Errors were found while loading the configuration. " "Please review the errors below and reload your configuration or " "ignore the erroneous lines." ) ) msg_box.setInformativeText(str(e)) msg_box.setStandardButtons(QMessageBox.Ignore) msg_box.setModal(False) msg_box.show() config_file = None config_overrides = {} config = load_config( config_file=config_file, config_overrides=config_overrides ) return config_file, config def menu(self, title, actions=None): menu = self.menuBar().addMenu(title) if actions: utils.addActions(menu, actions) return menu # Support Functions def noShapes(self) -> bool: return not len(self._docks.label_list) def populateModeActions(self) -> None: self._canvas_widgets.canvas.menus[0].clear() utils.addActions( self._canvas_widgets.canvas.menus[0], self._actions.context_menu ) self._menus.edit.clear() actions = ( *[draw_action for _, draw_action in self._actions.draw], self._actions.edit_mode, *self._actions.edit_menu, ) utils.addActions(self._menus.edit, actions) def _get_window_title(self, dirty: bool) -> str: window_title: str = __appname__ if self._image_path: window_title = f"{window_title} - {self._image_path}" if self._docks.file_list.count() and self._docks.file_list.currentItem(): window_title = ( f"{window_title} " f"[{self._docks.file_list.currentRow() + 1}" f"/{self._docks.file_list.count()}]" ) if dirty: window_title = f"{window_title}*" return window_title def setDirty(self) -> None: # Even if we autosave the file, we keep the ability to undo self._actions.undo.setEnabled(self._canvas_widgets.canvas.isShapeRestorable) if self._config["auto_save"] or self._actions.save_auto.isChecked(): assert self._image_path label_file = f"{osp.splitext(self._image_path)[0]}.json" if self._output_dir: label_file_without_path = osp.basename(label_file) label_file = osp.join(self._output_dir, label_file_without_path) self.saveLabels(label_file) return self._is_changed = True self._actions.save.setEnabled(True) self.setWindowTitle(self._get_window_title(dirty=True)) def setClean(self) -> None: self._is_changed = False self._actions.save.setEnabled(False) for _, action in self._actions.draw: action.setEnabled(True) self.setWindowTitle(self._get_window_title(dirty=False)) if self.hasLabelFile(): self._actions.delete_file.setEnabled(True) else: self._actions.delete_file.setEnabled(False) def toggleActions(self, value: bool = True) -> None: """Enable/Disable widgets which depend on an opened image.""" for z in self._actions.zoom: z.setEnabled(value) for action in self._actions.on_load_active: action.setEnabled(value) def queueEvent(self, function: Callable[[], None]) -> None: QtCore.QTimer.singleShot(0, function) def show_status_message(self, message: str, delay: int = 500) -> None: self.statusBar().showMessage(message, delay) def _submit_ai_prompt(self, _) -> None: if ( self._canvas_widgets.canvas.createMode not in _AI_TEXT_TO_ANNOTATION_CREATE_MODE_TO_SHAPE_TYPE ): logger.warning( "Unsupported createMode={!r}", self._canvas_widgets.canvas.createMode ) return shape_type: Literal["rectangle", "polygon", "mask"] = ( _AI_TEXT_TO_ANNOTATION_CREATE_MODE_TO_SHAPE_TYPE[ self._canvas_widgets.canvas.createMode ] ) texts = self._ai_text.get_text_prompt().split(",") model_name: str = self._ai_text.get_model_name() model_type = osam.apis.get_model_type_by_name(model_name) if not (_is_already_downloaded := model_type.get_size() is not None): if not download_ai_model(model_name=model_name, parent=self): return if ( self._text_osam_session is None or self._text_osam_session.model_name != model_name ): self._text_osam_session = OsamSession(model_name=model_name) boxes, scores, labels, masks = bbox_from_text.get_bboxes_from_texts( session=self._text_osam_session, image=utils.img_qt_to_arr(self._image)[:, :, :3], image_id=str(hash(self._image_path)), texts=texts, ) SCORE_FOR_EXISTING_SHAPE: float = 1.01 for shape in self._canvas_widgets.canvas.shapes: if shape.shape_type != shape_type or shape.label not in texts: continue points: NDArray[np.float64] = np.array( [[p.x(), p.y()] for p in shape.points] ) xmin, ymin = points.min(axis=0) xmax, ymax = points.max(axis=0) box = np.array([xmin, ymin, xmax, ymax], dtype=np.float32) boxes = np.r_[boxes, [box]] scores = np.r_[scores, [SCORE_FOR_EXISTING_SHAPE]] labels = np.r_[labels, [texts.index(shape.label)]] boxes, scores, labels, indices = bbox_from_text.nms_bboxes( boxes=boxes, scores=scores, labels=labels, iou_threshold=self._ai_text.get_iou_threshold(), score_threshold=self._ai_text.get_score_threshold(), max_num_detections=100, ) is_new = scores != SCORE_FOR_EXISTING_SHAPE boxes = boxes[is_new] scores = scores[is_new] labels = labels[is_new] indices = indices[is_new] if masks is not None: masks = masks[indices] del indices shapes: list[Shape] = bbox_from_text.get_shapes_from_bboxes( boxes=boxes, scores=scores, labels=labels, texts=texts, masks=masks, shape_type=shape_type, ) self._canvas_widgets.canvas.storeShapes() self._load_shapes(shapes, replace=False) self.setDirty() def resetState(self) -> None: self._docks.label_list.clear() self._filename = None self._image_path = None self.imageData = None self._label_file = None self._other_data = None self._canvas_widgets.canvas.resetState() def currentItem(self) -> LabelListWidgetItem | None: items = self._docks.label_list.selectedItems() if items: return items[0] return None def addRecentFile(self, filename: str) -> None: if filename in self._recent_files: self._recent_files.remove(filename) elif len(self._recent_files) >= self._max_recent: self._recent_files.pop() self._recent_files.insert(0, filename) # Callbacks def undoShapeEdit(self) -> None: self._canvas_widgets.canvas.restoreShape() self._docks.label_list.clear() self._load_shapes(self._canvas_widgets.canvas.shapes) self._actions.undo.setEnabled(self._canvas_widgets.canvas.isShapeRestorable) def tutorial(self): url = "https://github.com/labelmeai/labelme/tree/main/examples/tutorial" # NOQA webbrowser.open(url) def toggleDrawingSensitive(self, drawing=True): """Toggle drawing sensitive. In the middle of drawing, toggling between modes should be disabled. """ self._actions.edit_mode.setEnabled(not drawing) self._actions.undo_last_point.setEnabled(drawing) self._actions.undo.setEnabled(not drawing) self._actions.delete.setEnabled(not drawing) def _switch_canvas_mode( self, edit: bool = True, createMode: str | None = None ) -> None: self._canvas_widgets.canvas.setEditing(edit) if createMode is not None: self._canvas_widgets.canvas.createMode = createMode if edit: for _, draw_action in self._actions.draw: draw_action.setEnabled(True) else: for draw_mode, draw_action in self._actions.draw: draw_action.setEnabled(createMode != draw_mode) self._actions.edit_mode.setEnabled(not edit) self._ai_text.setEnabled( not edit and createMode in _AI_TEXT_TO_ANNOTATION_CREATE_MODE_TO_SHAPE_TYPE ) self._ai_annotation.setEnabled( not edit and createMode in ("ai_polygon", "ai_mask") ) def updateFileMenu(self): current = self._filename def exists(filename): return osp.exists(str(filename)) menu = self._menus.recent_files menu.clear() files = [f for f in self._recent_files if f != current and exists(f)] for i, f in enumerate(files): icon = utils.newIcon("labels") action = QtWidgets.QAction( icon, f"&{i + 1} {QtCore.QFileInfo(f).fileName()}", self ) action.triggered.connect(functools.partial(self.loadRecent, f)) menu.addAction(action) def popLabelListMenu(self, point: QtCore.QPoint) -> None: self._menus.label_list.exec(self._docks.label_list.mapToGlobal(point)) # type: ignore[invalid-argument-type] def validateLabel(self, label): # no validation if self._config["validate_label"] is None: return True for i in range(self._docks.unique_label_list.count()): label_i = self._docks.unique_label_list.item(i).data(Qt.UserRole) # type: ignore[attr-defined,union-attr] if self._config["validate_label"] in ["exact"]: if label_i == label: return True return False def _edit_label(self, value=None): items = self._docks.label_list.selectedItems() if not items: logger.warning("No label is selected, so cannot edit label.") return shape = items[0].shape() if len(items) == 1: edit_text = True edit_flags = True edit_group_id = True edit_description = True else: edit_text = all(item.shape().label == shape.label for item in items[1:]) edit_flags = all(item.shape().flags == shape.flags for item in items[1:]) edit_group_id = all( item.shape().group_id == shape.group_id for item in items[1:] ) edit_description = all( item.shape().description == shape.description for item in items[1:] ) if not edit_text: self._label_dialog.edit.setDisabled(True) self._label_dialog.labelList.setDisabled(True) if not edit_group_id: self._label_dialog.edit_group_id.setDisabled(True) if not edit_description: self._label_dialog.editDescription.setDisabled(True) text, flags, group_id, description = self._label_dialog.popUp( text=shape.label if edit_text else "", flags=shape.flags if edit_flags else None, group_id=shape.group_id if edit_group_id else None, description=shape.description if edit_description else None, flags_disabled=not edit_flags, ) if not edit_text: self._label_dialog.edit.setDisabled(False) self._label_dialog.labelList.setDisabled(False) if not edit_group_id: self._label_dialog.edit_group_id.setDisabled(False) if not edit_description: self._label_dialog.editDescription.setDisabled(False) if text is None: assert flags is None assert group_id is None assert description is None return if not self.validateLabel(text): self.errorMessage( self.tr("Invalid label"), self.tr("Invalid label '{}' with validation type '{}'").format( text, self._config["validate_label"] ), ) return self._canvas_widgets.canvas.storeShapes() for item in items: shape: Shape = item.shape() # type: ignore[no-redef] if edit_text: shape.label = text if edit_flags: shape.flags = flags if edit_group_id: shape.group_id = group_id if edit_description: shape.description = description self._update_shape_color(shape) if shape.group_id is None: r, g, b = shape.fill_color.getRgb()[:3] item.setText( f"{html.escape(shape.label)} " f'' ) else: item.setText(f"{shape.label} ({shape.group_id})") self.setDirty() if self._docks.unique_label_list.find_label_item(shape.label) is None: self._docks.unique_label_list.add_label_item( label=shape.label, color=self._get_rgb_by_label( label=shape.label, unique_label_list=self._docks.unique_label_list, ), ) def fileSearchChanged(self): self._import_images_from_dir( root_dir=self._prev_opened_dir, pattern=self._docks.file_search.text() ) def fileSelectionChanged(self) -> None: items = self._docks.file_list.selectedItems() if not items: return item = items[0] if not self._can_continue(): return curr_index = self.imageList.index(str(item.text())) if curr_index < len(self.imageList): filename = self.imageList[curr_index] if filename: self._load_file(filename) # React to canvas signals. def shapeSelectionChanged(self, selected_shapes: list[Shape]) -> None: self._docks.label_list.itemSelectionChanged.disconnect( self._label_selection_changed ) for shape in self._canvas_widgets.canvas.selectedShapes: shape.selected = False self._docks.label_list.clearSelection() self._canvas_widgets.canvas.selectedShapes = selected_shapes for shape in self._canvas_widgets.canvas.selectedShapes: shape.selected = True item = self._docks.label_list.findItemByShape(shape) self._docks.label_list.selectItem(item) self._docks.label_list.scrollToItem(item) self._docks.label_list.itemSelectionChanged.connect( self._label_selection_changed ) n_selected = len(selected_shapes) > 0 self._actions.delete.setEnabled(n_selected) self._actions.duplicate.setEnabled(n_selected) self._actions.copy.setEnabled(n_selected) self._actions.edit.setEnabled(n_selected) def addLabel(self, shape: Shape) -> None: assert shape.label is not None if shape.group_id is None: text = shape.label else: text = f"{shape.label} ({shape.group_id})" label_list_item = LabelListWidgetItem(text, shape) self._docks.label_list.addItem(label_list_item) if self._docks.unique_label_list.find_label_item(shape.label) is None: self._docks.unique_label_list.add_label_item( label=shape.label, color=self._get_rgb_by_label( label=shape.label, unique_label_list=self._docks.unique_label_list, ), ) self._label_dialog.addLabelHistory(shape.label) for action in self._actions.on_shapes_present: action.setEnabled(True) self._update_shape_color(shape) r, g, b = shape.fill_color.getRgb()[:3] label_list_item.setText( f'{html.escape(text)} ' ) def _update_shape_color(self, shape: Shape) -> None: assert shape.label is not None r, g, b = self._get_rgb_by_label( shape.label, unique_label_list=self._docks.unique_label_list ) shape.line_color = QtGui.QColor(r, g, b) shape.vertex_fill_color = QtGui.QColor(r, g, b) shape.hvertex_fill_color = QtGui.QColor(255, 255, 255) shape.fill_color = QtGui.QColor(r, g, b, 128) shape.select_line_color = QtGui.QColor(255, 255, 255) shape.select_fill_color = QtGui.QColor(r, g, b, 155) def _get_rgb_by_label( self, label: str, unique_label_list: UniqueLabelQListWidget, ) -> tuple[int, int, int]: if self._config["shape_color"] == "auto": item = unique_label_list.find_label_item(label) item_index: int = ( unique_label_list.indexFromItem(item).row() if item else unique_label_list.count() ) label_id: int = ( 1 # skip black color by default + item_index + self._config["shift_auto_shape_color"] ) rgb: tuple[int, int, int] = tuple( LABEL_COLORMAP[label_id % len(LABEL_COLORMAP)].tolist() ) return rgb elif ( self._config["shape_color"] == "manual" and self._config["label_colors"] and label in self._config["label_colors"] ): if not ( len(self._config["label_colors"][label]) == 3 and all(0 <= c <= 255 for c in self._config["label_colors"][label]) ): raise ValueError( "Color for label must be 0-255 RGB tuple, but got: " f"{self._config['label_colors'][label]}" ) return tuple(self._config["label_colors"][label]) elif self._config["default_shape_color"]: return self._config["default_shape_color"] return (0, 255, 0) def remLabels(self, shapes: list[Shape]) -> None: for shape in shapes: item = self._docks.label_list.findItemByShape(shape) self._docks.label_list.removeItem(item) def _load_shapes(self, shapes: list[Shape], replace: bool = True) -> None: self._docks.label_list.itemSelectionChanged.disconnect( self._label_selection_changed ) shape: Shape for shape in shapes: self.addLabel(shape) self._docks.label_list.clearSelection() self._docks.label_list.itemSelectionChanged.connect( self._label_selection_changed ) self._canvas_widgets.canvas.loadShapes(shapes=shapes, replace=replace) def _load_shape_dicts(self, shape_dicts: list[ShapeDict]) -> None: shapes: list[Shape] = [] shape_dict: ShapeDict for shape_dict in shape_dicts: shape: Shape = Shape( label=shape_dict["label"], shape_type=shape_dict["shape_type"], group_id=shape_dict["group_id"], description=shape_dict["description"], mask=shape_dict["mask"], ) for x, y in shape_dict["points"]: shape.addPoint(QtCore.QPointF(x, y)) shape.close() default_flags = {} if self._config["label_flags"]: for pattern, keys in self._config["label_flags"].items(): if not isinstance(shape.label, str): logger.warning("shape.label is not str: {}", shape.label) continue if re.match(pattern, shape.label): for key in keys: default_flags[key] = False shape.flags = default_flags shape.flags.update(shape_dict["flags"]) shape.other_data = shape_dict["other_data"] shapes.append(shape) self._load_shapes(shapes=shapes) def _load_flags( self, flags: dict[str, bool], widget: QtWidgets.QListWidget, ) -> None: widget.clear() key: str flag: bool for key, flag in flags.items(): item: QtWidgets.QListWidgetItem = QtWidgets.QListWidgetItem(key) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked if flag else Qt.Unchecked) widget.addItem(item) def saveLabels(self, filename): lf = LabelFile() def format_shape(s): data = s.other_data.copy() data.update( dict( label=s.label, points=[(p.x(), p.y()) for p in s.points], group_id=s.group_id, description=s.description, shape_type=s.shape_type, flags=s.flags, mask=None if s.mask is None else utils.img_arr_to_b64(s.mask.astype(np.uint8)), ) ) return data shapes = [format_shape(item.shape()) for item in self._docks.label_list] flags = {} for i in range(self._docks.flag_list.count()): item = self._docks.flag_list.item(i) assert item key = item.text() flag = item.checkState() == Qt.Checked flags[key] = flag try: assert self._image_path imagePath = osp.relpath(self._image_path, osp.dirname(filename)) imageData = self.imageData if self._config["with_image_data"] else None if osp.dirname(filename) and not osp.exists(osp.dirname(filename)): os.makedirs(osp.dirname(filename)) lf.save( filename=filename, shapes=shapes, imagePath=imagePath, imageData=imageData, imageHeight=self._image.height(), imageWidth=self._image.width(), otherData=self._other_data, flags=flags, ) self._label_file = lf items = self._docks.file_list.findItems(self._image_path, Qt.MatchExactly) if len(items) > 0: if len(items) != 1: raise RuntimeError("There are duplicate files.") items[0].setCheckState(Qt.Checked) # disable allows next and previous image to proceed # self._filename = filename return True except LabelFileError as e: self.errorMessage( self.tr("Error saving label data"), self.tr("%s") % e ) return False def duplicateSelectedShape(self) -> None: self.copySelectedShape() self.pasteSelectedShape() def pasteSelectedShape(self) -> None: self._load_shapes(shapes=self._copied_shapes, replace=False) self._canvas_widgets.canvas.selectShapes(self._copied_shapes) self.setDirty() def copySelectedShape(self) -> None: self._copied_shapes = [ s.copy() for s in self._canvas_widgets.canvas.selectedShapes ] self._actions.paste.setEnabled(len(self._copied_shapes) > 0) def _label_selection_changed(self) -> None: selected_shapes: list[Shape] = [] for item in self._docks.label_list.selectedItems(): selected_shapes.append(item.shape()) if selected_shapes: self._canvas_widgets.canvas.selectShapes(selected_shapes) else: if self._canvas_widgets.canvas.deSelectShape(): self._canvas_widgets.canvas.update() def labelItemChanged(self, item: LabelListWidgetItem) -> None: shape = item.shape() self._canvas_widgets.canvas.setShapeVisible( shape, item.checkState() == Qt.Checked ) def labelOrderChanged(self) -> None: self.setDirty() self._canvas_widgets.canvas.loadShapes( [item.shape() for item in self._docks.label_list] ) # Callback functions: def newShape(self) -> None: """Pop-up and give focus to the label editor. position MUST be in global coordinates. """ items = self._docks.unique_label_list.selectedItems() text = None if items: text = items[0].data(Qt.UserRole) flags = {} group_id = None description = "" if self._config["display_label_popup"] or not text: previous_text = self._label_dialog.edit.text() text, flags, group_id, description = self._label_dialog.popUp(text) if not text: self._label_dialog.edit.setText(previous_text) if text and not self.validateLabel(text): self.errorMessage( self.tr("Invalid label"), self.tr("Invalid label '{}' with validation type '{}'").format( text, self._config["validate_label"] ), ) text = "" if text: self._docks.label_list.clearSelection() shape = self._canvas_widgets.canvas.setLastLabel(text, flags) shape.group_id = group_id shape.description = description self.addLabel(shape) self._actions.edit_mode.setEnabled(True) self._actions.undo_last_point.setEnabled(False) self._actions.undo.setEnabled(True) self.setDirty() else: self._canvas_widgets.canvas.undoLastLine() self._canvas_widgets.canvas.shapesBackups.pop() def scrollRequest(self, delta: int, orientation: Qt.Orientation) -> None: units = -delta * 0.1 # natural scroll bar = self._canvas_widgets.scroll_bars[orientation] value = bar.value() + bar.singleStep() * units self.setScroll(orientation, value) def setScroll(self, orientation: Qt.Orientation, value: float) -> None: self._canvas_widgets.scroll_bars[orientation].setValue(int(value)) if self._filename is not None: self._scroll_values[orientation][self._filename] = value def _set_zoom(self, value: int, pos: QtCore.QPointF | None = None) -> None: if self._filename is None: logger.warning("filename is None, cannot set zoom") return if pos is None: pos = QtCore.QPointF( self._canvas_widgets.canvas.visibleRegion().boundingRect().center() ) canvas_width_old: int = self._canvas_widgets.canvas.width() self._actions.fit_width.setChecked(self._zoom_mode == _ZoomMode.FIT_WIDTH) self._actions.fit_window.setChecked(self._zoom_mode == _ZoomMode.FIT_WINDOW) self._canvas_widgets.canvas.enableDragging( enabled=value > int(self._scalers[_ZoomMode.FIT_WINDOW]() * 100) ) self._canvas_widgets.zoom_widget.setValue(value) # triggers self._paint_canvas self._zoom_values[self._filename] = (self._zoom_mode, value) canvas_width_new: int = self._canvas_widgets.canvas.width() if canvas_width_old == canvas_width_new: return canvas_scale_factor = canvas_width_new / canvas_width_old x_shift: float = pos.x() * canvas_scale_factor - pos.x() y_shift: float = pos.y() * canvas_scale_factor - pos.y() self.setScroll( Qt.Horizontal, self._canvas_widgets.scroll_bars[Qt.Horizontal].value() + x_shift, ) self.setScroll( Qt.Vertical, self._canvas_widgets.scroll_bars[Qt.Vertical].value() + y_shift, ) def _set_zoom_to_original(self): self._zoom_mode = _ZoomMode.MANUAL_ZOOM self._set_zoom(value=100) def _add_zoom(self, increment: float, pos: QtCore.QPointF | None = None) -> None: zoom_value: int if increment > 1: zoom_value = math.ceil(self._canvas_widgets.zoom_widget.value() * increment) else: zoom_value = math.floor( self._canvas_widgets.zoom_widget.value() * increment ) self._zoom_mode = _ZoomMode.MANUAL_ZOOM self._set_zoom(value=zoom_value, pos=pos) def _zoom_requested(self, delta: int, pos: QtCore.QPointF) -> None: self._add_zoom(increment=1.1 if delta > 0 else 0.9, pos=pos) def setFitWindow(self, value=True): if value: self._actions.fit_width.setChecked(False) self._zoom_mode = _ZoomMode.FIT_WINDOW if value else _ZoomMode.MANUAL_ZOOM self._adjust_scale() def setFitWidth(self, value=True): if value: self._actions.fit_window.setChecked(False) self._zoom_mode = _ZoomMode.FIT_WIDTH if value else _ZoomMode.MANUAL_ZOOM self._adjust_scale() def enableKeepPrevScale(self, enabled): self._config["keep_prev_scale"] = enabled self._actions.keep_prev_scale.setChecked(enabled) def onNewBrightnessContrast(self, qimage): self._canvas_widgets.canvas.loadPixmap( QtGui.QPixmap.fromImage(qimage), clear_shapes=False ) def brightnessContrast(self, value: bool, is_initial_load: bool = False): if self._filename is None: logger.warning("filename is None, cannot set brightness/contrast") return brightness: int | None contrast: int | None brightness, contrast = self._brightness_contrast_values.get( self._filename, (None, None) ) if is_initial_load: prev_filename: str = self._recent_files[0] if self._recent_files else "" if self._config["keep_prev_brightness_contrast"] and prev_filename: brightness, contrast = self._brightness_contrast_values.get( prev_filename, (None, None) ) if brightness is None and contrast is None: return logger.debug( "Opening brightness/contrast dialog with brightness={}, contrast={}", brightness, contrast, ) dialog = BrightnessContrastDialog( utils.img_data_to_pil(self.imageData), self.onNewBrightnessContrast, parent=self, ) if brightness is not None: dialog.slider_brightness.setValue(brightness) if contrast is not None: dialog.slider_contrast.setValue(contrast) if is_initial_load: dialog.onNewValue(None) else: dialog.exec_() brightness = dialog.slider_brightness.value() contrast = dialog.slider_contrast.value() self._brightness_contrast_values[self._filename] = (brightness, contrast) logger.debug( "Updated states for {}: brightness={}, contrast={}", self._filename, brightness, contrast, ) def toggleShapes(self, value): flag = value for item in self._docks.label_list: if value is None: flag = item.checkState() == Qt.Unchecked item.setCheckState(Qt.Checked if flag else Qt.Unchecked) def _load_file(self, filename=None): """Load the specified file, or the last opened file if None.""" # changing fileListWidget loads file if filename in self.imageList and ( self._docks.file_list.currentRow() != self.imageList.index(filename) ): self._docks.file_list.setCurrentRow(self.imageList.index(filename)) self._docks.file_list.repaint() return prev_shapes: list[Shape] = ( self._canvas_widgets.canvas.shapes if self._config["keep_prev"] or QtWidgets.QApplication.keyboardModifiers() == (Qt.ControlModifier | Qt.ShiftModifier) else [] ) self.resetState() self._canvas_widgets.canvas.setEnabled(False) if filename is None: filename = self.settings.value("filename", "") filename = str(filename) if not QtCore.QFile.exists(filename): self.errorMessage( self.tr("Error opening file"), self.tr("No such file: %s") % filename, ) return False # assumes same name, but json extension self.show_status_message(self.tr("Loading %s...") % osp.basename(str(filename))) t0_load_file = time.time() label_file = f"{osp.splitext(filename)[0]}.json" if self._output_dir: label_file_without_path = osp.basename(label_file) label_file = osp.join(self._output_dir, label_file_without_path) if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(label_file): try: self._label_file = LabelFile(label_file) except LabelFileError as e: self.errorMessage( self.tr("Error opening file"), self.tr( "

%s

" "

Make sure %s is a valid label file.

" ) % (e, label_file), ) self.show_status_message(self.tr("Error reading %s") % label_file) return False assert self._label_file is not None self.imageData = self._label_file.imageData assert self._label_file.imagePath self._image_path = osp.join( osp.dirname(label_file), self._label_file.imagePath, ) self._other_data = self._label_file.otherData else: try: self.imageData = LabelFile.load_image_file(filename) except OSError as e: self.errorMessage( self.tr("Error opening file"), self.tr( "

%s

" "

Make sure %s is a valid image file.

" ) % (e, filename), ) self.show_status_message(self.tr("Error reading %s") % filename) return False if self.imageData: self._image_path = filename self._label_file = None assert self.imageData is not None t0 = time.time() image = QtGui.QImage.fromData(self.imageData) logger.debug("Created QImage in {:.0f}ms", (time.time() - t0) * 1000) if image.isNull(): formats = [ f"*.{fmt.data().decode()}" for fmt in QtGui.QImageReader.supportedImageFormats() ] self.errorMessage( self.tr("Error opening file"), self.tr( "

Make sure {0} is a valid image file.
" "Supported image formats: {1}

" ).format(filename, ",".join(formats)), ) self.show_status_message(self.tr("Error reading %s") % filename) return False self._image = image self._filename = filename t0 = time.time() self._canvas_widgets.canvas.loadPixmap(QtGui.QPixmap.fromImage(image)) logger.debug("Loaded pixmap in {:.0f}ms", (time.time() - t0) * 1000) flags = {k: False for k in self._config["flags"] or []} if self._label_file: self._load_shape_dicts(shape_dicts=self._label_file.shapes) if self._label_file.flags is not None: flags.update(self._label_file.flags) self._load_flags(flags=flags, widget=self._docks.flag_list) if prev_shapes and self.noShapes(): self._load_shapes(shapes=prev_shapes, replace=False) self.setDirty() else: self.setClean() self._canvas_widgets.canvas.setEnabled(True) # set zoom values is_initial_load = not self._zoom_values if self._filename in self._zoom_values: self._zoom_mode = self._zoom_values[self._filename][0] self._set_zoom(self._zoom_values[self._filename][1]) elif is_initial_load or not self._config["keep_prev_scale"]: self._zoom_mode = _ZoomMode.FIT_WINDOW self._adjust_scale() # set scroll values for orientation in self._scroll_values: if self._filename in self._scroll_values[orientation]: self.setScroll( orientation, self._scroll_values[orientation][self._filename] ) self.brightnessContrast(value=False, is_initial_load=True) self._paint_canvas() self.addRecentFile(self._filename) self.toggleActions(True) self._canvas_widgets.canvas.setFocus() self.show_status_message(self.tr("Loaded %s") % osp.basename(filename)) logger.info( "Loaded file: {!r} in {:.0f}ms", filename, (time.time() - t0_load_file) * 1000, ) return True def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: if ( self._canvas_widgets.canvas and not self._image.isNull() and self._zoom_mode != _ZoomMode.MANUAL_ZOOM ): self._adjust_scale() super().resizeEvent(a0) def _paint_canvas(self) -> None: if self._image.isNull(): logger.warning("image is null, cannot paint canvas") return self._canvas_widgets.canvas.scale = ( 0.01 * self._canvas_widgets.zoom_widget.value() ) self._canvas_widgets.canvas.adjustSize() self._canvas_widgets.canvas.update() def _adjust_scale(self) -> None: self._set_zoom(value=int(self._scalers[self._zoom_mode]() * 100)) def scaleFitWindow(self) -> float: EPSILON_TO_HIDE_SCROLLBAR: float = 2.0 w1: float = self.centralWidget().width() - EPSILON_TO_HIDE_SCROLLBAR h1: float = self.centralWidget().height() - EPSILON_TO_HIDE_SCROLLBAR a1: float = w1 / h1 w2: float = self._canvas_widgets.canvas.pixmap.width() h2: float = self._canvas_widgets.canvas.pixmap.height() a2: float = w2 / h2 return w1 / w2 if a2 >= a1 else h1 / h2 def scaleFitWidth(self): EPSILON_TO_HIDE_SCROLLBAR: float = 15.0 w = self.centralWidget().width() - EPSILON_TO_HIDE_SCROLLBAR return w / self._canvas_widgets.canvas.pixmap.width() def enableSaveImageWithData(self, enabled): self._config["with_image_data"] = enabled self._actions.save_with_image_data.setChecked(enabled) def _reset_layout(self): self.settings.remove("window/state") self.restoreState(self._default_state) def closeEvent(self, a0: QtGui.QCloseEvent) -> None: if not self._can_continue(): a0.ignore() self.settings.setValue("filename", self._filename if self._filename else "") self.settings.setValue("window/size", self.size()) self.settings.setValue("window/position", self.pos()) self.settings.setValue("window/state", self.saveState()) self.settings.setValue("recentFiles", self._recent_files) def dragEnterEvent(self, a0: QtGui.QDragEnterEvent) -> None: extensions = [ f".{fmt.data().decode().lower()}" for fmt in QtGui.QImageReader.supportedImageFormats() ] if a0.mimeData().hasUrls(): items = [i.toLocalFile() for i in a0.mimeData().urls()] if any([i.lower().endswith(tuple(extensions)) for i in items]): a0.accept() else: a0.ignore() def dropEvent(self, a0: QtGui.QDropEvent) -> None: if not self._can_continue(): a0.ignore() return items = [i.toLocalFile() for i in a0.mimeData().urls()] self.importDroppedImageFiles(items) # User Dialogs # def loadRecent(self, filename): if self._can_continue(): self._load_file(filename) def _open_prev_image(self, _value=False) -> None: row_prev: int = self._docks.file_list.currentRow() - 1 if row_prev < 0: logger.debug("there is no prev image") return logger.debug("setting current row to {:d}", row_prev) self._docks.file_list.setCurrentRow(row_prev) self._docks.file_list.repaint() def _open_next_image(self, _value=False) -> None: row_next: int = self._docks.file_list.currentRow() + 1 if row_next >= self._docks.file_list.count(): logger.debug("there is no next image") return logger.debug("setting current row to {:d}", row_next) self._docks.file_list.setCurrentRow(row_next) self._docks.file_list.repaint() def _open_file_with_dialog(self, _value: bool = False) -> None: if not self._can_continue(): return path = osp.dirname(str(self._filename)) if self._filename else "." formats = [ f"*.{fmt.data().decode()}" for fmt in QtGui.QImageReader.supportedImageFormats() ] filters = self.tr("Image & Label files (%s)") % " ".join( formats + [f"*{LabelFile.suffix}"] ) fileDialog = FileDialogPreview(self) fileDialog.setFileMode(FileDialogPreview.ExistingFile) fileDialog.setNameFilter(filters) fileDialog.setWindowTitle( self.tr("%s - Choose Image or Label file") % __appname__, ) fileDialog.setWindowFilePath(path) fileDialog.setViewMode(FileDialogPreview.Detail) if fileDialog.exec_(): fileName = fileDialog.selectedFiles()[0] if fileName: self._load_file(fileName) def changeOutputDirDialog(self, _value=False): default_output_dir = self._output_dir if default_output_dir is None and self._filename: default_output_dir = osp.dirname(self._filename) if default_output_dir is None: default_output_dir = self.currentPath() output_dir = QtWidgets.QFileDialog.getExistingDirectory( self, self.tr("%s - Save/Load Annotations in Directory") % __appname__, default_output_dir, QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks, ) output_dir = str(output_dir) if not output_dir: return self._output_dir = output_dir self.statusBar().showMessage( self.tr("%s . Annotations will be saved/loaded in %s") % ("Change Annotations Dir", self._output_dir) ) self.statusBar().show() current_filename = self._filename self._import_images_from_dir(root_dir=self._prev_opened_dir) if current_filename in self.imageList: # retain currently selected file self._docks.file_list.setCurrentRow(self.imageList.index(current_filename)) self._docks.file_list.repaint() def saveFile(self, _value: bool = False) -> None: assert not self._image.isNull(), "cannot save empty image" if self._label_file: self._saveFile(self._label_file.filename) else: self._saveFile(self.saveFileDialog()) def saveFileAs(self, _value: bool = False) -> None: assert not self._image.isNull(), "cannot save empty image" self._saveFile(self.saveFileDialog()) def saveFileDialog(self) -> str: assert self._filename is not None caption = self.tr("%s - Choose File") % __appname__ filters = self.tr("Label files (*%s)") % LabelFile.suffix start_dir = self._output_dir if self._output_dir else self.currentPath() dlg = QtWidgets.QFileDialog(self, caption, start_dir, filters) dlg.setDefaultSuffix(LabelFile.suffix[1:]) dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) dlg.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False) dlg.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, False) basename = osp.basename(osp.splitext(self._filename)[0]) if self._output_dir: default_labelfile_name = osp.join( self._output_dir, basename + LabelFile.suffix ) else: default_labelfile_name = osp.join( self.currentPath(), basename + LabelFile.suffix ) filename = dlg.getSaveFileName( self, self.tr("Choose File"), default_labelfile_name, self.tr("Label files (*%s)") % LabelFile.suffix, ) if isinstance(filename, tuple): return filename[0] return filename def _saveFile(self, filename: str | None) -> None: if filename and self.saveLabels(filename): self.addRecentFile(filename) self.setClean() def closeFile(self, _value: bool = False) -> None: if not self._can_continue(): return self.resetState() self.setClean() self.toggleActions(False) self._canvas_widgets.canvas.setEnabled(False) self._docks.file_list.setFocus() self._actions.save_as.setEnabled(False) def getLabelFile(self) -> str: assert self._filename is not None if self._filename.lower().endswith(".json"): return self._filename return f"{osp.splitext(self._filename)[0]}.json" def deleteFile(self) -> None: mb = QtWidgets.QMessageBox msg = self.tr( "You are about to permanently delete this label file, proceed anyway?" ) answer = mb.warning(self, self.tr("Attention"), msg, mb.Yes | mb.No) if answer != mb.Yes: return label_file = self.getLabelFile() if osp.exists(label_file): os.remove(label_file) logger.info(f"Label file is removed: {label_file}") item = self._docks.file_list.currentItem() if item: item.setCheckState(Qt.Unchecked) self.resetState() def _open_config_file(self) -> None: if self._config_file is None: QtWidgets.QMessageBox.information( self, self.tr("No Config File"), self.tr( "Configuration was provided as a YAML expression via " "command line.\n\n" "To use the preferences editor, start Labelme with a config file:\n" " labelme --config ~/.labelmerc" ), ) return config_file: Path = self._config_file system: str = platform.system() if system == "Darwin": subprocess.Popen(["open", "-t", config_file]) elif system == "Windows": os.startfile(config_file) # type: ignore[attr-defined] else: subprocess.Popen(["xdg-open", config_file]) # Message Dialogs. # def hasLabels(self) -> bool: if self.noShapes(): self.errorMessage( "No objects labeled", "You must label at least one object to save the file.", ) return False return True def hasLabelFile(self) -> bool: if self._filename is None: return False label_file = self.getLabelFile() return osp.exists(label_file) def _can_continue(self) -> bool: if not self._is_changed: return True mb = QtWidgets.QMessageBox msg = self.tr('Save annotations to "{}" before closing?').format(self._filename) answer = mb.question( self, self.tr("Save annotations?"), msg, mb.Save | mb.Discard | mb.Cancel, mb.Save, ) if answer == mb.Discard: return True elif answer == mb.Save: self.saveFile() return True else: # answer == mb.Cancel return False def errorMessage(self, title: str, message: str) -> int: return QtWidgets.QMessageBox.critical( self, title, f"

{title}

{message}" ) def currentPath(self) -> str: return osp.dirname(str(self._filename)) if self._filename else "." def toggleKeepPrevMode(self) -> None: self._config["keep_prev"] = not self._config["keep_prev"] def removeSelectedPoint(self) -> None: self._canvas_widgets.canvas.removeSelectedPoint() self._canvas_widgets.canvas.update() if ( self._canvas_widgets.canvas.hShape and not self._canvas_widgets.canvas.hShape.points ): self._canvas_widgets.canvas.deleteShape(self._canvas_widgets.canvas.hShape) self.remLabels([self._canvas_widgets.canvas.hShape]) if self.noShapes(): for action in self._actions.on_shapes_present: action.setEnabled(False) self.setDirty() def deleteSelectedShape(self) -> None: yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No msg = self.tr( "You are about to permanently delete {} shapes, proceed anyway?" ).format(len(self._canvas_widgets.canvas.selectedShapes)) if yes == QtWidgets.QMessageBox.warning( self, self.tr("Attention"), msg, yes | no, yes ): self.remLabels(self._canvas_widgets.canvas.deleteSelected()) self.setDirty() if self.noShapes(): for action in self._actions.on_shapes_present: action.setEnabled(False) def copyShape(self) -> None: self._canvas_widgets.canvas.endMove(copy=True) for shape in self._canvas_widgets.canvas.selectedShapes: self.addLabel(shape) self._docks.label_list.clearSelection() self.setDirty() def moveShape(self) -> None: self._canvas_widgets.canvas.endMove(copy=False) self.setDirty() def _open_dir_with_dialog(self, _value: bool = False) -> None: if not self._can_continue(): return defaultOpenDirPath: str if self._prev_opened_dir and osp.exists(self._prev_opened_dir): defaultOpenDirPath = self._prev_opened_dir else: defaultOpenDirPath = osp.dirname(self._filename) if self._filename else "." targetDirPath = str( QtWidgets.QFileDialog.getExistingDirectory( self, self.tr("%s - Open Directory") % __appname__, defaultOpenDirPath, QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks, ) ) self._import_images_from_dir(root_dir=targetDirPath) self._open_next_image() @property def imageList(self) -> list[str]: lst = [] for i in range(self._docks.file_list.count()): item = self._docks.file_list.item(i) assert item lst.append(item.text()) return lst def importDroppedImageFiles(self, imageFiles): extensions = [ f".{fmt.data().decode().lower()}" for fmt in QtGui.QImageReader.supportedImageFormats() ] self._filename = None for file in imageFiles: if file in self.imageList or not file.lower().endswith(tuple(extensions)): continue label_file = f"{osp.splitext(file)[0]}.json" if self._output_dir: label_file_without_path = osp.basename(label_file) label_file = osp.join(self._output_dir, label_file_without_path) item = QtWidgets.QListWidgetItem(file) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(label_file): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) self._docks.file_list.addItem(item) if len(self.imageList) > 1: self._actions.open_next_img.setEnabled(True) self._actions.open_prev_img.setEnabled(True) self._open_next_image() def _import_images_from_dir( self, root_dir: str | None, pattern: str | None = None ) -> None: self._actions.open_next_img.setEnabled(True) self._actions.open_prev_img.setEnabled(True) if not self._can_continue() or not root_dir: return self._prev_opened_dir = root_dir self._filename = None self._docks.file_list.clear() filenames = _scan_image_files(root_dir=root_dir) if pattern: try: filenames = [f for f in filenames if re.search(pattern, f)] except re.error: pass for filename in filenames: label_file = f"{osp.splitext(filename)[0]}.json" if self._output_dir: label_file_without_path = osp.basename(label_file) label_file = osp.join(self._output_dir, label_file_without_path) item = QtWidgets.QListWidgetItem(filename) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(label_file): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) self._docks.file_list.addItem(item) def _update_status_stats(self, mouse_pos: QtCore.QPointF) -> None: stats: list[str] = [] stats.append(f"mode={self._canvas_widgets.canvas.mode.name}") stats.append(f"x={mouse_pos.x():6.1f}, y={mouse_pos.y():6.1f}") self._status_bar.stats.setText(" | ".join(stats)) def _scan_image_files(root_dir: str) -> list[str]: extensions: list[str] = [ f".{fmt.data().decode().lower()}" for fmt in QtGui.QImageReader.supportedImageFormats() ] images: list[str] = [] for root, dirs, files in os.walk(root_dir): for file in files: if file.lower().endswith(tuple(extensions)): relativePath = os.path.normpath(osp.join(root, file)) images.append(relativePath) logger.debug("found {:d} images in {!r}", len(images), root_dir) return natsort.os_sorted(images) ================================================ FILE: labelme/config/__init__.py ================================================ import os.path as osp import re from pathlib import Path import yaml from loguru import logger here = osp.dirname(osp.abspath(__file__)) def _update_dict(target_dict, new_dict, validate_item=None): for key, value in new_dict.items(): if validate_item: validate_item(key, value) if key not in target_dict: raise ValueError(f"Unexpected key in config: {key}") if isinstance(target_dict[key], dict) and isinstance(value, dict): _update_dict(target_dict[key], value, validate_item=validate_item) else: target_dict[key] = value def _validate_config_item(key, value): if key == "validate_label" and value not in [None, "exact"]: raise ValueError(f"Unexpected value for config key 'validate_label': {value}") if key == "shape_color" and value not in [None, "auto", "manual"]: raise ValueError(f"Unexpected value for config key 'shape_color': {value}") if key == "labels" and value is not None and len(value) != len(set(value)): raise ValueError(f"Duplicates are detected for config key 'labels': {value}") def _migrate_config_from_file(config_from_yaml: dict) -> None: keep_prev_brightness: bool = config_from_yaml.pop("keep_prev_brightness", False) keep_prev_contrast: bool = config_from_yaml.pop("keep_prev_contrast", False) if keep_prev_brightness or keep_prev_contrast: logger.info( "Migrating old config: keep_prev_brightness={} or keep_prev_contrast={} " "-> keep_prev_brightness_contrast=True", keep_prev_brightness, keep_prev_contrast, ) config_from_yaml["keep_prev_brightness_contrast"] = True if "store_data" in config_from_yaml: logger.info("Migrating old config: store_data -> with_image_data") config_from_yaml["with_image_data"] = config_from_yaml.pop("store_data") if config_from_yaml.get("shortcuts", {}).pop("add_point_to_edge", None): logger.info("Migrating old config: removing shortcuts.add_point_to_edge") if (model_name := config_from_yaml.get("ai", {}).get("default")) and ( m := re.match(r"^SegmentAnything \((.*)\)$", model_name) ): model_name_new: str = f"Sam ({m.group(1)})" logger.info( "Migrating old config: ai.default={!r} -> ai.default={!r}", model_name, model_name_new, ) config_from_yaml["ai"]["default"] = model_name_new # Migrate polygon shortcut keys to shape _POLYGON_TO_SHAPE_RENAMES = { "edit_polygon": "edit_shape", "delete_polygon": "delete_shape", "duplicate_polygon": "duplicate_shape", "copy_polygon": "copy_shape", "paste_polygon": "paste_shape", "show_all_polygons": "show_all_shapes", "hide_all_polygons": "hide_all_shapes", "toggle_all_polygons": "toggle_all_shapes", } shortcuts = config_from_yaml.get("shortcuts", {}) for old_key, new_key in _POLYGON_TO_SHAPE_RENAMES.items(): if old_key in shortcuts and new_key not in shortcuts: logger.info( "Migrating old config: shortcuts.{} -> shortcuts.{}", old_key, new_key, ) shortcuts[new_key] = shortcuts.pop(old_key) def get_user_config_file(create_if_missing: bool = True) -> str: user_config_file: str = osp.join(osp.expanduser("~"), ".labelmerc") if not osp.exists(user_config_file) and create_if_missing: try: with open(user_config_file, "w") as f: f.write( "# Labelme config file.\n" "# Only add settings you want to override.\n" "# For all available options and defaults, see:\n" "# https://github.com/wkentaro/labelme/blob/main/labelme/config/default_config.yaml\n" "#\n" "# Example:\n" "# with_image_data: true\n" "# auto_save: false\n" "# labels: [cat, dog]\n" ) except Exception: logger.warning("Failed to save config: {!r}", user_config_file) return user_config_file def load_config(config_file: Path | None, config_overrides: dict) -> dict: config: dict with open(osp.join(here, "default_config.yaml")) as f: config = yaml.safe_load(f) if config_file is not None: with open(config_file) as f: config_from_yaml = yaml.safe_load(f) if isinstance(config_from_yaml, dict): _migrate_config_from_file(config_from_yaml=config_from_yaml) _update_dict(config, config_from_yaml, validate_item=_validate_config_item) _update_dict(config, config_overrides, validate_item=_validate_config_item) if not config["labels"] and config["validate_label"]: raise ValueError("labels must be specified when validate_label is enabled") return config ================================================ FILE: labelme/config/default_config.yaml ================================================ auto_save: true display_label_popup: true with_image_data: false keep_prev: false keep_prev_scale: false keep_prev_brightness_contrast: false logger_level: info flags: null label_flags: null labels: null file_search: null sort_labels: true validate_label: null default_shape_color: [0, 255, 0] shape_color: auto # null, 'auto', 'manual' shift_auto_shape_color: 0 label_colors: null shape: # drawing line_color: [0, 255, 0, 128] fill_color: [0, 0, 0, 64] vertex_fill_color: [0, 255, 0, 255] # selecting / hovering select_line_color: [255, 255, 255, 255] select_fill_color: [0, 255, 0, 64] hvertex_fill_color: [255, 255, 255, 255] point_size: 8 ai: default: 'Sam2 (balanced)' # main flag_dock: show: true closable: true movable: true floatable: true label_dock: show: true closable: true movable: true floatable: true shape_dock: show: true closable: true movable: true floatable: true file_dock: show: true closable: true movable: true floatable: true # label_dialog show_label_text_field: true label_completion: startswith fit_to_content: column: true row: false # canvas epsilon: 10.0 canvas: fill_drawing: true # None: do nothing # close: close polygon double_click: close # The max number of edits we can undo num_backups: 10 # show crosshair crosshair: polygon: false rectangle: true circle: false line: false point: false linestrip: false ai_polygon: false ai_mask: false shortcuts: close: Ctrl+W open: Ctrl+O open_dir: Ctrl+U quit: Ctrl+Q save: Ctrl+S save_as: Ctrl+Shift+S save_to: null delete_file: Ctrl+Delete open_next: [D, Ctrl+Shift+D] open_prev: [A, Ctrl+Shift+A] zoom_in: [Ctrl++, Ctrl+=] zoom_out: Ctrl+- zoom_to_original: Ctrl+0 fit_window: Ctrl+F fit_width: Ctrl+Shift+F create_polygon: Ctrl+N create_rectangle: Ctrl+R create_circle: null create_line: null create_point: null create_linestrip: null edit_shape: Ctrl+J delete_shape: Delete duplicate_shape: Ctrl+D copy_shape: Ctrl+C paste_shape: Ctrl+V undo: Ctrl+Z undo_last_point: Ctrl+Z edit_label: Ctrl+E toggle_keep_prev_mode: Ctrl+P remove_selected_point: [Meta+H, Backspace] show_all_shapes: null hide_all_shapes: null toggle_all_shapes: T ================================================ FILE: labelme/shape.py ================================================ from __future__ import annotations import copy import numpy as np import numpy.typing as npt import skimage.measure from loguru import logger from PyQt5 import QtCore from PyQt5 import QtGui import labelme.utils class Shape: # Handle point styles: square or round P_SQUARE = 0 P_ROUND = 1 # Vertex interaction modes MOVE_VERTEX = 0 NEAR_VERTEX = 1 # Width of the shape outline pen PEN_WIDTH = 2 # The following class variables influence the drawing of all shape objects. line_color: QtGui.QColor = QtGui.QColor(0, 255, 0, 128) fill_color: QtGui.QColor = QtGui.QColor(0, 0, 0, 64) vertex_fill_color: QtGui.QColor = QtGui.QColor(0, 255, 0, 255) select_line_color: QtGui.QColor = QtGui.QColor(255, 255, 255, 255) select_fill_color: QtGui.QColor = QtGui.QColor(0, 255, 0, 64) hvertex_fill_color: QtGui.QColor = QtGui.QColor(255, 255, 255, 255) # Default handle style, size, and zoom scale point_type = P_ROUND point_size = 8 scale = 1.0 _current_vertex_fill_color: QtGui.QColor def __init__( self, label: str | None = None, line_color: QtGui.QColor | None = None, shape_type: str | None = None, flags: dict[str, bool] | None = None, group_id: int | None = None, description: str | None = None, mask: npt.NDArray[np.bool_] | None = None, ): self.label = label self.group_id = group_id self.points = [] self.point_labels = [] self.shape_type = shape_type self._shape_raw = None self._points_raw = [] self._shape_type_raw = None self.fill = False self.selected = False self.flags = flags self.description = description self.other_data = {} self.mask = mask # Highlight state: which vertex is highlighted and how it looks self._highlightIndex = None self._highlightMode = self.NEAR_VERTEX self._highlightSettings = { self.NEAR_VERTEX: (4, self.P_ROUND), self.MOVE_VERTEX: (1.5, self.P_SQUARE), } self._closed = False if line_color is not None: # Per-instance line color override (used for the pending line). self.line_color = line_color def _scale_point(self, point: QtCore.QPointF) -> QtCore.QPointF: return QtCore.QPointF(point.x() * self.scale, point.y() * self.scale) def setShapeRefined(self, shape_type, points, point_labels, mask=None): self._shape_raw = (self.shape_type, self.points, self.point_labels) self.shape_type = shape_type self.points = points self.point_labels = point_labels self.mask = mask def restoreShapeRaw(self): if self._shape_raw is None: return self.shape_type, self.points, self.point_labels = self._shape_raw self._shape_raw = None @property def shape_type(self): return self._shape_type @shape_type.setter def shape_type(self, value): if value is None: value = "polygon" if value not in [ "polygon", "rectangle", "point", "line", "circle", "linestrip", "points", "mask", ]: raise ValueError(f"Unexpected shape_type: {value}") self._shape_type = value def close(self): self._closed = True def addPoint(self, point, label=1): if self.points and point == self.points[0]: self.close() else: self.points.append(point) self.point_labels.append(label) def canAddPoint(self): return self.shape_type in ["polygon", "linestrip"] def popPoint(self): if self.points: if self.point_labels: self.point_labels.pop() return self.points.pop() return None def insertPoint(self, i, point, label=1): self.points.insert(i, point) self.point_labels.insert(i, label) def canRemovePoint(self) -> bool: if not self.canAddPoint(): return False if self.shape_type == "polygon" and len(self.points) <= 3: return False if self.shape_type == "linestrip" and len(self.points) <= 2: return False return True def removePoint(self, i: int): if not self.canRemovePoint(): logger.warning( "Cannot remove point from: shape_type=%r, len(points)=%d", self.shape_type, len(self.points), ) return self.points.pop(i) self.point_labels.pop(i) def isClosed(self): return self._closed def setOpen(self): self._closed = False def paint(self, painter): if self.mask is None and not self.points: return color = self.select_line_color if self.selected else self.line_color pen = QtGui.QPen(color) # Try using integer sizes for smoother drawing(?) pen.setWidth(self.PEN_WIDTH) painter.setPen(pen) if self.shape_type == "mask" and self.mask is not None: image_to_draw = np.zeros(self.mask.shape + (4,), dtype=np.uint8) fill_color = ( self.select_fill_color.getRgb() if self.selected else self.fill_color.getRgb() ) image_to_draw[self.mask] = fill_color qimage = QtGui.QImage.fromData(labelme.utils.img_arr_to_data(image_to_draw)) qimage = qimage.scaled( qimage.size() * self.scale, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation, ) painter.drawImage(self._scale_point(point=self.points[0]), qimage) line_path = QtGui.QPainterPath() contours = skimage.measure.find_contours(np.pad(self.mask, pad_width=1)) for contour in contours: contour += [self.points[0].y(), self.points[0].x()] line_path.moveTo( self._scale_point(QtCore.QPointF(contour[0, 1], contour[0, 0])) ) for point in contour[1:]: line_path.lineTo( self._scale_point(QtCore.QPointF(point[1], point[0])) ) painter.drawPath(line_path) if self.points: line_path = QtGui.QPainterPath() vrtx_path = QtGui.QPainterPath() negative_vrtx_path = QtGui.QPainterPath() if self.shape_type in ["rectangle", "mask"]: assert len(self.points) in [1, 2] if len(self.points) == 2: rectangle = QtCore.QRectF( self._scale_point(self.points[0]), self._scale_point(self.points[1]), ) line_path.addRect(rectangle) if self.shape_type == "rectangle": for i in range(len(self.points)): self.drawVertex(vrtx_path, i) elif self.shape_type == "circle": assert len(self.points) in [1, 2] if len(self.points) == 2: radius = labelme.utils.distance( self._scale_point(self.points[0] - self.points[1]) ) line_path.addEllipse( self._scale_point(self.points[0]), radius, radius ) for i in range(len(self.points)): self.drawVertex(vrtx_path, i) elif self.shape_type == "linestrip": line_path.moveTo(self._scale_point(self.points[0])) for i, p in enumerate(self.points): line_path.lineTo(self._scale_point(p)) self.drawVertex(vrtx_path, i) elif self.shape_type == "points": assert len(self.points) == len(self.point_labels) for i, point_label in enumerate(self.point_labels): if point_label == 1: self.drawVertex(vrtx_path, i) else: self.drawVertex(negative_vrtx_path, i) else: line_path.moveTo(self._scale_point(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(vrtx_path, 0) for i, p in enumerate(self.points): line_path.lineTo(self._scale_point(p)) self.drawVertex(vrtx_path, i) if self.isClosed(): line_path.lineTo(self._scale_point(self.points[0])) painter.drawPath(line_path) if vrtx_path.length() > 0: painter.drawPath(vrtx_path) painter.fillPath(vrtx_path, self._current_vertex_fill_color) if self.fill and self.shape_type not in [ "line", "linestrip", "points", "mask", ]: color = self.select_fill_color if self.selected else self.fill_color painter.fillPath(line_path, color) pen.setColor(QtGui.QColor(255, 0, 0, 255)) painter.setPen(pen) painter.drawPath(negative_vrtx_path) painter.fillPath(negative_vrtx_path, QtGui.QColor(255, 0, 0, 255)) def drawVertex(self, path, i): d = self.point_size shape = self.point_type point = self._scale_point(self.points[i]) if i == self._highlightIndex: size, shape = self._highlightSettings[self._highlightMode] d *= size # type: ignore[assignment] if self._highlightIndex is not None: self._current_vertex_fill_color = self.hvertex_fill_color else: self._current_vertex_fill_color = self.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 nearestVertex(self, point: QtCore.QPointF, epsilon: float) -> int | None: min_distance = float("inf") min_i = None point = self._scale_point(point) for i, p in enumerate(self.points): p = self._scale_point(p) dist = labelme.utils.distance(p - point) if dist <= epsilon and dist < min_distance: min_distance = dist min_i = i return min_i def nearestEdge(self, point: QtCore.QPointF, epsilon: float) -> int | None: min_distance = float("inf") post_i = None point = self._scale_point(point) for i in range(len(self.points)): start = self._scale_point(self.points[i - 1]) end = self._scale_point(self.points[i]) line = [start, end] dist = labelme.utils.distancetoline(point, line) if dist <= epsilon and dist < min_distance: min_distance = dist post_i = i return post_i def containsPoint(self, point: QtCore.QPointF) -> bool: if self.shape_type in ["line", "linestrip", "points"]: return False if self.shape_type == "point": if not self.points: return False return labelme.utils.distance(point - self.points[0]) <= self.point_size / 2 if self.mask is not None: raw_y = int(round(point.y() - self.points[0].y())) raw_x = int(round(point.x() - self.points[0].x())) if ( raw_y < 0 or raw_y >= self.mask.shape[0] or raw_x < 0 or raw_x >= self.mask.shape[1] ): return False return bool(self.mask[raw_y, raw_x]) return self.makePath().contains(point) def makePath(self): if self.shape_type in ["rectangle", "mask"]: path = QtGui.QPainterPath() if len(self.points) == 2: path.addRect(QtCore.QRectF(self.points[0], self.points[1])) elif self.shape_type == "circle": path = QtGui.QPainterPath() if len(self.points) == 2: raidus = labelme.utils.distance(self.points[0] - self.points[1]) path.addEllipse(self.points[0], raidus, raidus) else: path = QtGui.QPainterPath(self.points[0]) for p in self.points[1:]: path.lineTo(p) return path def boundingRect(self): return self.makePath().boundingRect() def moveBy(self, offset): self.points = [p + offset for p in self.points] def moveVertex(self, i: int, pos: QtCore.QPointF) -> None: self.points[i] = pos def highlightVertex(self, i: int, action: int) -> None: self._highlightIndex = i self._highlightMode = action def highlightClear(self) -> None: self._highlightIndex = None def copy(self): return copy.deepcopy(self) def __len__(self): return len(self.points) def __getitem__(self, key): return self.points[key] def __setitem__(self, key, value): self.points[key] = value ================================================ FILE: labelme/testing.py ================================================ import json import os.path as osp import imgviz import labelme.utils def assert_labelfile_sanity(filename): assert osp.exists(filename) data = json.load(open(filename)) assert "imagePath" in data imageData = data.get("imageData", None) if imageData is None: parent_dir = osp.dirname(filename) img_file = osp.join(parent_dir, data["imagePath"]) assert osp.exists(img_file) img = imgviz.io.imread(img_file) else: img = labelme.utils.img_b64_to_arr(imageData) H, W = img.shape[:2] assert H == data["imageHeight"] assert W == data["imageWidth"] assert "shapes" in data for shape in data["shapes"]: assert "label" in shape assert "points" in shape for x, y in shape["points"]: assert 0 <= x <= W assert 0 <= y <= H ================================================ FILE: labelme/translate/de_DE.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation KI-gestützte Annotation AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes KI schlägt Annotation in den Modi 'AI-Polygon' und 'AI-Mask' vor Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Wählen Sie den Modus 'AI-Polygon' oder 'AI-Mask', um AI-Assisted Annotation zu aktivieren AiTextToAnnotationWidget AI Text-to-Annotation KI-Prompt e.g., dog,cat,bird z.B. Hund,Katze,Vogel Run Ausführen Score Score IoU IoU AI creates annotations from the text prompt KI erstellt Annotationen aus dem Textprompt Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Wählen Sie den Modus 'Polygon', 'Rechteck', 'AI-Polygon' oder 'AI-Maske' zum Aktivieren Canvas Creating %r %r wird erstellt ESC to cancel ESC zum Abbrechen Enter or Space to finalize Enter oder Leertaste zum Abschließen Editing shapes Formen bearbeiten Click points to include or Shift+Click to exclude for ai_polygon Punkte anklicken zum Einbeziehen oder Shift+Klick zum Ausschließen (KI-Polygon) Click points to include or Shift+Click to exclude for ai_mask Punkte anklicken zum Einbeziehen oder Shift+Klick zum Ausschließen (KI-Maske) Click start point for line Startpunkt der Linie anklicken Click end point for line Endpunkt der Linie anklicken Click start point for linestrip Startpunkt der Linienfolge anklicken Click next point or finish by Ctrl/Cmd+Click for linestrip Nächsten Punkt anklicken oder mit Ctrl/Cmd+Klick abschließen (Linienfolge) Click center point for circle Mittelpunkt des Kreises anklicken Click point on circumference for circle Punkt auf dem Kreisumfang anklicken Click first corner for rectangle Erste Ecke des Rechtecks anklicken Click to add point Klicken zum Hinzufügen eines Punktes Click & drag to move point Klicken und ziehen zum Verschieben des Punktes ALT + SHIFT + Click to delete point ALT + SHIFT + Klick zum Löschen des Punktes ALT + Click to create point on shape ALT + Klick zum Erstellen eines Punktes auf der Form Click & drag to move shape Klicken und ziehen zum Verschieben der Form Right-click & drag to copy shape Rechtsklick und Ziehen, um die Form zu kopieren Click opposite corner for rectangle (Shift for square) Gegenüberliegende Ecke des Rechtecks anklicken (Shift für Quadrat) MainWindow Flags Markierungen Annotation List Annotationsliste Select label to start annotating for it. Press 'Esc' to deselect. Label auswählen, um mit der Annotation zu beginnen. Mit 'Esc' abwählen. Label List Label-Liste Search Filename Dateiname suchen File List Dateiliste &Quit &Beenden Quit application Anwendung beenden &Open &Öffnen Open image or label file Bild- oder Label-Datei öffnen Open Dir Verzeichnis öffnen &Next Image &Nächstes Bild Open next (hold Ctl+Shift to copy labels) Nächstes öffnen (Strg+Umschalt gedrückt halten, um Labels zu kopieren) &Prev Image &Vorheriges Bild Open prev (hold Ctl+Shift to copy labels) Vorheriges öffnen (Strg+Umschalt gedrückt halten, um Labels zu kopieren) &Save &Speichern Save labels to file Labels in Datei speichern &Save As Speichern &unter Save labels to a different file Labels in anderer Datei speichern &Delete File &Datei löschen Delete current label file Aktuelle Label-Datei löschen &Change Output Dir &Ausgabeverzeichnis ändern Change where annotations are loaded/saved Ändern, wo Annotationen geladen/gespeichert werden Save &Automatically &Automatisch speichern Save automatically Automatisch speichern Save With Image Data Mit Bilddaten speichern Save image data in label file Bilddaten in Label-Datei speichern &Close &Schließen Close current file Aktuelle Datei schließen Keep Previous Annotation Vorherige Annotation beibehalten Toggle "keep previous annotation" mode "Vorherige Annotation beibehalten" Modus umschalten Create Polygons Polygone erstellen Start drawing polygons Mit dem Zeichnen von Polygonen beginnen Create Rectangle Rechteck erstellen Start drawing rectangles Mit dem Zeichnen von Rechtecken beginnen Create Circle Kreis erstellen Start drawing circles Mit dem Zeichnen von Kreisen beginnen Create Line Linie erstellen Start drawing lines Mit dem Zeichnen von Linien beginnen Create Point Punkt erstellen Start drawing points Mit dem Zeichnen von Punkten beginnen Create LineStrip Linienfolge erstellen Start drawing linestrip. Ctrl+LeftClick ends creation. Mit dem Zeichnen einer Linienfolge beginnen. Strg+Linksklick beendet die Erstellung. Create AI-Polygon KI-Polygon erstellen Start drawing ai_polygon. Ctrl+LeftClick ends creation. Mit dem Zeichnen eines KI-Polygons beginnen. Strg+Linksklick beendet die Erstellung. Create AI-Mask KI-Maske erstellen Start drawing ai_mask. Ctrl+LeftClick ends creation. Mit dem Zeichnen einer KI-Maske beginnen. Strg+Linksklick beendet die Erstellung. Edit Shapes Formen bearbeiten Move and edit the selected shapes Ausgewählte Formen verschieben und bearbeiten Delete Shapes Formen löschen Delete the selected shapes Ausgewählte Formen löschen Duplicate Shapes Formen duplizieren Create a duplicate of the selected shapes Duplikat der ausgewählten Formen erstellen Copy Shapes Formen kopieren Copy selected shapes to clipboard Ausgewählte Formen in Zwischenablage kopieren Paste Shapes Formen einfügen Paste copied shapes Kopierte Formen einfügen Undo last point Letzten Punkt rückgängig machen Undo last drawn point Letzten gezeichneten Punkt rückgängig machen Remove Selected Point Ausgewählten Punkt entfernen Remove selected point from polygon Ausgewählten Punkt aus Polygon entfernen Undo Rückgängig Undo last add and edit of shape Letztes Hinzufügen und Bearbeiten der Form rückgängig machen &Hide Shapes &Verbergen Formen Hide all shapes Alle Formen verbergen &Show Shapes &Anzeigen Formen Show all shapes Alle Formen anzeigen &Toggle Shapes &Umschalten Formen Toggle all shapes Alle Formen umschalten &Tutorial &Tutorial Show tutorial page Tutorial-Seite anzeigen Zoom Zoom Zoom in or out of the image. Also accessible with {} and {} from the canvas. Bild vergrößern oder verkleinern. Auch mit {} und {} von der Leinwand aus zugänglich. Ctrl+Wheel Strg+Mausrad Zoom &In &Vergrößern Increase zoom level Zoomstufe erhöhen &Zoom Out &Verkleinern Decrease zoom level Zoomstufe verringern &Original size &Originalgröße Zoom to original size Auf Originalgröße zoomen &Keep Previous Scale &Vorherige Skalierung beibehalten Keep previous zoom scale Vorherige Zoomskalierung beibehalten &Fit Window &An Fenster anpassen Zoom follows window size Zoom folgt der Fenstergröße Fit &Width &An Breite anpassen Zoom follows window width Zoom folgt der Fensterbreite &Brightness Contrast &Helligkeit/Kontrast Adjust brightness and contrast Helligkeit und Kontrast einstellen &Edit Label &Label bearbeiten Modify the label of the selected shape Label der ausgewählten Form ändern Fill Drawing Polygon Gezeichnetes Polygon füllen Fill polygon while drawing Polygon beim Zeichnen füllen Keep Previous Brightness/Contrast Vorherige Helligkeit/Kontrast beibehalten &File &Datei &Edit &Bearbeiten &View &Ansicht &Help &Hilfe Open &Recent &Zuletzt geöffnet %s started. %s gestartet. Invalid label Ungültiges Label Invalid label '{}' with validation type '{}' Ungültiges Label '{}' mit Validierungstyp '{}' Error saving label data Fehler beim Speichern der Labeldaten <b>%s</b> <b>%s</b> Error opening file Fehler beim Öffnen der Datei No such file: <b>%s</b> Datei nicht gefunden: <b>%s</b> Loading %s... %s wird geladen... Error reading %s Fehler beim Lesen von %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Stellen Sie sicher, dass <i>{0}</i> eine gültige Bilddatei ist.<br/>Unterstützte Bildformate: {1}</p> Loaded %s %s geladen Image & Label files (%s) Bild- und Label-Dateien (%s) %s - Choose Image or Label file %s - Bild- oder Label-Datei auswählen %s - Save/Load Annotations in Directory %s - Annotationen im Verzeichnis speichern/laden %s . Annotations will be saved/loaded in %s %s . Annotationen werden in %s gespeichert/geladen %s - Choose File %s - Datei auswählen Label files (*%s) Label-Dateien (*%s) Choose File Datei auswählen You are about to permanently delete this label file, proceed anyway? Möchten Sie diese Label-Datei endgültig löschen? Attention Achtung Save annotations to "{}" before closing? Annotationen in "{}" speichern vor dem Schließen? Save annotations? Annotationen speichern? You are about to permanently delete {} shapes, proceed anyway? Möchten Sie {} Formen endgültig löschen? %s - Open Directory %s - Verzeichnis öffnen Preferences… Einstellungen… Open config file in text editor Konfigurationsdatei im Texteditor öffnen No Config File Keine Konfigurationsdatei Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc Die Konfiguration wurde als YAML-Ausdruck über die Kommandozeile bereitgestellt. Um den Einstellungseditor zu verwenden, starten Sie Labelme mit einer Konfigurationsdatei: labelme --config ~/.labelmerc Configuration Errors Konfigurationsfehler Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Beim Laden der Konfiguration wurden Fehler gefunden. Bitte überprüfen Sie die folgenden Fehler und laden Sie Ihre Konfiguration neu oder ignorieren Sie die fehlerhaften Zeilen. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Stellen Sie sicher, dass <i>%s</i> eine gültige Beschriftungsdatei ist.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Stellen Sie sicher, dass <i>%s</i> eine gültige Bilddatei ist.</p> Reset Layout Layout zurücksetzen ================================================ FILE: labelme/translate/es_ES.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation Anotación asistida por IA AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes La IA sugiere anotación en los modos 'AI-Polygon' y 'AI-Mask' Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Seleccione el modo 'AI-Polygon' o 'AI-Mask' para habilitar AI-Assisted Annotation AiTextToAnnotationWidget AI Text-to-Annotation Indicación IA e.g., dog,cat,bird p. ej., perro, gato, pájaro Run Ejecutar Score Puntuación IoU IoU AI creates annotations from the text prompt La IA crea anotaciones a partir del texto Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Seleccione el modo 'Polygon', 'Rectangle', 'AI-Polygon' o 'AI-Mask' para habilitar Canvas Click & drag to move point Haz clic y arrastra para mover el punto Click & drag to move shape Haz clic y arrastra para mover la forma Creating %r Creando %r ESC to cancel ESC para cancelar Enter or Space to finalize Enter o Espacio para finalizar Editing shapes Editando formas Click points to include or Shift+Click to exclude for ai_polygon Haz clic en los puntos para incluir o Mayús+Clic para excluir (polígono IA) Click points to include or Shift+Click to exclude for ai_mask Haz clic en los puntos para incluir o Mayús+Clic para excluir (máscara IA) Click start point for line Haz clic en el punto inicial de la línea Click end point for line Haz clic en el punto final de la línea Click start point for linestrip Haz clic en el punto inicial de la línea continua Click next point or finish by Ctrl/Cmd+Click for linestrip Haz clic en el siguiente punto o Ctrl/Cmd+Clic para finalizar (línea continua) Click center point for circle Haz clic en el punto central del círculo Click point on circumference for circle Haz clic en un punto de la circunferencia del círculo Click first corner for rectangle Haz clic en la primera esquina del rectángulo Click to add point Haz clic para añadir un punto ALT + SHIFT + Click to delete point ALT + MAYÚS + Clic para eliminar el punto ALT + Click to create point on shape ALT + Clic para crear un punto en la forma Right-click & drag to copy shape Clic derecho y arrastra para copiar la forma Click opposite corner for rectangle (Shift for square) Haz clic en la esquina opuesta del rectángulo (Shift para cuadrado) MainWindow Flags Marcadores Annotation List Lista de anotaciones Select label to start annotating for it. Press 'Esc' to deselect. Selecciona una etiqueta para comenzar a anotar. Presiona 'Esc' para deseleccionar. Label List Lista de etiquetas Search Filename Buscar nombre de archivo File List Lista de archivos &Quit &Salir Quit application Salir de la aplicación &Open &Abrir Open image or label file Abrir archivo de imagen o etiqueta Open Dir Abrir directorio &Next Image Imagen &siguiente Open next (hold Ctl+Shift to copy labels) Abrir siguiente (mantén Ctrl+Mayús para copiar etiquetas) &Prev Image Imagen &anterior Open prev (hold Ctl+Shift to copy labels) Abrir anterior (mantén Ctrl+Mayús para copiar etiquetas) &Save &Guardar Save labels to file Guardar etiquetas en archivo &Save As Guardar &como Save labels to a different file Guardar etiquetas en un archivo diferente &Delete File &Eliminar Delete current label file Eliminar archivo de etiqueta actual &Change Output Dir Cambiar directorio de &salida Change where annotations are loaded/saved Cambiar dónde se cargan/guardan las anotaciones Save &Automatically Guardar &automáticamente Save automatically Guardar automáticamente Save With Image Data Guardar con datos de imagen Save image data in label file Guardar datos de imagen en el archivo de etiqueta &Close &Cerrar Close current file Cerrar archivo actual Keep Previous Annotation Mantener anotación anterior Create Polygons Crear polígonos Start drawing polygons Empezar a dibujar polígonos Create Rectangle Crear rectángulo Start drawing rectangles Empezar a dibujar rectángulos Create Circle Crear círculo Start drawing circles Empezar a dibujar círculos Create Line Crear línea Start drawing lines Empezar a dibujar líneas Create Point Crear punto Start drawing points Empezar a dibujar puntos Create LineStrip Crear línea continua Start drawing linestrip. Ctrl+LeftClick ends creation. Empezar a dibujar línea continua. Ctrl+Clic izquierdo finaliza la creación. Create AI-Polygon Crear polígono IA Start drawing ai_polygon. Ctrl+LeftClick ends creation. Empezar a dibujar polígono IA. Ctrl+Clic izquierdo finaliza la creación. Create AI-Mask Crear máscara IA Start drawing ai_mask. Ctrl+LeftClick ends creation. Empezar a dibujar máscara IA. Ctrl+Clic izquierdo finaliza la creación. Edit Shapes Editar formas Move and edit the selected shapes Mover y editar las formas seleccionadas Delete Shapes Eliminar formas Delete the selected shapes Eliminar las formas seleccionadas Duplicate Shapes Duplicar formas Create a duplicate of the selected shapes Crear un duplicado de las formas seleccionadas Copy Shapes Copiar formas Copy selected shapes to clipboard Copiar formas seleccionadas al portapapeles Paste Shapes Pegar formas Paste copied shapes Pegar formas copiadas Undo last point Deshacer último punto Undo last drawn point Deshacer último punto dibujado Remove Selected Point Eliminar punto seleccionado Remove selected point from polygon Eliminar punto seleccionado del polígono Undo Deshacer Undo last add and edit of shape Deshacer última adición y edición de forma &Hide Shapes Ocultar &formas Hide all shapes Ocultar todas las formas &Show Shapes Mostrar &formas Show all shapes Mostrar todas las formas &Toggle Shapes Alternar &formas Toggle all shapes Alternar todas las formas &Tutorial &Tutorial Show tutorial page Mostrar página del tutorial Zoom Zoom Zoom in or out of the image. Also accessible with {} and {} from the canvas. Acercar o alejar la imagen. También accesible con {} y {} desde el lienzo. Ctrl+Wheel Ctrl+Rueda Zoom &In &Acercar Increase zoom level Aumentar nivel de zoom &Zoom Out &Alejar Decrease zoom level Disminuir nivel de zoom &Original size Tamaño &original Zoom to original size Zoom al tamaño original &Keep Previous Scale Mantener escala &anterior Keep previous zoom scale Mantener escala de zoom anterior &Fit Window Ajustar a &ventana Zoom follows window size El zoom sigue el tamaño de la ventana Fit &Width Ajustar a &ancho Zoom follows window width El zoom sigue el ancho de la ventana &Brightness Contrast Brillo y &contraste Adjust brightness and contrast Ajustar brillo y contraste &Edit Label &Editar etiqueta Modify the label of the selected shape Modificar la etiqueta de la forma seleccionada Fill Drawing Polygon Rellenar polígono al dibujar Fill polygon while drawing Rellenar polígono mientras se dibuja &File &Archivo &Edit &Editar &View &Ver &Help &Ayuda Open &Recent Abrir &recientes %s started. %s iniciado. Invalid label Etiqueta no válida Invalid label '{}' with validation type '{}' Etiqueta no válida '{}' con tipo de validación '{}' Error saving label data Error al guardar datos de etiqueta <b>%s</b> <b>%s</b> Error opening file Error al abrir archivo No such file: <b>%s</b> No existe el archivo: <b>%s</b> Loading %s... Cargando %s... Error reading %s Error al leer %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Asegúrate de que <i>{0}</i> sea un archivo de imagen válido.<br/>Formatos de imagen admitidos: {1}</p> Loaded %s Cargado %s Image & Label files (%s) Archivos de imagen y etiqueta (%s) %s - Choose Image or Label file %s - Elegir archivo de imagen o etiqueta %s - Save/Load Annotations in Directory %s - Guardar/Cargar anotaciones en directorio %s . Annotations will be saved/loaded in %s %s . Las anotaciones se guardarán/cargarán en %s %s - Choose File %s - Elegir archivo Label files (*%s) Archivos de etiqueta (*%s) Choose File Elegir archivo You are about to permanently delete this label file, proceed anyway? Estás a punto de eliminar permanentemente este archivo de etiqueta, ¿continuar de todos modos? Attention Atención Save annotations to "{}" before closing? ¿Guardar anotaciones en "{}" antes de cerrar? Save annotations? ¿Guardar anotaciones? You are about to permanently delete {} shapes, proceed anyway? Estás a punto de eliminar permanentemente {} formas, ¿continuar de todos modos? %s - Open Directory %s - Abrir directorio Toggle "keep previous annotation" mode Alternar modo "mantener anotación anterior" Keep Previous Brightness/Contrast Mantener brillo/contraste anterior Preferences… Preferencias… Open config file in text editor Abrir archivo de configuración en editor de texto No Config File Sin archivo de configuración Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc La configuración se proporcionó como una expresión YAML a través de la línea de comandos. Para usar el editor de preferencias, inicie Labelme con un archivo de configuración: labelme --config ~/.labelmerc Configuration Errors Errores de Configuración Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Se encontraron errores al cargar la configuración. Por favor, revise los errores a continuación y recargue su configuración o ignore las líneas erróneas. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Asegúrate de que <i>%s</i> sea un archivo de etiqueta válido.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Asegúrate de que <i>%s</i> sea un archivo de imagen válido.</p> Reset Layout Restablecer diseño ================================================ FILE: labelme/translate/fa_IR.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation حاشیه‌نویسی با کمک هوش مصنوعی AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes هوش مصنوعی حاشیه‌نویسی را در حالت‌های 'AI-Polygon' و 'AI-Mask' پیشنهاد می‌دهد Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation برای فعال کردن AI-Assisted Annotation، حالت «AI-Polygon» یا «AI-Mask» را انتخاب کنید AiTextToAnnotationWidget AI Text-to-Annotation پیشنهاد هوش مصنوعی e.g., dog,cat,bird مثال: سگ، گربه، پرنده Run اجرا Score امتیاز IoU IoU AI creates annotations from the text prompt هوش مصنوعی حاشیه‌نویسی‌ها را از متن ایجاد می‌کند Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable برای فعال کردن، حالت «Polygon»، «Rectangle»، «AI-Polygon» یا «AI-Mask» را انتخاب کنید Canvas Click & drag to move point کلیک و کشیدن برای جابجایی نقطه Click & drag to move shape کلیک و کشیدن برای جابجایی شکل Creating %r در حال ایجاد %r ESC to cancel ESC برای لغو Enter or Space to finalize Enter یا Space برای نهایی کردن Editing shapes ویرایش اشکال Click points to include or Shift+Click to exclude for ai_polygon کلیک برای شامل کردن نقاط یا Shift+کلیک برای حذف (چندضلعی هوش مصنوعی) Click points to include or Shift+Click to exclude for ai_mask کلیک برای شامل کردن نقاط یا Shift+کلیک برای حذف (ماسک هوش مصنوعی) Click start point for line کلیک روی نقطه شروع خط Click end point for line کلیک روی نقطه پایان خط Click start point for linestrip کلیک روی نقطه شروع خط چندتکه‌ای Click next point or finish by Ctrl/Cmd+Click for linestrip کلیک روی نقطه بعدی یا Ctrl/Cmd+کلیک برای پایان خط چندتکه‌ای Click center point for circle کلیک روی نقطه مرکز دایره Click point on circumference for circle کلیک روی نقطه روی محیط دایره Click first corner for rectangle کلیک روی گوشه اول مستطیل Click to add point کلیک برای افزودن نقطه ALT + SHIFT + Click to delete point ALT + SHIFT + کلیک برای حذف نقطه ALT + Click to create point on shape ALT + کلیک برای ایجاد نقطه روی شکل Right-click & drag to copy shape کلیک راست و کشیدن برای کپی شکل Click opposite corner for rectangle (Shift for square) کلیک روی گوشه مقابل مستطیل (Shift برای مربع) MainWindow Flags پرچم‌ها Annotation List فهرست حاشیه‌نویسی‌ها Select label to start annotating for it. Press 'Esc' to deselect. برچسب را انتخاب کنید تا شروع به حاشیه‌نویسی کنید. 'Esc' را فشار دهید تا انتخاب لغو شود. Label List فهرست برچسب‌ها Search Filename جستجوی نام فایل File List فهرست فایل‌ها &Quit خروج(&Q) Quit application خروج از برنامه &Open باز کردن(&O) Open image or label file باز کردن فایل تصویر یا برچسب Open Dir باز کردن پوشه &Next Image تصویر بعدی(&N) Open next (hold Ctl+Shift to copy labels) باز کردن بعدی (Ctrl+Shift را نگه دارید تا برچسب‌ها کپی شوند) &Prev Image تصویر قبلی(&P) Open prev (hold Ctl+Shift to copy labels) باز کردن قبلی (Ctrl+Shift را نگه دارید تا برچسب‌ها کپی شوند) &Save ذخیره(&S) Save labels to file ذخیره برچسب‌ها در فایل &Save As ذخیره با نام دیگر(&S) Save labels to a different file ذخیره برچسب‌ها در فایل دیگر &Delete File حذف(&D) Delete current label file حذف فایل برچسب فعلی &Change Output Dir تغییر مسیر خروجی(&C) Change where annotations are loaded/saved تغییر محل بارگذاری/ذخیره حاشیه‌نویسی‌ها Save &Automatically ذخیره خودکار(&A) Save automatically ذخیره خودکار Save With Image Data ذخیره با داده تصویر Save image data in label file ذخیره داده تصویر در فایل برچسب &Close بستن(&C) Close current file بستن فایل فعلی Keep Previous Annotation نگه داشتن حاشیه‌نویسی قبلی Create Polygons ایجاد چندضلعی Start drawing polygons شروع رسم چندضلعی Create Rectangle ایجاد مستطیل Start drawing rectangles شروع رسم مستطیل Create Circle ایجاد دایره Start drawing circles شروع رسم دایره Create Line ایجاد خط Start drawing lines شروع رسم خط Create Point ایجاد نقطه Start drawing points شروع رسم نقطه Create LineStrip ایجاد خط چندتکه‌ای Start drawing linestrip. Ctrl+LeftClick ends creation. شروع رسم خط چندتکه‌ای. Ctrl+کلیک چپ برای پایان. Create AI-Polygon ایجاد چندضلعی هوش مصنوعی Start drawing ai_polygon. Ctrl+LeftClick ends creation. شروع رسم چندضلعی هوش مصنوعی. Ctrl+کلیک چپ برای پایان. Create AI-Mask ایجاد ماسک هوش مصنوعی Start drawing ai_mask. Ctrl+LeftClick ends creation. شروع رسم ماسک هوش مصنوعی. Ctrl+کلیک چپ برای پایان. Edit Shapes ویرایش شکل Move and edit the selected shapes جابجایی و ویرایش شکل‌های انتخاب شده Delete Shapes حذف شکل Delete the selected shapes حذف شکل‌های انتخاب شده Duplicate Shapes تکثیر شکل Create a duplicate of the selected shapes ایجاد کپی از شکل‌های انتخاب شده Copy Shapes کپی شکل Copy selected shapes to clipboard کپی شکل‌های انتخاب شده به کلیپ‌بورد Paste Shapes چسباندن شکل Paste copied shapes چسباندن شکل‌های کپی شده Undo last point بازگشت آخرین نقطه Undo last drawn point بازگشت آخرین نقطه رسم شده Remove Selected Point حذف نقطه انتخاب شده Remove selected point from polygon حذف نقطه انتخاب شده از چندضلعی Undo بازگشت Undo last add and edit of shape بازگشت آخرین افزودن و ویرایش شکل &Hide Shapes مخفی کردن شکل(&H) Hide all shapes مخفی کردن همه شکل‌ها &Show Shapes نمایش شکل(&S) Show all shapes نمایش همه شکل‌ها &Toggle Shapes تغییر وضعیت شکل(&S) Toggle all shapes تغییر وضعیت همه شکل‌ها &Tutorial آموزش(&T) Show tutorial page نمایش صفحه آموزش Zoom زوم Zoom in or out of the image. Also accessible with {} and {} from the canvas. زوم کردن تصویر. همچنین از طریق {} و {} در بوم قابل دسترسی است. Ctrl+Wheel Ctrl+چرخ Zoom &In بزرگ‌نمایی(&I) Increase zoom level افزایش سطح زوم &Zoom Out کوچک‌نمایی(&Z) Decrease zoom level کاهش سطح زوم &Original size اندازه اصلی(&O) Zoom to original size زوم به اندازه اصلی &Keep Previous Scale نگه داشتن مقیاس قبلی(&K) Keep previous zoom scale نگه داشتن مقیاس زوم قبلی &Fit Window تناسب با پنجره(&F) Zoom follows window size زوم متناسب با اندازه پنجره Fit &Width تناسب با عرض(&W) Zoom follows window width زوم متناسب با عرض پنجره &Brightness Contrast روشنایی و کنتراست(&B) Adjust brightness and contrast تنظیم روشنایی و کنتراست &Edit Label ویرایش برچسب(&E) Modify the label of the selected shape تغییر برچسب شکل انتخاب شده Fill Drawing Polygon پر کردن چندضلعی رسم شده Fill polygon while drawing پر کردن چندضلعی هنگام رسم &File فایل(&F) &Edit ویرایش(&E) &View نمایش(&V) &Help راهنما(&H) Open &Recent باز کردن اخیر(&R) %s started. %s راه‌اندازی شد. Invalid label برچسب نامعتبر Invalid label '{}' with validation type '{}' برچسب نامعتبر '{}' با نوع اعتبارسنجی '{}' Error saving label data خطا در ذخیره داده برچسب <b>%s</b> <b>%s</b> Error opening file خطا در باز کردن فایل No such file: <b>%s</b> چنین فایلی وجود ندارد: <b>%s</b> Loading %s... در حال بارگذاری %s... Error reading %s خطا در خواندن %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>مطمئن شوید <i>{0}</i> یک فایل تصویر معتبر است.<br/>فرمت‌های تصویر پشتیبانی شده: {1}</p> Loaded %s بارگذاری شد %s Image & Label files (%s) فایل‌های تصویر و برچسب (%s) %s - Choose Image or Label file %s - انتخاب فایل تصویر یا برچسب %s - Save/Load Annotations in Directory %s - ذخیره/بارگذاری حاشیه‌نویسی‌ها در پوشه %s . Annotations will be saved/loaded in %s %s . حاشیه‌نویسی‌ها در %s ذخیره/بارگذاری خواهند شد %s - Choose File %s - انتخاب فایل Label files (*%s) فایل‌های برچسب (*%s) Choose File انتخاب فایل You are about to permanently delete this label file, proceed anyway? شما در حال حذف دائمی این فایل برچسب هستید، ادامه دهید؟ Attention توجه Save annotations to "{}" before closing? قبل از بستن، حاشیه‌نویسی‌ها را در "{}" ذخیره کنید؟ Save annotations? ذخیره حاشیه‌نویسی‌ها؟ You are about to permanently delete {} shapes, proceed anyway? شما در حال حذف دائمی {} شکل هستید، ادامه دهید؟ %s - Open Directory %s - باز کردن پوشه Toggle "keep previous annotation" mode تغییر وضعیت حالت "نگه داشتن حاشیه‌نویسی قبلی" Keep Previous Brightness/Contrast نگه داشتن روشنایی/کنتراست قبلی Preferences… تنظیمات… Open config file in text editor باز کردن فایل پیکربندی در ویرایشگر متن No Config File فایل پیکربندی وجود ندارد Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc پیکربندی به صورت عبارت YAML از طریق خط فرمان ارائه شده است. برای استفاده از ویرایشگر تنظیمات، Labelme را با یک فایل پیکربندی اجرا کنید: labelme --config ~/.labelmerc Configuration Errors خطاهای پیکربندی Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. هنگام بارگیری پیکربندی خطاهایی یافت شد. لطفاً خطاهای زیر را بررسی کنید و پیکربندی خود را مجدداً بارگیری کنید یا خطوط نادرست را نادیده بگیرید. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>مطمئن شوید <i>%s</i> یک فایل برچسب معتبر است.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>مطمئن شوید <i>%s</i> یک فایل تصویر معتبر است.</p> Reset Layout بازنشانی چیدمان ================================================ FILE: labelme/translate/fr_FR.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation Annotation assistée par IA AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes L'IA suggère l'annotation dans les modes 'AI-Polygon' et 'AI-Mask' Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Sélectionnez le mode 'AI-Polygon' ou 'AI-Mask' pour activer AI-Assisted Annotation AiTextToAnnotationWidget AI Text-to-Annotation Invite IA e.g., dog,cat,bird ex. : chien,chat,oiseau Run Exécuter Score Score IoU IoU AI creates annotations from the text prompt L'IA crée des annotations à partir du texte Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Sélectionnez le mode 'Polygon', 'Rectangle', 'AI-Polygon' ou 'AI-Mask' pour activer Canvas Click & drag to move point Cliquer et glisser pour déplacer le point Click & drag to move shape Cliquer et glisser pour déplacer la forme Creating %r Création de %r ESC to cancel ESC pour annuler Enter or Space to finalize Entrée ou Espace pour finaliser Editing shapes Modification des formes Click points to include or Shift+Click to exclude for ai_polygon Cliquer sur les points à inclure ou Maj+Cliquer pour exclure (polygone IA) Click points to include or Shift+Click to exclude for ai_mask Cliquer sur les points à inclure ou Maj+Cliquer pour exclure (masque IA) Click start point for line Cliquer sur le point de départ de la ligne Click end point for line Cliquer sur le point d'arrivée de la ligne Click start point for linestrip Cliquer sur le point de départ de la polyligne Click next point or finish by Ctrl/Cmd+Click for linestrip Cliquer sur le point suivant ou Ctrl/Cmd+Cliquer pour terminer la polyligne Click center point for circle Cliquer sur le point central du cercle Click point on circumference for circle Cliquer sur un point de la circonférence du cercle Click first corner for rectangle Cliquer sur le premier coin du rectangle Click to add point Cliquer pour ajouter un point ALT + SHIFT + Click to delete point ALT + MAJ + Cliquer pour supprimer le point ALT + Click to create point on shape ALT + Cliquer pour créer un point sur la forme Right-click & drag to copy shape Clic droit et glisser pour copier la forme Click opposite corner for rectangle (Shift for square) Cliquer sur le coin opposé du rectangle (Shift pour carré) MainWindow Flags Drapeaux Annotation List Liste des annotations Select label to start annotating for it. Press 'Esc' to deselect. Sélectionner une étiquette pour commencer l'annotation. Appuyer sur 'Esc' pour désélectionner. Label List Liste des étiquettes Search Filename Rechercher un nom de fichier File List Liste des fichiers &Quit &Quitter Quit application Quitter l'application &Open &Ouvrir Open image or label file Ouvrir une image ou un fichier d'étiquettes Open Dir Ouvrir le répertoire &Next Image Image &suivante Open next (hold Ctl+Shift to copy labels) Ouvrir la suivante (maintenir Ctrl+Maj pour copier les étiquettes) &Prev Image Image &précédente Open prev (hold Ctl+Shift to copy labels) Ouvrir la précédente (maintenir Ctrl+Maj pour copier les étiquettes) &Save &Enregistrer Save labels to file Enregistrer les étiquettes dans un fichier &Save As Enregistrer &sous Save labels to a different file Enregistrer les étiquettes dans un autre fichier &Delete File &Supprimer le fichier Delete current label file Supprimer le fichier d'étiquettes actuel &Change Output Dir &Changer le répertoire de sortie Change where annotations are loaded/saved Changer où les annotations sont chargées/enregistrées Save &Automatically Enregistrer &automatiquement Save automatically Enregistrer automatiquement Save With Image Data Enregistrer avec les données d'image Save image data in label file Enregistrer les données d'image dans le fichier d'étiquettes &Close &Fermer Close current file Fermer le fichier actuel Keep Previous Annotation Conserver l'annotation précédente Create Polygons Créer des polygones Start drawing polygons Commencer à dessiner des polygones Create Rectangle Créer un rectangle Start drawing rectangles Commencer à dessiner des rectangles Create Circle Créer un cercle Start drawing circles Commencer à dessiner des cercles Create Line Créer une ligne Start drawing lines Commencer à dessiner des lignes Create Point Créer un point Start drawing points Commencer à dessiner des points Create LineStrip Créer une polyligne Start drawing linestrip. Ctrl+LeftClick ends creation. Commencer à dessiner une polyligne. Ctrl+Clic gauche termine la création. Create AI-Polygon Créer un polygone IA Start drawing ai_polygon. Ctrl+LeftClick ends creation. Commencer à dessiner un polygone IA. Ctrl+Clic gauche termine la création. Create AI-Mask Créer un masque IA Start drawing ai_mask. Ctrl+LeftClick ends creation. Commencer à dessiner un masque IA. Ctrl+Clic gauche termine la création. Edit Shapes Modifier les formes Move and edit the selected shapes Déplacer et modifier les formes sélectionnées Delete Shapes Supprimer les formes Delete the selected shapes Supprimer les formes sélectionnées Duplicate Shapes Dupliquer les formes Create a duplicate of the selected shapes Créer un doublon des formes sélectionnées Copy Shapes Copier les formes Copy selected shapes to clipboard Copier les formes sélectionnées dans le presse-papiers Paste Shapes Coller les formes Paste copied shapes Coller les formes copiées Undo last point Annuler le dernier point Undo last drawn point Annuler le dernier point dessiné Remove Selected Point Supprimer le point sélectionné Remove selected point from polygon Supprimer le point sélectionné du polygone Undo Annuler Undo last add and edit of shape Annuler le dernier ajout et la dernière modification de forme &Hide Shapes &Masquer les formes Hide all shapes Masquer toutes les formes &Show Shapes &Afficher les formes Show all shapes Afficher toutes les formes &Toggle Shapes &Basculer les formes Toggle all shapes Basculer toutes les formes &Tutorial &Tutoriel Show tutorial page Afficher la page du tutoriel Zoom Zoom Zoom in or out of the image. Also accessible with {} and {} from the canvas. Zoomer ou dézoomer l'image. Également accessible avec {} et {} depuis le canevas. Ctrl+Wheel Ctrl+Molette Zoom &In Zoom &avant Increase zoom level Augmenter le niveau de zoom &Zoom Out Zoom &arrière Decrease zoom level Diminuer le niveau de zoom &Original size Taille &d'origine Zoom to original size Zoomer à la taille d'origine &Keep Previous Scale &Conserver l'échelle précédente Keep previous zoom scale Conserver l'échelle de zoom précédente &Fit Window &Adapter à la fenêtre Zoom follows window size Le zoom suit la taille de la fenêtre Fit &Width Adapter la &largeur Zoom follows window width Le zoom suit la largeur de la fenêtre &Brightness Contrast &Luminosité et contraste Adjust brightness and contrast Ajuster la luminosité et le contraste &Edit Label &Modifier l'étiquette Modify the label of the selected shape Modifier l'étiquette de la forme sélectionnée Fill Drawing Polygon Remplir le polygone en cours de dessin Fill polygon while drawing Remplir le polygone pendant le dessin &File &Fichier &Edit &Modifier &View &Affichage &Help &Aide Open &Recent Fichiers &récents %s started. %s démarré. Invalid label Étiquette invalide Invalid label '{}' with validation type '{}' Étiquette invalide '{}' avec le type de validation '{}' Error saving label data Erreur lors de l'enregistrement des données d'étiquettes <b>%s</b> <b>%s</b> Error opening file Erreur lors de l'ouverture du fichier No such file: <b>%s</b> Fichier introuvable : <b>%s</b> Loading %s... Chargement de %s... Error reading %s Erreur lors de la lecture de %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Assurez-vous que <i>{0}</i> est un fichier d'image valide.<br/>Formats d'image pris en charge : {1}</p> Loaded %s %s chargé Image & Label files (%s) Fichiers Image & Étiquettes (%s) %s - Choose Image or Label file %s - Choisir un fichier Image ou Étiquette %s - Save/Load Annotations in Directory %s - Enregistrer/Charger les annotations dans le répertoire %s . Annotations will be saved/loaded in %s %s . Les annotations seront enregistrées/chargées dans %s %s - Choose File %s - Choisir un fichier Label files (*%s) Fichiers d'étiquettes (*%s) Choose File Choisir un fichier You are about to permanently delete this label file, proceed anyway? Vous êtes sur le point de supprimer définitivement ce fichier d'étiquettes, continuer quand même ? Attention Attention Save annotations to "{}" before closing? Enregistrer les annotations dans "{}" avant de fermer ? Save annotations? Enregistrer les annotations ? You are about to permanently delete {} shapes, proceed anyway? Vous êtes sur le point de supprimer définitivement {} formes, continuer quand même ? %s - Open Directory %s - Ouvrir le répertoire Toggle "keep previous annotation" mode Activer/désactiver le mode "conserver l'annotation précédente" Keep Previous Brightness/Contrast Conserver les réglages de luminosité/contraste Preferences… Préférences… Open config file in text editor Ouvrir le fichier de configuration dans l'éditeur de texte No Config File Aucun fichier de configuration Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc La configuration a été fournie sous forme d'expression YAML via la ligne de commande. Pour utiliser l'éditeur de préférences, démarrez Labelme avec un fichier de configuration : labelme --config ~/.labelmerc Configuration Errors Erreurs de Configuration Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Des erreurs ont été trouvées lors du chargement de la configuration. Veuillez examiner les erreurs ci-dessous et recharger votre configuration ou ignorer les lignes erronées. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Assurez-vous que <i>%s</i> est un fichier d'étiquettes valide.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Assurez-vous que <i>%s</i> est un fichier image valide.</p> Reset Layout Réinitialiser la disposition ================================================ FILE: labelme/translate/hu_HU.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation MI-támogatott annotáció AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes Az MI annotációt javasol az 'AI-Polygon' és 'AI-Mask' módokban Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Válassza az 'AI-Polygon' vagy 'AI-Mask' módot az AI-Assisted Annotation engedélyezéséhez AiTextToAnnotationWidget AI Text-to-Annotation AI prompt e.g., dog,cat,bird pl. kutya,macska,madár Run Futtatás Score Pontszám IoU IoU AI creates annotations from the text prompt Az AI annotációkat hoz létre a szöveges promptból Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Válassza a 'Polygon', 'Rectangle', 'AI-Polygon' vagy 'AI-Mask' módot az engedélyezéshez Canvas Creating %r %r létrehozása ESC to cancel ESC a megszakításhoz Enter or Space to finalize Enter vagy Szóköz a befejezéshez Editing shapes Alakzatok szerkesztése Click points to include or Shift+Click to exclude for ai_polygon Kattintson a pontokra a belefoglaláshoz vagy Shift+Kattintás a kizáráshoz (AI sokszög) Click points to include or Shift+Click to exclude for ai_mask Kattintson a pontokra a belefoglaláshoz vagy Shift+Kattintás a kizáráshoz (AI maszk) Click start point for line Kattintson a vonal kezdőpontjára Click end point for line Kattintson a vonal végpontjára Click start point for linestrip Kattintson a vonallánc kezdőpontjára Click next point or finish by Ctrl/Cmd+Click for linestrip Kattintson a következő pontra vagy Ctrl/Cmd+Kattintás a befejezéshez (vonallánc) Click center point for circle Kattintson a kör középpontjára Click point on circumference for circle Kattintson a kör kerületén lévő pontra Click first corner for rectangle Kattintson a téglalap első sarkára Click to add point Kattintson pont hozzáadásához Click & drag to move point Kattintson és húzza a pont mozgatásához ALT + SHIFT + Click to delete point ALT + SHIFT + Kattintás a pont törléséhez ALT + Click to create point on shape ALT + Kattintás pont létrehozásához az alakzaton Click & drag to move shape Kattintson és húzza az alakzat mozgatásához Right-click & drag to copy shape Jobb gombbal kattintás és húzás az alakzat másolásához Click opposite corner for rectangle (Shift for square) Kattintson a téglalap ellentétes sarkára (Shift a négyzethez) MainWindow Flags Jelzők Annotation List Annotációlista Select label to start annotating for it. Press 'Esc' to deselect. Válasszon címkét az annotálás megkezdéséhez. Nyomja meg az 'Esc' gombot a kijelölés megszüntetéséhez. Label List Címkelista Search Filename Fájlnév keresése File List Fájllista &Quit &Kilépés Quit application Alkalmazás bezárása &Open &Megnyitás Open image or label file Kép vagy címke fájl megnyitása Open Dir Könyvtár megnyitása &Next Image &Következő kép Open next (hold Ctl+Shift to copy labels) Következő megnyitása (tartsa lenyomva a Ctrl+Shift-et a címkék másolásához) &Prev Image &Előző kép Open prev (hold Ctl+Shift to copy labels) Előző megnyitása (tartsa lenyomva a Ctrl+Shift-et a címkék másolásához) &Save &Mentés Save labels to file Címkék mentése fájlba &Save As Mentés &másként Save labels to a different file Címkék mentése másik fájlba &Delete File &Fájl törlése Delete current label file Jelenlegi címke fájl törlése &Change Output Dir &Kimeneti könyvtár módosítása Change where annotations are loaded/saved Az annotációk betöltési/mentési helyének módosítása Save &Automatically &Automatikus mentés Save automatically Automatikus mentés Save With Image Data Mentés kép adatokkal Save image data in label file Kép adatok mentése a címke fájlba &Close &Bezárás Close current file Jelenlegi fájl bezárása Keep Previous Annotation Előző annotáció megtartása Toggle "keep previous annotation" mode "Előző annotáció megtartása" mód váltása Create Polygons Sokszögek létrehozása Start drawing polygons Sokszögek rajzolásának megkezdése Create Rectangle Téglalap létrehozása Start drawing rectangles Téglalapok rajzolásának megkezdése Create Circle Kör létrehozása Start drawing circles Körök rajzolásának megkezdése Create Line Vonal létrehozása Start drawing lines Vonalak rajzolásának megkezdése Create Point Pont létrehozása Start drawing points Pontok rajzolásának megkezdése Create LineStrip Vonallánc létrehozása Start drawing linestrip. Ctrl+LeftClick ends creation. Vonallánc rajzolásának megkezdése. Ctrl+Bal klikk befejezi a létrehozást. Create AI-Polygon AI sokszög létrehozása Start drawing ai_polygon. Ctrl+LeftClick ends creation. AI sokszög rajzolásának megkezdése. Ctrl+Bal klikk befejezi a létrehozást. Create AI-Mask AI maszk létrehozása Start drawing ai_mask. Ctrl+LeftClick ends creation. AI maszk rajzolásának megkezdése. Ctrl+Bal klikk befejezi a létrehozást. Edit Shapes Alakzatok szerkesztése Move and edit the selected shapes A kijelölt alakzatok mozgatása és szerkesztése Delete Shapes Alakzatok törlése Delete the selected shapes A kijelölt alakzatok törlése Duplicate Shapes Alakzatok duplikálása Create a duplicate of the selected shapes A kijelölt alakzatok másolatának létrehozása Copy Shapes Alakzatok másolása Copy selected shapes to clipboard Kijelölt alakzatok másolása a vágólapra Paste Shapes Alakzatok beillesztése Paste copied shapes Másolt alakzatok beillesztése Undo last point Utolsó pont visszavonása Undo last drawn point Utolsó rajzolt pont visszavonása Remove Selected Point Kijelölt pont eltávolítása Remove selected point from polygon Kijelölt pont eltávolítása a sokszögből Undo Visszavonás Undo last add and edit of shape Alakzat utolsó hozzáadásának és szerkesztésének visszavonása &Hide Shapes Alakzatok &elrejtése Hide all shapes Összes alakzat elrejtése &Show Shapes Alakzatok &megjelenítése Show all shapes Összes alakzat megjelenítése &Toggle Shapes Alakzatok &váltása Toggle all shapes Összes alakzat váltása &Tutorial &Oktatóanyag Show tutorial page Oktatóanyag oldal megjelenítése Zoom Nagyítás Zoom in or out of the image. Also accessible with {} and {} from the canvas. Kép nagyítása vagy kicsinyítése. A vászonról {} és {} billentyűkkel is elérhető. Ctrl+Wheel Ctrl+Görgő Zoom &In &Nagyítás Increase zoom level Nagyítási szint növelése &Zoom Out &Kicsinyítés Decrease zoom level Nagyítási szint csökkentése &Original size &Eredeti méret Zoom to original size Nagyítás eredeti méretre &Keep Previous Scale &Előző méretarány megtartása Keep previous zoom scale Előző nagyítási méretarány megtartása &Fit Window &Ablakhoz igazítás Zoom follows window size A nagyítás követi az ablak méretét Fit &Width &Szélességhez igazítás Zoom follows window width A nagyítás követi az ablak szélességét &Brightness Contrast &Fényerő/Kontraszt Adjust brightness and contrast Fényerő és kontraszt beállítása &Edit Label &Címke szerkesztése Modify the label of the selected shape A kijelölt alakzat címkéjének módosítása Fill Drawing Polygon Rajzolt sokszög kitöltése Fill polygon while drawing Sokszög kitöltése rajzolás közben Keep Previous Brightness/Contrast Előző fényerő/kontraszt megtartása &File &Fájl &Edit &Szerkesztés &View &Nézet &Help &Súgó Open &Recent &Legutóbbi megnyitása %s started. %s elindítva. Invalid label Érvénytelen címke Invalid label '{}' with validation type '{}' Érvénytelen címke '{}' '{}' validációs típussal Error saving label data Hiba a címke adatok mentésekor <b>%s</b> <b>%s</b> Error opening file Hiba a fájl megnyitásakor No such file: <b>%s</b> Nincs ilyen fájl: <b>%s</b> Loading %s... %s betöltése... Error reading %s Hiba a %s olvasásakor <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Győződjön meg arról, hogy <i>{0}</i> érvényes kép fájl.<br/>Támogatott kép formátumok: {1}</p> Loaded %s %s betöltve Image & Label files (%s) Kép és címke fájlok (%s) %s - Choose Image or Label file %s - Válasszon képet vagy címke fájlt %s - Save/Load Annotations in Directory %s - Annotációk mentése/betöltése könyvtárból %s . Annotations will be saved/loaded in %s %s . Az annotációk a %s könyvtárban lesznek mentve/betöltve %s - Choose File %s - Fájl kiválasztása Label files (*%s) Címke fájlok (*%s) Choose File Fájl kiválasztása You are about to permanently delete this label file, proceed anyway? Biztosan véglegesen törli ezt a címke fájlt? Attention Figyelem Save annotations to "{}" before closing? Mentse az annotációkat a "{}" fájlba bezárás előtt? Save annotations? Mentse az annotációkat? You are about to permanently delete {} shapes, proceed anyway? Biztosan véglegesen törli a {} alakzatot? %s - Open Directory %s - Könyvtár megnyitása Preferences… Beállítások… Open config file in text editor Konfigurációs fájl megnyitása szövegszerkesztőben No Config File Nincs konfigurációs fájl Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc A konfiguráció YAML kifejezésként lett megadva a parancssorból. A beállítások szerkesztő használatához indítsa el a Labelme-t konfigurációs fájllal: labelme --config ~/.labelmerc Configuration Errors Konfigurációs Hibák Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Hibák találhatók a konfiguráció betöltése közben. Kérjük, tekintse át az alábbi hibákat, és töltse újra a konfigurációt, vagy hagyja figyelmen kívül a hibás sorokat. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Győződjön meg arról, hogy <i>%s</i> érvényes címke fájl.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Győződjön meg arról, hogy <i>%s</i> érvényes képfájl.</p> Reset Layout Elrendezés visszaállítása ================================================ FILE: labelme/translate/it_IT.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation Annotazione assistita da IA AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes L'IA suggerisce annotazioni nelle modalità 'AI-Polygon' e 'AI-Mask' Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Seleziona la modalità 'AI-Polygon' o 'AI-Mask' per abilitare l'Annotazione assistita da IA AiTextToAnnotationWidget AI Text-to-Annotation Prompt IA e.g., dog,cat,bird es. : cane,gatto,uccello Run Esegui Score Punteggio IoU IoU AI creates annotations from the text prompt L'IA crea annotazioni dal testo Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Seleziona la modalità 'Polygon', 'Rectangle', 'AI-Polygon' o 'AI-Mask' per abilitare Canvas Click & drag to move point Clicca e trascina per spostare il punto Click & drag to move shape Clicca e trascina per spostare la forma Creating %r Creazione di %r ESC to cancel ESC per annullare Enter or Space to finalize Invio o Spazio per finalizzare Editing shapes Modifica delle forme Click points to include or Shift+Click to exclude for ai_polygon Clicca sui punti da includere o Maiusc+Clicca per escludere (poligono IA) Click points to include or Shift+Click to exclude for ai_mask Clicca sui punti da includere o Maiusc+Clicca per escludere (maschera IA) Click start point for line Clicca sul punto di partenza della linea Click end point for line Clicca sul punto finale della linea Click start point for linestrip Clicca sul punto di partenza della polilinea Click next point or finish by Ctrl/Cmd+Click for linestrip Clicca sul punto successivo o Ctrl/Cmd+Clicca per terminare la polilinea Click center point for circle Clicca sul punto centrale del cerchio Click point on circumference for circle Clicca su un punto della circonferenza del cerchio Click first corner for rectangle Clicca sul primo angolo del rettangolo Click to add point Clicca per aggiungere un punto ALT + SHIFT + Click to delete point ALT + MAIUSC + Clicca per eliminare il punto ALT + Click to create point on shape ALT + Clicca per creare un punto sulla forma Right-click & drag to copy shape Tasto destro e trascina per copiare la forma Click opposite corner for rectangle (Shift for square) Clicca sull'angolo opposto del rettangolo (Shift per quadrato) MainWindow Flags Flag Annotation List Lista annotazioni Select label to start annotating for it. Press 'Esc' to deselect. Seleziona un'etichetta per iniziare ad annotare. Premi 'Esc' per deselezionare. Label List Lista etichette Search Filename Cerca nome file File List Lista file &Quit &Esci Quit application Esci dall'applicazione &Open &Apri Open image or label file Apri un'immagine o un file di etichette Open Dir Apri directory &Next Image Immagine &successiva Open next (hold Ctl+Shift to copy labels) Apri la successiva (tieni premuto Ctrl+Maiusc per copiare le etichette) &Prev Image Immagine &precedente Open prev (hold Ctl+Shift to copy labels) Apri la precedente (tieni premuto Ctrl+Maiusc per copiare le etichette) &Save &Salva Save labels to file Salva le etichette in un file &Save As Salva &come Save labels to a different file Salva le etichette in un file diverso &Delete File &Elimina file Delete current label file Elimina il file di etichette corrente &Change Output Dir &Cambia directory di output Change where annotations are loaded/saved Cambia dove vengono caricate/salvate le annotazioni Save &Automatically Salva &automaticamente Save automatically Salva automaticamente Save With Image Data Salva con dati immagine Save image data in label file Salva i dati dell'immagine nel file di etichette &Close &Chiudi Close current file Chiudi il file corrente Keep Previous Annotation Mantieni annotazione precedente Create Polygons Crea poligoni Start drawing polygons Inizia a disegnare poligoni Create Rectangle Crea rettangolo Start drawing rectangles Inizia a disegnare rettangoli Create Circle Crea cerchio Start drawing circles Inizia a disegnare cerchi Create Line Crea linea Start drawing lines Inizia a disegnare linee Create Point Crea punto Start drawing points Inizia a disegnare punti Create LineStrip Crea polilinea Start drawing linestrip. Ctrl+LeftClick ends creation. Inizia a disegnare una polilinea. Ctrl+Clic sinistro termina la creazione. Create AI-Polygon Crea poligono IA Start drawing ai_polygon. Ctrl+LeftClick ends creation. Inizia a disegnare un poligono IA. Ctrl+Clic sinistro termina la creazione. Create AI-Mask Crea maschera IA Start drawing ai_mask. Ctrl+LeftClick ends creation. Inizia a disegnare una maschera IA. Ctrl+Clic sinistro termina la creazione. Edit Shapes Modifica forme Move and edit the selected shapes Sposta e modifica le forme selezionate Delete Shapes Elimina forme Delete the selected shapes Elimina le forme selezionate Duplicate Shapes Duplica forme Create a duplicate of the selected shapes Crea un duplicato dele forme selezionate Copy Shapes Copia forme Copy selected shapes to clipboard Copia le forme selezionate negli appunti Paste Shapes Incolla forme Paste copied shapes Incolla le forme copiate Undo last point Annulla ultimo punto Undo last drawn point Annulla l'ultimo punto disegnato Remove Selected Point Rimuovi punto selezionato Remove selected point from polygon Rimuovi il punto selezionato dal poligono Undo Annulla Undo last add and edit of shape Annulla l'ultimo aggiunto e la modifica della forma &Hide Shapes &Nascondi forme Hide all shapes Nascondi tutte le forme &Show Shapes &Mostra forme Show all shapes Mostra tutte le forme &Toggle Shapes &Commuta forme Toggle all shapes Commuta tutte le forme &Tutorial &Tutorial Show tutorial page Mostra la pagina del tutorial Zoom Zoom Zoom in or out of the image. Also accessible with {} and {} from the canvas. Ingrandisci o riduci l'immagine. Accessibile anche con {} e {} dalla tela. Ctrl+Wheel Ctrl+Rotella Zoom &In Zoom &avanti Increase zoom level Aumenta il livello di zoom &Zoom Out Zoom &indietro Decrease zoom level Diminuisci il livello di zoom &Original size Dimensione &originale Zoom to original size Zoom alla dimensione originale &Keep Previous Scale &Mantieni scala precedente Keep previous zoom scale Mantieni la scala di zoom precedente &Fit Window &Adatta alla finestra Zoom follows window size Lo zoom segue la dimensione della finestra Fit &Width Adatta &larghezza Zoom follows window width Lo zoom segue la larghezza della finestra &Brightness Contrast &Luminosità e contrasto Adjust brightness and contrast Regola luminosità e contrasto &Edit Label &Modifica etichetta Modify the label of the selected shape Modifica l'etichetta della forma selezionata Fill Drawing Polygon Riempire poligono in disegno Fill polygon while drawing Riempire il poligono durante il disegno &File &File &Edit &Modifica &View &Visualizza &Help &Aiuto Open &Recent Apri &recenti %s started. %s avviato. Invalid label Etichetta non valida Invalid label '{}' with validation type '{}' Etichetta non valida '{}' con tipo di validazione '{}' Error saving label data Errore nel salvataggio dei dati delle etichette <b>%s</b> <b>%s</b> Error opening file Errore nell'apertura del file No such file: <b>%s</b> File non trovato : <b>%s</b> Loading %s... Caricamento di %s... Error reading %s Errore nella lettura di %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Assicurati che <i>{0}</i> sia un file immagine valido.<br/>Formati immagine supportati : {1}</p> Loaded %s %s caricato Image & Label files (%s) File Immagine & Etichette (%s) %s - Choose Image or Label file %s - Scegli un file Immagine o Etichetta %s - Save/Load Annotations in Directory %s - Salva/Carica annotazioni nella directory %s . Annotations will be saved/loaded in %s %s . Le annotazioni saranno salvate/caricate in %s %s - Choose File %s - Scegli un file Label files (*%s) File di etichette (*%s) Choose File Scegli un file You are about to permanently delete this label file, proceed anyway? Stai per eliminare definitivamente questo file di etichette, procedere comunque? Attention Attenzione Save annotations to "{}" before closing? Salvare le annotazioni in "{}" prima di chiudere? Save annotations? Salvare le annotazioni? You are about to permanently delete {} shapes, proceed anyway? Stai per eliminare definitivamente {} forme, procedere comunque? %s - Open Directory %s - Apri directory Toggle "keep previous annotation" mode Attiva/disattiva la modalità "mantieni annotazione precedente" Keep Previous Brightness/Contrast Mantieni luminosità/contrasto precedenti Preferences… Preferenze… Open config file in text editor Apri file di configurazione nell'editor di testo No Config File Nessun file di configurazione Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc La configurazione è stata fornita come espressione YAML tramite riga di comando. Per utilizzare l'editor delle preferenze, avvia Labelme con un file di configurazione: labelme --config ~/.labelmerc Configuration Errors Errori di Configurazione Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Sono stati trovati errori durante il caricamento della configurazione. Si prega di esaminare gli errori sottostanti e ricaricare la configurazione o ignorare le righe errate. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Assicurati che <i>%s</i> sia un file di etichette valido.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Assicurati che <i>%s</i> sia un file di immagine valido.</p> Reset Layout Ripristina layout ================================================ FILE: labelme/translate/ja_JP.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation AI支援アノテーション AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AIが'AI-Polygon'と'AI-Mask'モードでアノテーションを提案します Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation 「AI-Polygon」または「AI-Mask」モードを選択してAI支援アノテーションを有効化 AiTextToAnnotationWidget AI Text-to-Annotation AIプロンプト e.g., dog,cat,bird 例: dog,cat,bird Run 実行 Score スコア IoU IoU AI creates annotations from the text prompt AIがテキストプロンプトからアノテーションを作成 Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable 「Polygon」「Rectangle」「AI-Polygon」「AI-Mask」モードで有効 Canvas Click & drag to move point クリック & ドラッグで頂点を移動 Click & drag to move shape クリック & ドラッグで図形を移動 Creating %r %r を作成中 ESC to cancel ESC でキャンセル Enter or Space to finalize Enter または Space で確定 Editing shapes 図形を編集中 Click points to include or Shift+Click to exclude for ai_polygon クリックで含める点を、Shift+クリックで除外する点を指定 (AIポリゴン) Click points to include or Shift+Click to exclude for ai_mask クリックで含める点を、Shift+クリックで除外する点を指定 (AIマスク) Click start point for line 直線の始点をクリック Click end point for line 直線の終点をクリック Click start point for linestrip 折れ線の始点をクリック Click next point or finish by Ctrl/Cmd+Click for linestrip 次の点をクリック、または Ctrl/Cmd+クリックで完了 Click center point for circle 円の中心点をクリック Click point on circumference for circle 円周上の点をクリック Click first corner for rectangle 矩形の最初の角をクリック Click to add point クリックで頂点を追加 ALT + SHIFT + Click to delete point ALT + SHIFT + クリックで頂点を削除 ALT + Click to create point on shape ALT + クリックで図形上に頂点を作成 Right-click & drag to copy shape 右クリック & ドラッグで図形をコピー Click opposite corner for rectangle (Shift for square) 矩形の対角をクリック(Shiftで正方形) MainWindow Flags フラグ Annotation List アノテーション一覧 Select label to start annotating for it. Press 'Esc' to deselect. ラベルを選択してアノテーションを開始。'Esc' で選択解除。 Label List ラベル一覧 Search Filename ファイル名で検索 File List ファイル一覧 &Quit 終了(&Q) Quit application アプリケーションを終了 &Open 開く(&O) Open image or label file 画像またはラベルファイルを開く Open Dir ディレクトリを 開く &Next Image 次の 画像(&N) Open next (hold Ctl+Shift to copy labels) 次を開く (Ctrl+Shift でラベルをコピー) &Prev Image 前の 画像(&P) Open prev (hold Ctl+Shift to copy labels) 前を開く (Ctrl+Shift でラベルをコピー) &Save 保存(&S) Save labels to file ラベルをファイルに保存 &Save As 名前を付けて保存(&A) Save labels to a different file ラベルを別のファイルに保存 &Delete File ファイルを 削除(&D) Delete current label file 現在のラベルファイルを削除 &Change Output Dir 出力先を変更(&C) Change where annotations are loaded/saved アノテーションの読み込み/保存先を変更 Save &Automatically 自動保存(&A) Save automatically 自動で保存 Save With Image Data 画像データと共に保存 Save image data in label file ラベルファイルに画像データを含める &Close 閉じる(&C) Close current file 現在のファイルを閉じる Keep Previous Annotation 前のアノテーションを保持 Create Polygons ポリゴン を作成 Start drawing polygons ポリゴンの描画を開始 Create Rectangle 矩形 を作成 Start drawing rectangles 矩形の描画を開始 Create Circle 円 を作成 Start drawing circles 円の描画を開始 Create Line 直線 を作成 Start drawing lines 直線の描画を開始 Create Point 点 を作成 Start drawing points 点の描画を開始 Create LineStrip 折れ線 を作成 Start drawing linestrip. Ctrl+LeftClick ends creation. 折れ線の描画を開始。Ctrl+左クリックで完了。 Create AI-Polygon AIポリゴン を作成 Start drawing ai_polygon. Ctrl+LeftClick ends creation. AIポリゴンの描画を開始。Ctrl+左クリックで完了。 Create AI-Mask AIマスク を作成 Start drawing ai_mask. Ctrl+LeftClick ends creation. AIマスクの描画を開始。Ctrl+左クリックで完了。 Edit Shapes 図形を 編集 Move and edit the selected shapes 選択した図形を移動・編集 Delete Shapes 図形を 削除 Delete the selected shapes 選択した図形を削除 Duplicate Shapes 図形を 複製 Create a duplicate of the selected shapes 選択した図形の複製を作成 Copy Shapes 図形をコピー Copy selected shapes to clipboard 選択した図形をクリップボードにコピー Paste Shapes 図形を貼り付け Paste copied shapes コピーした図形を貼り付け Undo last point 最後の頂点を取り消し Undo last drawn point 最後に描画した頂点を取り消し Remove Selected Point 選択した頂点を削除 Remove selected point from polygon ポリゴンから選択した頂点を削除 Undo 元に戻す Undo last add and edit of shape 最後の図形追加・編集を元に戻す &Hide Shapes 図形を 非表示(&H) Hide all shapes すべての図形を非表示 &Show Shapes 図形を 表示(&S) Show all shapes すべての図形を表示 &Toggle Shapes 図形を 切り替え(&T) Toggle all shapes すべての図形の表示を切り替え &Tutorial チュートリアル(&T) Show tutorial page チュートリアルページを表示 Zoom ズーム Zoom in or out of the image. Also accessible with {} and {} from the canvas. 画像を拡大・縮小します。キャンバス上で {} と {} も使用できます。 Ctrl+Wheel Ctrl+ホイール Zoom &In 拡大(&I) Increase zoom level 拡大率を上げる &Zoom Out 縮小(&Z) Decrease zoom level 拡大率を下げる &Original size 原寸大(&O) Zoom to original size 原寸大で表示 &Keep Previous Scale 前の倍率を保持(&K) Keep previous zoom scale 前のズーム倍率を保持 &Fit Window ウィンドウに 合わせる(&F) Zoom follows window size ウィンドウサイズに合わせてズーム Fit &Width 幅に合わせる(&W) Zoom follows window width ウィンドウ幅に合わせてズーム &Brightness Contrast 明るさ・ コントラスト(&B) Adjust brightness and contrast 明るさとコントラストを調整 &Edit Label ラベルを編集(&E) Modify the label of the selected shape 選択した図形のラベルを変更 Fill Drawing Polygon 描画中のポリゴンを塗りつぶす Fill polygon while drawing 描画中にポリゴンを塗りつぶす &File ファイル(&F) &Edit 編集(&E) &View 表示(&V) &Help ヘルプ(&H) Open &Recent 最近使用したファイル(&R) %s started. %s を起動しました Invalid label 無効なラベル Invalid label '{}' with validation type '{}' ラベル '{}' は検証タイプ '{}' では無効です Error saving label data ラベルデータの保存エラー <b>%s</b> <b>%s</b> Error opening file ファイルを開けませんでした No such file: <b>%s</b> ファイルが見つかりません: <b>%s</b> Loading %s... %s を読み込み中... Error reading %s %s の読み込みエラー <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p><i>{0}</i> が有効な画像ファイルであることを確認してください。<br/>対応形式: {1}</p> Loaded %s %s を読み込みました Image & Label files (%s) 画像とラベルファイル (%s) %s - Choose Image or Label file %s - 画像またはラベルファイルを選択 %s - Save/Load Annotations in Directory %s - アノテーションの保存/読み込みディレクトリ %s . Annotations will be saved/loaded in %s %s - アノテーションは %s に保存/読み込みされます %s - Choose File %s - ファイルを選択 Label files (*%s) ラベルファイル (*%s) Choose File ファイルを選択 You are about to permanently delete this label file, proceed anyway? このラベルファイルを完全に削除します。続行しますか? Attention 注意 Save annotations to "{}" before closing? 閉じる前にアノテーションを "{}" に保存しますか? Save annotations? アノテーションを保存しますか? You are about to permanently delete {} shapes, proceed anyway? {} 個の図形を完全に削除します。続行しますか? %s - Open Directory %s - ディレクトリを開く Toggle "keep previous annotation" mode 「前のアノテーションを保持」モードを切り替え Keep Previous Brightness/Contrast 前の明るさ/コントラストを保持 Preferences… 設定… Open config file in text editor テキストエディタで設定ファイルを開く No Config File 設定ファイルがありません Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc 設定はコマンドラインからYAML式として提供されました。 設定エディタを使用するには、設定ファイルを指定してLabelmeを起動してください: labelme --config ~/.labelmerc Configuration Errors 設定エラー Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. 設定の読み込み中にエラーが見つかりました。以下のエラーを確認し、設定を再読み込みするか、エラーのある行を無視してください。 <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p><i>%s</i> が有効なラベルファイルであることを確認してください。</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p><i>%s</i> が有効な画像ファイルであることを確認してください。</p> Reset Layout レイアウトをリセット ================================================ FILE: labelme/translate/ko_KR.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation AI 지원 주석 AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI가 'AI-Polygon' 및 'AI-Mask' 모드에서 주석을 제안합니다 Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation AI 지원 주석을 활성화하려면 'AI-Polygon' 또는 'AI-Mask' 모드를 선택하세요 AiTextToAnnotationWidget AI Text-to-Annotation AI 프롬프트 e.g., dog,cat,bird 예: 개, 고양이, 새 Run 실행 Score 점수 IoU IoU AI creates annotations from the text prompt AI가 텍스트 프롬프트에서 주석을 생성합니다 Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable 활성화하려면 'Polygon', 'Rectangle', 'AI-Polygon' 또는 'AI-Mask' 모드를 선택하세요 Canvas Click & drag to move point 클릭하고 드래그하여 점 이동 Click & drag to move shape 클릭하고 드래그하여 도형 이동 Creating %r %r 생성 중 ESC to cancel ESC 키를 눌러 취소 Enter or Space to finalize Enter 또는 Space로 완료 Editing shapes 도형 편집 중 Click points to include or Shift+Click to exclude for ai_polygon 점을 클릭하여 포함하거나 Shift+클릭하여 제외 (AI 다각형) Click points to include or Shift+Click to exclude for ai_mask 점을 클릭하여 포함하거나 Shift+클릭하여 제외 (AI 마스크) Click start point for line 선의 시작점 클릭 Click end point for line 선의 끝점 클릭 Click start point for linestrip 연속선의 시작점 클릭 Click next point or finish by Ctrl/Cmd+Click for linestrip 다음 점을 클릭하거나 Ctrl/Cmd+클릭으로 완료 (연속선) Click center point for circle 원의 중심점 클릭 Click point on circumference for circle 원의 둘레 위 점 클릭 Click first corner for rectangle 사각형의 첫 번째 모서리 클릭 Click to add point 점 추가를 위해 클릭 ALT + SHIFT + Click to delete point ALT + SHIFT + 클릭으로 점 삭제 ALT + Click to create point on shape ALT + 클릭으로 도형 위에 점 생성 Right-click & drag to copy shape 우클릭하고 드래그하여 도형 복사 Click opposite corner for rectangle (Shift for square) 사각형의 대각 모서리 클릭 (Shift로 정사각형) MainWindow Flags 플래그 Annotation List 주석 목록 Select label to start annotating for it. Press 'Esc' to deselect. 주석을 시작할 레이블을 선택하세요. 'Esc'를 눌러 선택 해제합니다. Label List 레이블 목록 Search Filename 파일명 검색 File List 파일 목록 &Quit 종료(&Q) Quit application 애플리케이션 종료 &Open 열기(&O) Open image or label file 이미지 또는 레이블 파일 열기 Open Dir 폴더 열기 &Next Image 다음 이미지(&N) Open next (hold Ctl+Shift to copy labels) 다음 열기 (Ctrl+Shift를 누르면 레이블 복사) &Prev Image 이전 이미지(&P) Open prev (hold Ctl+Shift to copy labels) 이전 열기 (Ctrl+Shift를 누르면 레이블 복사) &Save 저장(&S) Save labels to file 레이블을 파일로 저장 &Save As 다른 이름으로 저장(&S) Save labels to a different file 레이블을 다른 파일로 저장 &Delete File 삭제(&D) Delete current label file 현재 레이블 파일 삭제 &Change Output Dir 출력 폴더 변경(&C) Change where annotations are loaded/saved 주석을 로드/저장할 위치 변경 Save &Automatically 자동 저장(&A) Save automatically 자동 저장 Save With Image Data 이미지 데이터와 함께 저장 Save image data in label file 레이블 파일에 이미지 데이터 저장 &Close 닫기(&C) Close current file 현재 파일 닫기 Keep Previous Annotation 이전 주석 유지 Create Polygons 다각형 생성 Start drawing polygons 다각형 그리기 시작 Create Rectangle 사각형 생성 Start drawing rectangles 사각형 그리기 시작 Create Circle 원 생성 Start drawing circles 원 그리기 시작 Create Line 선 생성 Start drawing lines 선 그리기 시작 Create Point 점 생성 Start drawing points 점 그리기 시작 Create LineStrip 연속선 생성 Start drawing linestrip. Ctrl+LeftClick ends creation. 연속선 그리기 시작. Ctrl+좌클릭으로 완료. Create AI-Polygon AI 다각형 생성 Start drawing ai_polygon. Ctrl+LeftClick ends creation. AI 다각형 그리기 시작. Ctrl+좌클릭으로 완료. Create AI-Mask AI 마스크 생성 Start drawing ai_mask. Ctrl+LeftClick ends creation. AI 마스크 그리기 시작. Ctrl+좌클릭으로 완료. Edit Shapes 도형 편집 Move and edit the selected shapes 선택한 도형 이동 및 편집 Delete Shapes 도형 삭제 Delete the selected shapes 선택한 도형 삭제 Duplicate Shapes 도형 복제 Create a duplicate of the selected shapes 선택한 도형의 복사본 생성 Copy Shapes 도형 복사 Copy selected shapes to clipboard 선택한 도형을 클립보드에 복사 Paste Shapes 도형 붙여넣기 Paste copied shapes 복사한 도형 붙여넣기 Undo last point 마지막 점 실행 취소 Undo last drawn point 마지막으로 그린 점 실행 취소 Remove Selected Point 선택한 점 제거 Remove selected point from polygon 다각형에서 선택한 점 제거 Undo 실행 취소 Undo last add and edit of shape 도형의 마지막 추가 및 편집 실행 취소 &Hide Shapes 도형 숨기기(&H) Hide all shapes 모든 도형 숨기기 &Show Shapes 도형 표시(&S) Show all shapes 모든 도형 표시 &Toggle Shapes 도형 토글(&S) Toggle all shapes 모든 도형 토글 &Tutorial 튜토리얼(&T) Show tutorial page 튜토리얼 페이지 표시 Zoom 확대/축소 Zoom in or out of the image. Also accessible with {} and {} from the canvas. 이미지 확대 또는 축소. 캔버스에서 {} 및 {}로도 접근 가능합니다. Ctrl+Wheel Ctrl+휠 Zoom &In 확대(&I) Increase zoom level 확대 수준 증가 &Zoom Out 축소(&Z) Decrease zoom level 확대 수준 감소 &Original size 원본 크기(&O) Zoom to original size 원본 크기로 확대/축소 &Keep Previous Scale 이전 배율 유지(&K) Keep previous zoom scale 이전 확대/축소 배율 유지 &Fit Window 창에 맞추기(&F) Zoom follows window size 확대/축소가 창 크기에 맞춤 Fit &Width 너비에 맞추기(&W) Zoom follows window width 확대/축소가 창 너비에 맞춤 &Brightness Contrast 밝기 대비(&B) Adjust brightness and contrast 밝기 및 대비 조정 &Edit Label 레이블 편집(&E) Modify the label of the selected shape 선택한 도형의 레이블 수정 Fill Drawing Polygon 그리기 다각형 채우기 Fill polygon while drawing 그리는 동안 다각형 채우기 &File 파일(&F) &Edit 편집(&E) &View 보기(&V) &Help 도움말(&H) Open &Recent 최근 열기(&R) %s started. %s가 시작되었습니다. Invalid label 잘못된 레이블 Invalid label '{}' with validation type '{}' 검증 유형 '{}'에 대한 잘못된 레이블 '{}' Error saving label data 레이블 데이터 저장 오류 <b>%s</b> <b>%s</b> Error opening file 파일 열기 오류 No such file: <b>%s</b> 파일이 없습니다: <b>%s</b> Loading %s... %s 로드 중... Error reading %s %s 읽기 오류 <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p><i>{0}</i>가 유효한 이미지 파일인지 확인하세요.<br/>지원되는 이미지 형식: {1}</p> Loaded %s %s 로드됨 Image & Label files (%s) 이미지 및 레이블 파일 (%s) %s - Choose Image or Label file %s - 이미지 또는 레이블 파일 선택 %s - Save/Load Annotations in Directory %s - 폴더에 주석 저장/로드 %s . Annotations will be saved/loaded in %s %s . 주석이 %s에 저장/로드됩니다 %s - Choose File %s - 파일 선택 Label files (*%s) 레이블 파일 (*%s) Choose File 파일 선택 You are about to permanently delete this label file, proceed anyway? 이 레이블 파일을 영구적으로 삭제하시겠습니까? Attention 주의 Save annotations to "{}" before closing? 닫기 전에 주석을 "{}"에 저장하시겠습니까? Save annotations? 주석을 저장하시겠습니까? You are about to permanently delete {} shapes, proceed anyway? {}개의 도형을 영구적으로 삭제하시겠습니까? %s - Open Directory %s - 폴더 열기 Toggle "keep previous annotation" mode "이전 주석 유지" 모드 토글 Keep Previous Brightness/Contrast 이전 밝기/대비 유지 Preferences… 환경설정… Open config file in text editor 텍스트 편집기에서 설정 파일 열기 No Config File 설정 파일 없음 Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc 설정이 명령줄에서 YAML 표현식으로 제공되었습니다. 환경설정 편집기를 사용하려면 설정 파일과 함께 Labelme를 시작하세요: labelme --config ~/.labelmerc Configuration Errors 구성 오류 Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. 구성을 불러오는 중 오류가 발견되었습니다. 아래 오류를 검토하고 구성을 다시 불러오거나 잘못된 줄을 무시하세요. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p><i>%s</i>가 유효한 레이블 파일인지 확인하세요.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p><i>%s</i>가 유효한 이미지 파일인지 확인하세요.</p> Reset Layout 레이아웃 초기화 ================================================ FILE: labelme/translate/nl_NL.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation AI-Ondersteunde Annotatie AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI stelt annotatie voor in 'AI-Polygoon' en 'AI-Masker' modi Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Selecteer 'AI-Polygoon' of 'AI-Masker' modus om AI-Ondersteunde Annotatie in te schakelen AiTextToAnnotationWidget AI Text-to-Annotation AI Tekst-naar-Annotatie AI creates annotations from the text prompt AI maakt annotaties van de tekstprompt e.g., dog,cat,bird bijv. hond, kat, vogel Run Uitvoeren Score Score IoU IoU Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Selecteer 'Polygoon', 'Rechthoek', 'AI-Polygoon' of 'AI-Masker' modus om in te schakelen Canvas Click & drag to move point Klik en sleep om punt te verplaatsen Click & drag to move shape Klik en sleep om vorm te verplaatsen Creating %r Maken van %r ESC to cancel ESC om te annuleren Enter or Space to finalize Enter of Spatie om te voltooien Editing shapes Vormen bewerken Click points to include or Shift+Click to exclude for ai_polygon Klik op punten om op te nemen of Shift+Klik om uit te sluiten (AI-polygoon) Click points to include or Shift+Click to exclude for ai_mask Klik op punten om op te nemen of Shift+Klik om uit te sluiten (AI-masker) Click start point for line Klik op startpunt voor lijn Click end point for line Klik op eindpunt voor lijn Click start point for linestrip Klik op startpunt voor lijnstrook Click next point or finish by Ctrl/Cmd+Click for linestrip Klik op volgend punt of Ctrl/Cmd+Klik om te voltooien (lijnstrook) Click center point for circle Klik op middelpunt voor cirkel Click point on circumference for circle Klik op punt op omtrek voor cirkel Click first corner for rectangle Klik op eerste hoek voor rechthoek Click to add point Klik om punt toe te voegen ALT + SHIFT + Click to delete point ALT + SHIFT + Klik om punt te verwijderen ALT + Click to create point on shape ALT + Klik om punt op vorm te maken Right-click & drag to copy shape Rechtsklik en sleep om vorm te kopiëren Click opposite corner for rectangle (Shift for square) Klik op tegenoverliggende hoek voor rechthoek (Shift voor vierkant) MainWindow Flags Vlaggen Annotation List Annotatielijst Select label to start annotating for it. Press 'Esc' to deselect. Selecteer label om te beginnen met annoteren. Druk op 'Esc' om te deselecteren. Label List Labellijst Search Filename Zoek Bestandsnaam File List Bestandslijst &Quit &Afsluiten Quit application Applicatie afsluiten &Open &Openen Open image or label file Afbeelding of labelbestand openen Open Dir Map Openen &Next Image Volgende &Afbeelding Open next (hold Ctl+Shift to copy labels) Volgende openen (houd Ctrl+Shift ingedrukt om labels te kopiëren) &Prev Image Vorige &Afbeelding Open prev (hold Ctl+Shift to copy labels) Vorige openen (houd Ctrl+Shift ingedrukt om labels te kopiëren) &Save &Opslaan Save labels to file Labels opslaan naar bestand &Save As Opslaan &als Save labels to a different file Labels opslaan naar ander bestand &Delete File Bestand &verwijderen Delete current label file Huidig labelbestand verwijderen &Change Output Dir Uitvoermap &wijzigen Change where annotations are loaded/saved Wijzig waar annotaties worden geladen/opgeslagen Save &Automatically &Automatisch opslaan Save automatically Automatisch opslaan Save With Image Data Opslaan met Afbeeldingsgegevens Save image data in label file Afbeeldingsgegevens opslaan in labelbestand &Close &Sluiten Close current file Huidig bestand sluiten Keep Previous Annotation Vorige Annotatie Behouden Create Polygons Polygonen Maken Start drawing polygons Begin met tekenen van polygonen Create Rectangle Rechthoek Maken Start drawing rectangles Begin met tekenen van rechthoeken Create Circle Cirkel Maken Start drawing circles Begin met tekenen van cirkels Create Line Lijn Maken Start drawing lines Begin met tekenen van lijnen Create Point Punt Maken Start drawing points Begin met tekenen van punten Create LineStrip Lijnstrook Maken Start drawing linestrip. Ctrl+LeftClick ends creation. Begin met tekenen van lijnstrook. Ctrl+Linkerklik beëindigt het maken. Create AI-Polygon AI-Polygoon Maken Start drawing ai_polygon. Ctrl+LeftClick ends creation. Begin met tekenen van AI-polygoon. Ctrl+Linkerklik beëindigt het maken. Create AI-Mask AI-Masker Maken Start drawing ai_mask. Ctrl+LeftClick ends creation. Begin met tekenen van AI-masker. Ctrl+Linkerklik beëindigt het maken. Edit Shapes Vormen Bewerken Move and edit the selected shapes Verplaats en bewerk de geselecteerde vormen Delete Shapes Vormen Verwijderen Delete the selected shapes Geselecteerde vormen verwijderen Duplicate Shapes Vormen Dupliceren Create a duplicate of the selected shapes Maak een duplicaat van de geselecteerde vormen Copy Shapes Vormen Kopiëren Copy selected shapes to clipboard Geselecteerde vormen naar klembord kopiëren Paste Shapes Vormen Plakken Paste copied shapes Gekopieerde vormen plakken Undo last point Laatste punt ongedaan maken Undo last drawn point Laatste getekende punt ongedaan maken Remove Selected Point Geselecteerd Punt Verwijderen Remove selected point from polygon Geselecteerd punt uit polygoon verwijderen Undo Ongedaan Maken Undo last add and edit of shape Laatste toevoeging en bewerking van vorm ongedaan maken &Hide Shapes Vormen &verbergen Hide all shapes Alle vormen verbergen &Show Shapes Vormen &tonen Show all shapes Alle vormen tonen &Toggle Shapes Vormen &omschakelen Toggle all shapes Alle vormen omschakelen &Tutorial &Tutorial Show tutorial page Tutorialpagina tonen Zoom Zoom Zoom in or out of the image. Also accessible with {} and {} from the canvas. In- of uitzoomen op de afbeelding. Ook toegankelijk met {} en {} vanaf het canvas. Ctrl+Wheel Ctrl+Wiel Zoom &In &Inzoomen Increase zoom level Zoomniveau verhogen &Zoom Out &Uitzoomen Decrease zoom level Zoomniveau verlagen &Original size &Originele grootte Zoom to original size Zoomen naar originele grootte &Keep Previous Scale Vorige Schaal &behouden Keep previous zoom scale Vorige zoomschaal behouden &Fit Window Aanpassen aan &venster Zoom follows window size Zoom volgt venstergrootte Fit &Width Aanpassen aan &breedte Zoom follows window width Zoom volgt vensterbreedte &Brightness Contrast Helderheid en &contrast Adjust brightness and contrast Helderheid en contrast aanpassen &Edit Label Label &bewerken Modify the label of the selected shape Label van geselecteerde vorm wijzigen Fill Drawing Polygon Polygoon Vullen bij Tekenen Fill polygon while drawing Polygoon vullen tijdens tekenen &File &Bestand &Edit &Bewerken &View &Beeld &Help &Help Open &Recent Open &recent %s started. %s gestart. Invalid label Ongeldig label Invalid label '{}' with validation type '{}' Ongeldig label '{}' met validatietype '{}' Error saving label data Fout bij opslaan van labelgegevens <b>%s</b> <b>%s</b> Error opening file Fout bij openen van bestand No such file: <b>%s</b> Bestand niet gevonden: <b>%s</b> Loading %s... Laden van %s... Error reading %s Fout bij lezen van %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Zorg ervoor dat <i>{0}</i> een geldig afbeeldingsbestand is.<br/>Ondersteunde afbeeldingsformaten: {1}</p> Loaded %s Geladen %s Image & Label files (%s) Afbeelding- en labelbestanden (%s) %s - Choose Image or Label file %s - Kies Afbeelding- of labelbestand %s - Save/Load Annotations in Directory %s - Annotaties Opslaan/Laden in Map %s . Annotations will be saved/loaded in %s %s . Annotaties worden opgeslagen/geladen in %s %s - Choose File %s - Kies Bestand Label files (*%s) Labelbestanden (*%s) Choose File Kies Bestand You are about to permanently delete this label file, proceed anyway? U staat op het punt dit labelbestand permanent te verwijderen, doorgaan? Attention Let op Save annotations to "{}" before closing? Annotaties opslaan naar "{}" voordat u sluit? Save annotations? Annotaties opslaan? You are about to permanently delete {} shapes, proceed anyway? U staat op het punt {} vormen permanent te verwijderen, doorgaan? %s - Open Directory %s - Map Openen Toggle "keep previous annotation" mode Modus "vorige annotatie behouden" omschakelen Keep Previous Brightness/Contrast Vorige Helderheid/Contrast Behouden Preferences… Voorkeuren… Open config file in text editor Configuratiebestand openen in teksteditor No Config File Geen configuratiebestand Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc De configuratie is als YAML-expressie via de opdrachtregel opgegeven. Om de voorkeureneditor te gebruiken, start Labelme met een configuratiebestand: labelme --config ~/.labelmerc Configuration Errors Configuratiefouten Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Er zijn fouten gevonden bij het laden van de configuratie. Bekijk de onderstaande fouten en herlaad uw configuratie of negeer de foutieve regels. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Zorg ervoor dat <i>%s</i> een geldig labelbestand is.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Zorg ervoor dat <i>%s</i> een geldig afbeeldingsbestand is.</p> Reset Layout Layout herstellen ================================================ FILE: labelme/translate/pl_PL.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation Adnotacja wspomagana przez AI AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI sugeruje adnotacje w trybach 'AI-Polygon' i 'AI-Mask' Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Wybierz tryb 'AI-Polygon' lub 'AI-Mask', aby włączyć adnotację wspomaganą przez AI AiTextToAnnotationWidget AI Text-to-Annotation AI — tekst na adnotację e.g., dog,cat,bird np. pies,kot,ptak Run Uruchom Score Wynik IoU IoU AI creates annotations from the text prompt AI tworzy adnotacje na podstawie podanego tekstu Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Wybierz tryb 'Polygon', 'Rectangle', 'AI-Polygon' lub 'AI-Mask', aby włączyć Canvas Click & drag to move point Kliknij i przeciągnij, aby przesunąć punkt Click & drag to move shape Kliknij i przeciągnij, aby przesunąć kształt Creating %r Tworzenie %r ESC to cancel ESC — anuluj Enter or Space to finalize Enter lub Spacja — zatwierdź Editing shapes Edycja kształtów Click points to include or Shift+Click to exclude for ai_polygon Kliknij punkty, aby dołączyć, lub Shift+Klik, aby wykluczyć (ai_polygon) Click points to include or Shift+Click to exclude for ai_mask Kliknij punkty, aby dołączyć, lub Shift+Klik, aby wykluczyć (ai_mask) Click start point for line Kliknij punkt początkowy linii Click end point for line Kliknij punkt końcowy linii Click start point for linestrip Kliknij punkt początkowy polilinii Click next point or finish by Ctrl/Cmd+Click for linestrip Kliknij następny punkt lub Ctrl/Cmd+Klik, aby zakończyć polilinię Click center point for circle Kliknij środek okręgu Click point on circumference for circle Kliknij punkt na obwodzie okręgu Click first corner for rectangle Kliknij pierwszy róg prostokąta Click to add point Kliknij, aby dodać punkt ALT + SHIFT + Click to delete point ALT + SHIFT + Klik — usuń punkt ALT + Click to create point on shape ALT + Klik — utwórz punkt na kształcie Right-click & drag to copy shape Prawy przycisk i przeciągnij — skopiuj kształt Click opposite corner for rectangle (Shift for square) Kliknij przeciwległy róg prostokąta (Shift — kwadrat) MainWindow Flags Flagi Annotation List Lista adnotacji Select label to start annotating for it. Press 'Esc' to deselect. Wybierz etykietę, aby zacząć adnotację. Naciśnij 'Esc', aby odznaczyć. Label List Lista etykiet Search Filename Szukaj nazwy pliku File List Lista plików &Quit &Zakończ Quit application Zamknij aplikację &Open &Otwórz Open image or label file Otwórz obraz lub plik etykiet Open Dir Otwórz folder &Next Image &Następny obraz Open next (hold Ctl+Shift to copy labels) Otwórz następny (Ctrl+Shift — kopiuj etykiety) &Prev Image &Poprzedni obraz Open prev (hold Ctl+Shift to copy labels) Otwórz poprzedni (Ctrl+Shift — kopiuj etykiety) &Save &Zapisz Save labels to file Zapisz etykiety do pliku &Save As Zapisz &jako Save labels to a different file Zapisz etykiety do innego pliku &Delete File &Usuń plik Delete current label file Usuń bieżący plik etykiet &Change Output Dir &Zmień folder wynikowy Change where annotations are loaded/saved Zmień miejsce wczytywania/zapisywania adnotacji Save &Automatically Zapisuj &automatycznie Save automatically Zapisuj automatycznie Save With Image Data Zapisz z danymi obrazu Save image data in label file Zapisz dane obrazu w pliku etykiet &Close &Zamknij Close current file Zamknij bieżący plik Keep Previous Annotation Zachowaj poprzednią adnotację Create Polygons Utwórz wielokąty Start drawing polygons Rozpocznij rysowanie wielokątów Create Rectangle Utwórz prostokąt Start drawing rectangles Rozpocznij rysowanie prostokątów Create Circle Utwórz okrąg Start drawing circles Rozpocznij rysowanie okręgów Create Line Utwórz linię Start drawing lines Rozpocznij rysowanie linii Create Point Utwórz punkt Start drawing points Rozpocznij rysowanie punktów Create LineStrip Utwórz polilinię Start drawing linestrip. Ctrl+LeftClick ends creation. Rozpocznij rysowanie polilinii. Ctrl+Klik lewy kończy tworzenie. Create AI-Polygon Utwórz AI-wielokąt Start drawing ai_polygon. Ctrl+LeftClick ends creation. Rozpocznij rysowanie ai_polygon. Ctrl+Klik lewy kończy tworzenie. Create AI-Mask Utwórz maskę AI Start drawing ai_mask. Ctrl+LeftClick ends creation. Rozpocznij rysowanie ai_mask. Ctrl+Klik lewy kończy tworzenie. Edit Shapes Edytuj kształty Move and edit the selected shapes Przesuń i edytuj zaznaczone kształty Delete Shapes Usuń kształty Delete the selected shapes Usuń zaznaczone kształty Duplicate Shapes Duplikuj kształty Create a duplicate of the selected shapes Utwórz duplikat zaznaczonych kształtów Copy Shapes Kopiuj kształty Copy selected shapes to clipboard Kopiuj zaznaczone kształty do schowka Paste Shapes Wklej kształty Paste copied shapes Wklej skopiowane kształty Undo last point Cofnij ostatni punkt Undo last drawn point Cofnij ostatnio narysowany punkt Remove Selected Point Usuń zaznaczony punkt Remove selected point from polygon Usuń zaznaczony punkt z wielokąta Undo Cofnij Undo last add and edit of shape Cofnij ostatnie dodanie i edycję kształtu &Hide Shapes &Ukryj kształty Hide all shapes Ukryj wszystkie kształty &Show Shapes &Pokaż kształty Show all shapes Pokaż wszystkie kształty &Toggle Shapes Przełącz &kształty Toggle all shapes Przełącz widoczność wszystkich kształtów &Tutorial &Samouczek Show tutorial page Pokaż stronę samouczka Zoom Powiększenie Zoom in or out of the image. Also accessible with {} and {} from the canvas. Powiększ lub pomniejsz obraz. Dostępne także przez {} i {} na płótnie. Ctrl+Wheel Ctrl+kółko Zoom &In Powiększ (&I) Increase zoom level Zwiększ powiększenie &Zoom Out Pomniejsz (&Z) Decrease zoom level Zmniejsz powiększenie &Original size Oryginalny rozmiar (&O) Zoom to original size Powiększ do oryginalnego rozmiaru &Keep Previous Scale Zachowaj poprzednią skalę (&K) Keep previous zoom scale Zachowaj poprzednią skalę powiększenia &Fit Window Dopasuj do okna (&F) Zoom follows window size Powiększenie dopasowane do rozmiaru okna Fit &Width Dopasuj szerokość (&W) Zoom follows window width Powiększenie dopasowane do szerokości okna &Brightness Contrast Jasność i kontrast (&B) Adjust brightness and contrast Dostosuj jasność i kontrast &Edit Label &Edytuj etykietę Modify the label of the selected shape Zmień etykietę zaznaczonego kształtu Fill Drawing Polygon Wypełnij rysowany wielokąt Fill polygon while drawing Wypełniaj wielokąt podczas rysowania &File &Plik &Edit &Edycja &View &Widok &Help &Pomoc Open &Recent Otwórz &ostatnie %s started. %s uruchomiono. Invalid label Nieprawidłowa etykieta Invalid label '{}' with validation type '{}' Nieprawidłowa etykieta '{}' dla typu walidacji '{}' Error saving label data Błąd zapisu etykiet <b>%s</b> <b>%s</b> Error opening file Błąd otwarcia pliku No such file: <b>%s</b> Brak pliku: <b>%s</b> Loading %s... Wczytywanie %s... <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Upewnij się, że <i>%s</i> jest prawidłowym plikiem etykiet.</p> Error reading %s Błąd odczytu %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Upewnij się, że <i>{0}</i> jest prawidłowym plikiem obrazu.<br/>Obsługiwane formaty: {1}</p> Loaded %s Wczytano %s Image & Label files (%s) Pliki obrazów i etykiet (%s) %s - Choose Image or Label file %s — Wybierz plik obrazu lub etykiet %s - Save/Load Annotations in Directory %s — Zapisz/wczytaj adnotacje w folderze %s . Annotations will be saved/loaded in %s %s. Adnotacje będą zapisywane/wczytywane w %s %s - Choose File %s — Wybierz plik Label files (*%s) Pliki etykiet (*%s) Choose File Wybierz plik You are about to permanently delete this label file, proceed anyway? Czy na pewno trwale usunąć ten plik etykiet? Attention Uwaga Save annotations to "{}" before closing? Zapisać adnotacje do "{}" przed zamknięciem? Save annotations? Zapisać adnotacje? You are about to permanently delete {} shapes, proceed anyway? Czy na pewno trwale usunąć {} kształtów? %s - Open Directory %s — Otwórz folder Toggle "keep previous annotation" mode Przełącz tryb „zachowaj poprzednią adnotację” Keep Previous Brightness/Contrast Zachowaj poprzednią jasność/kontrast Preferences… Preferencje… Open config file in text editor Otwórz plik konfiguracji w edytorze tekstu No Config File Brak pliku konfiguracji Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc Konfiguracja została podana w linii poleceń jako wyrażenie YAML. Aby użyć edytora preferencji, uruchom Labelme z plikiem konfiguracji: labelme --config ~/.labelmerc Configuration Errors Błędy konfiguracji Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Wystąpiły błędy podczas wczytywania konfiguracji. Sprawdź listę poniżej i przeładuj konfigurację lub zignoruj błędne wiersze. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Upewnij się, że <i>%s</i> jest prawidłowym plikiem obrazu.</p> Reset Layout Zresetuj układ ================================================ FILE: labelme/translate/pt_BR.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation Anotação Assistida por IA AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes IA sugere anotação nos modos 'Polígono IA' e 'Máscara IA' Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Selecione o modo 'Polígono IA' ou 'Máscara IA' para ativar a Anotação Assistida por IA AiTextToAnnotationWidget AI Text-to-Annotation Texto para Anotação IA AI creates annotations from the text prompt IA cria anotações a partir do prompt de texto e.g., dog,cat,bird ex.: cachorro, gato, pássaro Run Executar Score Pontuação IoU IoU Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Selecione o modo 'Polígono', 'Retângulo', 'Polígono IA' ou 'Máscara IA' para ativar Canvas Click & drag to move point Clique e arraste para mover o ponto Click & drag to move shape Clique e arraste para mover a forma Creating %r Criando %r ESC to cancel ESC para cancelar Enter or Space to finalize Enter ou Espaço para finalizar Editing shapes Editando formas Click points to include or Shift+Click to exclude for ai_polygon Clique nos pontos para incluir ou Shift+Clique para excluir (polígono IA) Click points to include or Shift+Click to exclude for ai_mask Clique nos pontos para incluir ou Shift+Clique para excluir (máscara IA) Click start point for line Clique no ponto inicial da linha Click end point for line Clique no ponto final da linha Click start point for linestrip Clique no ponto inicial da linha contínua Click next point or finish by Ctrl/Cmd+Click for linestrip Clique no próximo ponto ou Ctrl/Cmd+Clique para finalizar (linha contínua) Click center point for circle Clique no ponto central do círculo Click point on circumference for circle Clique em um ponto da circunferência do círculo Click first corner for rectangle Clique na primeira esquina do retângulo Click to add point Clique para adicionar um ponto ALT + SHIFT + Click to delete point ALT + SHIFT + Clique para excluir o ponto ALT + Click to create point on shape ALT + Clique para criar um ponto na forma Right-click & drag to copy shape Clique com o botão direito e arraste para copiar a forma Click opposite corner for rectangle (Shift for square) Clique na esquina oposta do retângulo (Shift para quadrado) MainWindow Flags Marcadores Annotation List Lista de Anotações Select label to start annotating for it. Press 'Esc' to deselect. Selecione um rótulo para começar a anotar. Pressione 'Esc' para desmarcar. Label List Lista de Rótulos Search Filename Buscar Nome do Arquivo File List Lista de Arquivos &Quit &Sair Quit application Sair do aplicativo &Open &Abrir Open image or label file Abrir arquivo de imagem ou rótulo Open Dir Abrir Diretório &Next Image Imagem &Seguinte Open next (hold Ctl+Shift to copy labels) Abrir seguinte (mantenha Ctrl+Shift para copiar rótulos) &Prev Image Imagem &Anterior Open prev (hold Ctl+Shift to copy labels) Abrir anterior (mantenha Ctrl+Shift para copiar rótulos) &Save &Salvar Save labels to file Salvar rótulos no arquivo &Save As Salvar &Como Save labels to a different file Salvar rótulos em um arquivo diferente &Delete File &Excluir Arquivo Delete current label file Excluir arquivo de rótulo atual &Change Output Dir Alterar Diretório de &Saída Change where annotations are loaded/saved Alterar onde as anotações são carregadas/salvas Save &Automatically Salvar &Automaticamente Save automatically Salvar automaticamente Save With Image Data Salvar com Dados da Imagem Save image data in label file Salvar dados da imagem no arquivo de rótulo &Close &Fechar Close current file Fechar arquivo atual Keep Previous Annotation Manter Anotação Anterior Create Polygons Criar Polígonos Start drawing polygons Começar a desenhar polígonos Create Rectangle Criar Retângulo Start drawing rectangles Começar a desenhar retângulos Create Circle Criar Círculo Start drawing circles Começar a desenhar círculos Create Line Criar Linha Start drawing lines Começar a desenhar linhas Create Point Criar Ponto Start drawing points Começar a desenhar pontos Create LineStrip Criar Linha Contínua Start drawing linestrip. Ctrl+LeftClick ends creation. Começar a desenhar linha contínua. Ctrl+Clique esquerdo finaliza a criação. Create AI-Polygon Criar Polígono IA Start drawing ai_polygon. Ctrl+LeftClick ends creation. Começar a desenhar polígono IA. Ctrl+Clique esquerdo finaliza a criação. Create AI-Mask Criar Máscara IA Start drawing ai_mask. Ctrl+LeftClick ends creation. Começar a desenhar máscara IA. Ctrl+Clique esquerdo finaliza a criação. Edit Shapes Editar Formas Move and edit the selected shapes Mover e editar as formas selecionadas Delete Shapes Excluir Formas Delete the selected shapes Excluir as formas selecionadas Duplicate Shapes Duplicar Formas Create a duplicate of the selected shapes Criar uma cópia das formas selecionadas Copy Shapes Copiar Formas Copy selected shapes to clipboard Copiar formas selecionados para a área de transferência Paste Shapes Colar Formas Paste copied shapes Colar formas copiadas Undo last point Desfazer último ponto Undo last drawn point Desfazer último ponto desenhado Remove Selected Point Remover Ponto Selecionado Remove selected point from polygon Remover ponto selecionado do polígono Undo Desfazer Undo last add and edit of shape Desfazer última adição e edição de forma &Hide Shapes Ocultar &Formas Hide all shapes Ocultar todas as formas &Show Shapes Mostrar &Formas Show all shapes Mostrar todas as formas &Toggle Shapes Alternar &Formas Toggle all shapes Alternar todas as formas &Tutorial &Tutorial Show tutorial page Mostrar página do tutorial Zoom Zoom Zoom in or out of the image. Also accessible with {} and {} from the canvas. Aproximar ou afastar a imagem. Também acessível com {} e {} a partir da tela. Ctrl+Wheel Ctrl+Roda Zoom &In &Aproximar Increase zoom level Aumentar nível de zoom &Zoom Out &Afastar Decrease zoom level Diminuir nível de zoom &Original size Tamanho &Original Zoom to original size Zoom para o tamanho original &Keep Previous Scale Manter Escala &Anterior Keep previous zoom scale Manter escala de zoom anterior &Fit Window Ajustar à &Janela Zoom follows window size O zoom segue o tamanho da janela Fit &Width Ajustar à &Largura Zoom follows window width O zoom segue a largura da janela &Brightness Contrast Brilho e &Contraste Adjust brightness and contrast Ajustar brilho e contraste &Edit Label &Editar Rótulo Modify the label of the selected shape Modificar o rótulo da forma selecionada Fill Drawing Polygon Preencher Polígono ao Desenhar Fill polygon while drawing Preencher polígono enquanto desenha &File &Arquivo &Edit &Editar &View &Visualizar &Help &Ajuda Open &Recent Abrir &Recentes %s started. %s iniciado. Invalid label Rótulo inválido Invalid label '{}' with validation type '{}' Rótulo inválido '{}' com tipo de validação '{}' Error saving label data Erro ao salvar dados do rótulo <b>%s</b> <b>%s</b> Error opening file Erro ao abrir arquivo No such file: <b>%s</b> Arquivo não encontrado: <b>%s</b> Loading %s... Carregando %s... Error reading %s Erro ao ler %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Certifique-se de que <i>{0}</i> seja um arquivo de imagem válido.<br/>Formatos de imagem suportados: {1}</p> Loaded %s Carregado %s Image & Label files (%s) Arquivos de Imagem e Rótulo (%s) %s - Choose Image or Label file %s - Escolher Arquivo de Imagem ou Rótulo %s - Save/Load Annotations in Directory %s - Salvar/Carregar Anotações no Diretório %s . Annotations will be saved/loaded in %s %s . As anotações serão salvas/carregadas em %s %s - Choose File %s - Escolher Arquivo Label files (*%s) Arquivos de rótulo (*%s) Choose File Escolher Arquivo You are about to permanently delete this label file, proceed anyway? Você está prestes a excluir permanentemente este arquivo de rótulo, continuar mesmo assim? Attention Atenção Save annotations to "{}" before closing? Salvar anotações em "{}" antes de fechar? Save annotations? Salvar anotações? You are about to permanently delete {} shapes, proceed anyway? Você está prestes a excluir permanentemente {} formas, continuar mesmo assim? %s - Open Directory %s - Abrir Diretório Toggle "keep previous annotation" mode Alternar modo "manter anotação anterior" Keep Previous Brightness/Contrast Manter Brilho/Contraste Anterior Preferences… Preferências… Open config file in text editor Abrir arquivo de configuração no editor de texto No Config File Sem arquivo de configuração Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc A configuração foi fornecida como uma expressão YAML via linha de comando. Para usar o editor de preferências, inicie o Labelme com um arquivo de configuração: labelme --config ~/.labelmerc Configuration Errors Erros de Configuração Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Foram encontrados erros ao carregar a configuração. Por favor, revise os erros abaixo e recarregue sua configuração ou ignore as linhas com erro. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Certifique-se de que <i>%s</i> seja um arquivo de rótulo válido.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Certifique-se de que <i>%s</i> seja um arquivo de imagem válido.</p> Reset Layout Redefinir layout ================================================ FILE: labelme/translate/th_TH.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation แอนโนเทชันด้วย AI AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI แนะนำแอนโนเทชันในโหมด 'AI-Polygon' และ 'AI-Mask' Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation เลือกโหมด 'AI-Polygon' หรือ 'AI-Mask' เพื่อเปิดใช้แอนโนเทชันด้วย AI AiTextToAnnotationWidget AI Text-to-Annotation AI ข้อความสู่แอนโนเทชัน e.g., dog,cat,bird เช่น สุนัข, แมว, นก Run รัน Score คะแนน IoU IoU AI creates annotations from the text prompt AI สร้างแอนโนเทชันจากข้อความที่ป้อน Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable เลือกโหมด 'Polygon', 'Rectangle', 'AI-Polygon' หรือ 'AI-Mask' เพื่อเปิดใช้ Canvas Click & drag to move point คลิกและลากเพื่อย้ายจุด Click & drag to move shape คลิกและลากเพื่อย้ายรูปร่าง Creating %r กำลังสร้าง %r ESC to cancel กด ESC เพื่อยกเลิก Enter or Space to finalize กด Enter หรือ Space เพื่อยืนยัน Editing shapes แก้ไขรูปร่าง Click points to include or Shift+Click to exclude for ai_polygon คลิกจุดเพื่อรวม หรือ Shift+คลิกเพื่อยกเว้น สำหรับ ai_polygon Click points to include or Shift+Click to exclude for ai_mask คลิกจุดเพื่อรวม หรือ Shift+คลิกเพื่อยกเว้น สำหรับ ai_mask Click start point for line คลิกจุดเริ่มต้นของเส้น Click end point for line คลิกจุดสิ้นสุดของเส้น Click start point for linestrip คลิกจุดเริ่มต้นของเส้นต่อเนื่อง Click next point or finish by Ctrl/Cmd+Click for linestrip คลิกจุดถัดไป หรือ Ctrl/Cmd+คลิกเพื่อจบเส้นต่อเนื่อง Click center point for circle คลิกจุดศูนย์กลางของวงกลม Click point on circumference for circle คลิกจุดบนเส้นรอบวงของวงกลม Click first corner for rectangle คลิกมุมแรกของสี่เหลี่ยม Click to add point คลิกเพื่อเพิ่มจุด ALT + SHIFT + Click to delete point ALT + SHIFT + คลิกเพื่อลบจุด ALT + Click to create point on shape ALT + คลิกเพื่อสร้างจุดบนรูปร่าง Right-click & drag to copy shape คลิกขวาและลากเพื่อคัดลอกรูปร่าง Click opposite corner for rectangle (Shift for square) คลิกมุมตรงข้ามของสี่เหลี่ยม (Shift สำหรับสี่เหลี่ยมจัตุรัส) MainWindow Flags แฟล็ก Annotation List รายการแอนโนเทชัน Select label to start annotating for it. Press 'Esc' to deselect. เลือกเลเบลเพื่อเริ่มกำกับข้อมูล กด 'Esc' เพื่อยกเลิกการเลือก Label List รายการเลเบล Search Filename ค้นหาชื่อไฟล์ File List รายการไฟล์ &Quit ออก (&Q) Quit application ปิดแอป &Open เปิด (&O) Open image or label file เปิดรูปหรือไฟล์เลเบล Open Dir เปิดโฟลเดอร์ &Next Image รูปถัดไป (&N) Open next (hold Ctl+Shift to copy labels) เปิดรูปถัดไป (กด Ctrl+Shift เพื่อคัดลอกเลเบล) &Prev Image รูปก่อนหน้า (&P) Open prev (hold Ctl+Shift to copy labels) เปิดรูปก่อนหน้า (กด Ctrl+Shift เพื่อคัดลอกเลเบล) &Save บันทึก (&S) Save labels to file บันทึกเลเบลลงไฟล์ &Save As บันทึกเป็น (&S) Save labels to a different file บันทึกเลเบลไปยังไฟล์อื่น &Delete File ลบไฟล์ (&D) Delete current label file ลบไฟล์เลเบลปัจจุบัน &Change Output Dir เปลี่ยนโฟลเดอร์ผลลัพธ์ (&C) Change where annotations are loaded/saved เปลี่ยนโฟลเดอร์สำหรับโหลด/บันทึกแอนโนเทชัน Save &Automatically บันทึกอัตโนมัติ (&A) Save automatically บันทึกอัตโนมัติ Save With Image Data บันทึกพร้อมข้อมูลรูป Save image data in label file บันทึกข้อมูลรูปในไฟล์เลเบล &Close ปิด (&C) Close current file ปิดไฟล์ปัจจุบัน Keep Previous Annotation คงแอนโนเทชันก่อนหน้า Create Polygons สร้างหลายเหลี่ยม Start drawing polygons เริ่มวาดหลายเหลี่ยม Create Rectangle สร้างสี่เหลี่ยม Start drawing rectangles เริ่มวาดสี่เหลี่ยม Create Circle สร้างวงกลม Start drawing circles เริ่มวาดวงกลม Create Line สร้างเส้น Start drawing lines เริ่มวาดเส้น Create Point สร้างจุด Start drawing points เริ่มวาดจุด Create LineStrip สร้างเส้นต่อเนื่อง Start drawing linestrip. Ctrl+LeftClick ends creation. เริ่มวาดเส้นต่อเนื่อง Ctrl+คลิกซ้ายเพื่อจบ Create AI-Polygon สร้าง AI-หลายเหลี่ยม Start drawing ai_polygon. Ctrl+LeftClick ends creation. เริ่มวาด ai_polygon Ctrl+คลิกซ้ายเพื่อจบ Create AI-Mask สร้าง AI-มาสก์ Start drawing ai_mask. Ctrl+LeftClick ends creation. เริ่มวาด ai_mask Ctrl+คลิกซ้ายเพื่อจบ Edit Shapes แก้ไขรูปร่าง Move and edit the selected shapes ย้ายและแก้ไขรูปร่างที่เลือก Delete Shapes ลบรูปร่าง Delete the selected shapes ลบรูปร่างที่เลือก Duplicate Shapes ทำซ้ำรูปร่าง Create a duplicate of the selected shapes สร้างสำเนารูปร่างที่เลือก Copy Shapes คัดลอกรูปร่าง Copy selected shapes to clipboard คัดลอกรูปร่างที่เลือกไปคลิปบอร์ด Paste Shapes วางรูปร่าง Paste copied shapes วางรูปร่างที่คัดลอก Undo last point ยกเลิกจุดล่าสุด Undo last drawn point ยกเลิกจุดที่วาดล่าสุด Remove Selected Point ลบจุดที่เลือก Remove selected point from polygon ลบจุดที่เลือกออกจากหลายเหลี่ยม Undo ยกเลิก Undo last add and edit of shape ยกเลิกการเพิ่มและแก้ไขรูปร่างล่าสุด &Hide Shapes ซ่อนรูปร่าง (&H) Hide all shapes ซ่อนรูปร่างทั้งหมด &Show Shapes แสดงรูปร่าง (&S) Show all shapes แสดงรูปร่างทั้งหมด &Toggle Shapes สลับรูปร่าง (&S) Toggle all shapes สลับรูปร่างทั้งหมด &Tutorial บทแนะนำ (&T) Show tutorial page แสดงหน้าบทแนะนำ Zoom ซูม Zoom in or out of the image. Also accessible with {} and {} from the canvas. ซูมเข้า-ออกของรูป ใช้ {} และ {} ที่แคนวาสได้เช่นกัน Ctrl+Wheel Ctrl+ล้อเลื่อน Zoom &In ซูมเข้า (&I) Increase zoom level เพิ่มระดับซูม &Zoom Out ซูมออก (&Z) Decrease zoom level ลดระดับซูม &Original size ขนาดจริง (&O) Zoom to original size ซูมเป็นขนาดจริง &Keep Previous Scale คงระดับซูมก่อนหน้า (&K) Keep previous zoom scale คงระดับซูมก่อนหน้า &Fit Window พอดีหน้าต่าง (&F) Zoom follows window size ซูมตามขนาดหน้าต่าง Fit &Width พอดีความกว้าง (&W) Zoom follows window width ซูมตามความกว้างหน้าต่าง &Brightness Contrast ความสว่าง คอนทราสต์ (&B) Adjust brightness and contrast ปรับความสว่างและคอนทราสต์ &Edit Label แก้ไขเลเบล (&E) Modify the label of the selected shape แก้ไขเลเบลของรูปร่างที่เลือก Fill Drawing Polygon เติมสีหลายเหลี่ยมที่วาด Fill polygon while drawing เติมสีหลายเหลี่ยมขณะวาด &File ไฟล์ (&F) &Edit แก้ไข (&E) &View มุมมอง (&V) &Help ช่วยเหลือ (&H) Open &Recent เปิดล่าสุด (&R) %s started. %s เริ่มทำงานแล้ว Invalid label เลเบลไม่ถูกต้อง Invalid label '{}' with validation type '{}' เลเบล '{}' ไม่ถูกต้อง ประเภทการตรวจ '{}' Error saving label data เกิดข้อผิดพลาดในการบันทึกข้อมูลเลเบล <b>%s</b> <b>%s</b> Error opening file เกิดข้อผิดพลาดในการเปิดไฟล์ No such file: <b>%s</b> ไม่มีไฟล์: <b>%s</b> Loading %s... กำลังโหลด %s... Error reading %s เกิดข้อผิดพลาดในการอ่าน %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>ตรวจสอบว่า <i>{0}</i> เป็นไฟล์รูปที่ถูกต้อง<br/>รูปแบบรูปที่รองรับ: {1}</p> Loaded %s โหลด %s แล้ว Image & Label files (%s) ไฟล์รูปและเลเบล (%s) %s - Choose Image or Label file %s - เลือกรูปหรือไฟล์เลเบล %s - Save/Load Annotations in Directory %s - บันทึก/โหลดแอนโนเทชันในโฟลเดอร์ %s . Annotations will be saved/loaded in %s %s แอนโนเทชันจะถูกบันทึก/โหลดใน %s %s - Choose File %s - เลือกไฟล์ Label files (*%s) ไฟล์เลเบล (*%s) Choose File เลือกไฟล์ You are about to permanently delete this label file, proceed anyway? คุณกำลังจะลบไฟล์เลเบลนี้อย่างถาวร ต้องการดำเนินการต่อหรือไม่? Attention คำเตือน Save annotations to "{}" before closing? บันทึกแอนโนเทชันไปที่ "{}" ก่อนปิดหรือไม่? Save annotations? บันทึกแอนโนเทชันหรือไม่? You are about to permanently delete {} shapes, proceed anyway? คุณกำลังจะลบรูปร่าง {} รายการอย่างถาวร ต้องการดำเนินการต่อหรือไม่? %s - Open Directory %s - เปิดโฟลเดอร์ Toggle "keep previous annotation" mode สลับโหมด "คงแอนโนเทชันก่อนหน้า" Keep Previous Brightness/Contrast คงความสว่าง/คอนทราสต์ก่อนหน้า Preferences… การตั้งค่า… Open config file in text editor เปิดไฟล์ตั้งค่าในโปรแกรมแก้ไขข้อความ No Config File ไม่มีไฟล์ตั้งค่า Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc การตั้งค่าถูกกำหนดเป็น YAML ผ่านบรรทัดคำสั่ง หากต้องการใช้ตัวแก้ไขการตั้งค่า ให้เริ่ม Labelme ด้วยไฟล์ config: labelme --config ~/.labelmerc Configuration Errors ข้อผิดพลาดการตั้งค่า Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. พบข้อผิดพลาดขณะโหลดการตั้งค่า กรุณาตรวจสอบด้านล่าง แล้วโหลดการตั้งค่าใหม่หรือข้ามบรรทัดที่มีข้อผิดพลาด <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>ตรวจสอบว่า <i>%s</i> เป็นไฟล์เลเบลที่ถูกต้อง</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>ตรวจสอบว่า <i>%s</i> เป็นไฟล์รูปที่ถูกต้อง</p> Reset Layout รีเซ็ตเค้าโครง ================================================ FILE: labelme/translate/tr_TR.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation AI Destekli Etiketleme AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI, 'AI-Polygon' ve 'AI-Mask' modlarında etiketleme önerir Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation AI Destekli Etiketlemeyi etkinleştirmek için 'AI-Polygon' veya 'AI-Mask' modunu seçin AiTextToAnnotationWidget AI Text-to-Annotation AI Metinden Etiketleme e.g., dog,cat,bird örn., köpek,kedi,kuş Run Çalıştır Score Skor IoU IoU AI creates annotations from the text prompt AI, metin isteminden etiketlemeler oluşturur Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Etkinleştirmek için 'Polygon', 'Rectangle', 'AI-Polygon' veya 'AI-Mask' modunu seçin Canvas Click & drag to move point Noktayı taşımak için tıkla ve sürükle Click & drag to move shape Şekli taşımak için tıkla ve sürükle Creating %r %r oluşturuluyor ESC to cancel İptal için ESC Enter or Space to finalize Bitirmek için Enter veya Space Editing shapes Şekiller düzenleniyor Click points to include or Shift+Click to exclude for ai_polygon ai_polygon için noktaları dahil etmek için tıkla, hariç tutmak için Shift+tıkla Click points to include or Shift+Click to exclude for ai_mask ai_mask için noktaları dahil etmek için tıkla, hariç tutmak için Shift+tıkla Click start point for line Çizgi için başlangıç noktasına tıkla Click end point for line Çizgi için bitiş noktasına tıkla Click start point for linestrip Çizgi şeridi için başlangıç noktasına tıkla Click next point or finish by Ctrl/Cmd+Click for linestrip Çizgi şeridi için sonraki noktaya tıkla veya Ctrl/Cmd+tıkla ile bitir Click center point for circle Daire için merkez noktaya tıkla Click point on circumference for circle Daire için çevre üzerindeki bir noktaya tıkla Click first corner for rectangle Dikdörtgen için ilk köşeye tıkla Click to add point Nokta eklemek için tıkla ALT + SHIFT + Click to delete point Noktayı silmek için ALT + SHIFT + tıkla ALT + Click to create point on shape Şekil üzerinde nokta oluşturmak için ALT + tıkla Right-click & drag to copy shape Şekli kopyalamak için sağ tıkla ve sürükle Click opposite corner for rectangle (Shift for square) Dikdörtgen için karşı köşeye tıkla (kare için Shift) MainWindow Flags Bayraklar Annotation List Etiketleme Listesi Select label to start annotating for it. Press 'Esc' to deselect. Etiketlemeye başlamak için bir etiket seçin. Seçimi kaldırmak için 'Esc' tuşuna basın. Label List Etiket Listesi Search Filename Dosya Adı Ara File List Dosya Listesi &Quit &Çıkış Quit application Uygulamadan çık &Open &Aç Open image or label file Görüntü veya etiket dosyası aç Open Dir Dizin Aç &Next Image &Sonraki Görüntü Open next (hold Ctl+Shift to copy labels) Sonrakini aç (etiketleri kopyalamak için Ctrl+Shift basılı tutun) &Prev Image &Önceki Görüntü Open prev (hold Ctl+Shift to copy labels) Öncekini aç (etiketleri kopyalamak için Ctrl+Shift basılı tutun) &Save &Kaydet Save labels to file Etiketleri dosyaya kaydet &Save As Farklı &Kaydet Save labels to a different file Etiketleri farklı bir dosyaya kaydet &Delete File Dosyayı &Sil Delete current label file Geçerli etiket dosyasını sil &Change Output Dir Çıktı Dizinini &Değiştir Change where annotations are loaded/saved Etiketlemelerin yüklendiği/kaydedildiği yeri değiştir Save &Automatically Otomatik &Kaydet Save automatically Otomatik kaydet Save With Image Data Görüntü Verisiyle Kaydet Save image data in label file Görüntü verisini etiket dosyasında kaydet &Close &Kapat Close current file Geçerli dosyayı kapat Keep Previous Annotation Önceki Etiketlemeyi Koru Create Polygons Çokgen Oluştur Start drawing polygons Çokgen çizmeye başla Create Rectangle Dikdörtgen Oluştur Start drawing rectangles Dikdörtgen çizmeye başla Create Circle Daire Oluştur Start drawing circles Daire çizmeye başla Create Line Çizgi Oluştur Start drawing lines Çizgi çizmeye başla Create Point Nokta Oluştur Start drawing points Nokta çizmeye başla Create LineStrip Çizgi Şeridi Oluştur Start drawing linestrip. Ctrl+LeftClick ends creation. Çizgi şeridi çizmeye başla. Ctrl+Sol Tık ile oluşturmayı bitirir. Create AI-Polygon AI-Polygon Oluştur Start drawing ai_polygon. Ctrl+LeftClick ends creation. ai_polygon çizmeye başla. Ctrl+Sol Tık ile oluşturmayı bitirir. Create AI-Mask AI-Mask Oluştur Start drawing ai_mask. Ctrl+LeftClick ends creation. ai_mask çizmeye başla. Ctrl+Sol Tık ile oluşturmayı bitirir. Edit Shapes Şekilleri Düzenle Move and edit the selected shapes Seçili şekilleri taşı ve düzenle Delete Shapes Şekilleri Sil Delete the selected shapes Seçili şekilleri sil Duplicate Shapes Şekilleri Çoğalt Create a duplicate of the selected shapes Seçili şekillerin kopyasını oluştur Copy Shapes Şekilleri Kopyala Copy selected shapes to clipboard Seçili şekilleri panoya kopyala Paste Shapes Şekilleri Yapıştır Paste copied shapes Kopyalanan şekilleri yapıştır Undo last point Son noktayı geri al Undo last drawn point Çizilen son noktayı geri al Remove Selected Point Seçili Noktayı Sil Remove selected point from polygon Seçili noktayı çokgenden sil Undo Geri Al Undo last add and edit of shape Şeklin son ekleme ve düzenlemesini geri al &Hide Shapes Şekilleri &Gizle Hide all shapes Tüm şekilleri gizle &Show Shapes Şekilleri &Göster Show all shapes Tüm şekilleri göster &Toggle Shapes Şekilleri &Aç/Kapat Toggle all shapes Tüm şekilleri aç/kapat &Tutorial &Öğretici Show tutorial page Öğretici sayfasını göster Zoom Yakınlaştırma Zoom in or out of the image. Also accessible with {} and {} from the canvas. Görüntüyü yakınlaştır veya uzaklaştır. Tuvalde {} ve {} ile de erişilebilir. Ctrl+Wheel Ctrl+Tekerlek Zoom &In &Yakınlaştır Increase zoom level Yakınlaştırma seviyesini artır &Zoom Out &Uzaklaştır Decrease zoom level Yakınlaştırma seviyesini azalt &Original size &Orijinal boyut Zoom to original size Orijinal boyuta getir &Keep Previous Scale Önceki Ölçeği &Koru Keep previous zoom scale Önceki yakınlaştırma ölçeğini koru &Fit Window &Pencereye Sığdır Zoom follows window size Yakınlaştırma pencere boyutunu izler Fit &Width &Genişliğe Sığdır Zoom follows window width Yakınlaştırma pencere genişliğini izler &Brightness Contrast &Parlaklık Kontrast Adjust brightness and contrast Parlaklık ve kontrastı ayarla &Edit Label Etiketi &Düzenle Modify the label of the selected shape Seçili şeklin etiketini değiştir Fill Drawing Polygon Çizerken Çokgeni Doldur Fill polygon while drawing Çizerken çokgeni doldur &File &Dosya &Edit &Düzenle &View &Görünüm &Help &Yardım Open &Recent &Son Açılanlar %s started. %s başlatıldı. Invalid label Geçersiz etiket Invalid label '{}' with validation type '{}' Doğrulama türü '{}' olan '{}' etiketi geçersiz Error saving label data Etiket verisi kaydedilirken hata <b>%s</b> <b>%s</b> Error opening file Dosya açma hatası No such file: <b>%s</b> Böyle bir dosya yok: <b>%s</b> Loading %s... %s yükleniyor... Error reading %s %s okunurken hata <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Lütfen <i>{0}</i> geçerli bir görüntü dosyası olduğundan emin olun.<br/>Desteklenen görüntü biçimleri: {1}</p> Loaded %s %s yüklendi Image & Label files (%s) Görüntü ve Etiket dosyaları (%s) %s - Choose Image or Label file %s - Görüntü veya Etiket dosyası seç %s - Save/Load Annotations in Directory %s - Dizinde Etiketlemeleri Kaydet/Yükle %s . Annotations will be saved/loaded in %s %s . Etiketlemeler %s içinde kaydedilecek/yüklenecek %s - Choose File %s - Dosya Seç Label files (*%s) Etiket dosyaları (*%s) Choose File Dosya Seç You are about to permanently delete this label file, proceed anyway? Bu etiket dosyasını kalıcı olarak silmek üzeresiniz, yine de devam edilsin mi? Attention Dikkat Save annotations to "{}" before closing? Kapatmadan önce etiketlemeler "{}" konumuna kaydedilsin mi? Save annotations? Etiketlemeler kaydedilsin mi? You are about to permanently delete {} shapes, proceed anyway? {} şekli kalıcı olarak silmek üzeresiniz, yine de devam edilsin mi? %s - Open Directory %s - Dizini Aç Toggle "keep previous annotation" mode "önceki etiketlemeyi koru" modunu değiştir Keep Previous Brightness/Contrast Önceki Parlaklık/Kontrastı Koru Preferences… Tercihler… Open config file in text editor Yapılandırma dosyasını metin düzenleyicide aç No Config File Yapılandırma Dosyası Yok Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc Yapılandırma, komut satırı üzerinden bir YAML ifadesi olarak sağlandı. Tercihler düzenleyicisini kullanmak için Labelme'i bir yapılandırma dosyasıyla başlatın: labelme --config ~/.labelmerc Configuration Errors Yapılandırma Hataları Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Yapılandırma yüklenirken hatalar bulundu. Lütfen aşağıdaki hataları inceleyin ve yapılandırmayı yeniden yükleyin ya da hatalı satırları yok sayın. <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Lütfen <i>%s</i> geçerli bir etiket dosyası olduğundan emin olun.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Lütfen <i>%s</i> geçerli bir görüntü dosyası olduğundan emin olun.</p> Reset Layout Düzeni sıfırla ================================================ FILE: labelme/translate/vi_VN.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation Chú thích dữ liệu với sự hỗ trợ của AI AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI đề xuất chú thích ở chế độ 'AI-Polygon' và 'AI-Mask' Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation Chọn chế độ 'AI-Polygon' hoặc 'AI-Mask' để bật Chú thích hỗ trợ AI AiTextToAnnotationWidget AI Text-to-Annotation AI Văn bản sang Chú thích e.g., dog,cat,bird ví dụ: chó,mèo,chim Run Chạy Score Điểm số IoU IoU AI creates annotations from the text prompt AI tạo chú thích từ lời nhắc văn bản Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable Chọn chế độ 'Polygon', 'Rectangle', 'AI-Polygon' hoặc 'AI-Mask' để bật Canvas Click & drag to move point Nhấn và kéo để di chuyển điểm Click & drag to move shape Nhấn và kéo để di chuyển hình dạng Creating %r Đang tạo %r ESC to cancel Nhấn ESC để hủy Enter or Space to finalize Nhấn Enter hoặc Space để hoàn tất Editing shapes Chỉnh sửa hình dạng Click points to include or Shift+Click to exclude for ai_polygon Nhấn điểm để bao gồm hoặc Shift+Nhấn để loại trừ cho ai_polygon Click points to include or Shift+Click to exclude for ai_mask Nhấn điểm để bao gồm hoặc Shift+Nhấn để loại trừ cho ai_mask Click start point for line Nhấn điểm bắt đầu cho đường thẳng Click end point for line Nhấn điểm kết thúc cho đường thẳng Click start point for linestrip Nhấn điểm bắt đầu cho đường gấp khúc Click next point or finish by Ctrl/Cmd+Click for linestrip Nhấn điểm tiếp theo hoặc hoàn tất bằng Ctrl/Cmd+Nhấn cho đường gấp khúc Click center point for circle Nhấn điểm trung tâm cho hình tròn Click point on circumference for circle Nhấn điểm trên chu vi cho hình tròn Click first corner for rectangle Nhấn góc đầu tiên cho hình chữ nhật Click to add point Nhấn để thêm điểm ALT + SHIFT + Click to delete point ALT + SHIFT + Nhấn để xóa điểm ALT + Click to create point on shape ALT + Nhấn để tạo điểm trên hình dạng Right-click & drag to copy shape Nhấn chuột phải và kéo để sao chép hình dạng Click opposite corner for rectangle (Shift for square) Nhấn góc đối diện cho hình chữ nhật (Shift để tạo hình vuông) MainWindow Flags Cờ Annotation List Danh sách Chú thích Select label to start annotating for it. Press 'Esc' to deselect. Chọn nhãn để bắt đầu chú thích. Nhấn 'Esc' để bỏ chọn. Label List Danh sách Nhãn Search Filename Tìm kiếm Tên tệp File List Danh sách Tệp &Quit Thoát(&Q) Quit application Thoát ứng dụng &Open Mở(&O) Open image or label file Mở tệp hình ảnh hoặc nhãn Open Dir Mở Thư mục &Next Image Hình ảnh Tiếp theo(&N) Open next (hold Ctl+Shift to copy labels) Mở tiếp theo (giữ Ctl+Shift để sao chép nhãn) &Prev Image Hình ảnh Trước(&P) Open prev (hold Ctl+Shift to copy labels) Mở trước đó (giữ Ctl+Shift để sao chép nhãn) &Save Lưu(&S) Save labels to file Lưu nhãn vào tệp &Save As Lưu thành(&S) Save labels to a different file Lưu nhãn vào tệp khác &Delete File Xóa(&D) Delete current label file Xóa tệp nhãn hiện tại &Change Output Dir Thay đổi Thư mục Đầu ra(&C) Change where annotations are loaded/saved Thay đổi nơi chú thích được tải/lưu Save &Automatically Tự động lưu (&A) Save automatically Tự động lưu Save With Image Data Lưu với Dữ liệu Hình ảnh Save image data in label file Lưu dữ liệu hình ảnh trong tệp nhãn &Close Đóng(&C) Close current file Đóng tệp hiện tại Keep Previous Annotation Giữ Chú thích Trước đó Create Polygons Tạo Đa giác Start drawing polygons Bắt đầu vẽ đa giác Create Rectangle Tạo Hình chữ nhật Start drawing rectangles Bắt đầu vẽ hình chữ nhật Create Circle Tạo Hình tròn Start drawing circles Bắt đầu vẽ hình tròn Create Line Tạo Đường thẳng Start drawing lines Bắt đầu vẽ đường thẳng Create Point Tạo Điểm Start drawing points Bắt đầu vẽ điểm Create LineStrip Tạo Đường gấp khúc Start drawing linestrip. Ctrl+LeftClick ends creation. Bắt đầu vẽ đường gấp khúc. Ctrl+Nhấn chuột trái để kết thúc tạo. Create AI-Polygon Tạo AI-Đa giác Start drawing ai_polygon. Ctrl+LeftClick ends creation. Bắt đầu vẽ ai_polygon. Ctrl+Nhấn chuột trái để kết thúc tạo. Create AI-Mask Tạo AI-Mask Start drawing ai_mask. Ctrl+LeftClick ends creation. Bắt đầu vẽ ai_mask. Ctrl+Nhấn chuột trái để kết thúc tạo. Edit Shapes Chỉnh sửa Hình dạng Move and edit the selected shapes Di chuyển và chỉnh sửa các hình dạng đã chọn Delete Shapes Xóa Hình dạng Delete the selected shapes Xóa các hình dạng đã chọn Duplicate Shapes Nhân đôi Hình dạng Create a duplicate of the selected shapes Tạo bản sao của các hình dạng đã chọn Copy Shapes Sao chép Hình dạng Copy selected shapes to clipboard Sao chép các hình dạng đã chọn vào clipboard Paste Shapes Dán Hình dạng Paste copied shapes Dán các hình dạng đã sao chép Undo last point Hoàn tác điểm cuối cùng Undo last drawn point Hoàn tác điểm vẽ cuối cùng Remove Selected Point Xóa Điểm đã Chọn Remove selected point from polygon Xóa điểm đã chọn khỏi đa giác Undo Hoàn tác Undo last add and edit of shape Hoàn tác lần thêm và chỉnh sửa hình dạng cuối cùng &Hide Shapes Ẩn Hình dạng(&H) Hide all shapes Ẩn tất cả hình dạng &Show Shapes Hiển thị Hình dạng(&S) Show all shapes Hiển thị tất cả hình dạng &Toggle Shapes Bật/tắt Hình dạng(&S) Toggle all shapes Bật/tắt tất cả hình dạng &Tutorial Hướng dẫn(&T) Show tutorial page Hiển thị trang hướng dẫn Zoom Thu phóng Zoom in or out of the image. Also accessible with {} and {} from the canvas. Phóng to hoặc thu nhỏ hình ảnh. Cũng có thể truy cập bằng {} và {} từ canvas. Ctrl+Wheel Ctrl+Bánh xe Zoom &In Phóng to(&I) Increase zoom level Tăng mức thu phóng &Zoom Out Thu nhỏ(&Z) Decrease zoom level Giảm mức thu phóng &Original size Kích thước Gốc(&O) Zoom to original size Thu phóng về kích thước gốc &Keep Previous Scale Giữ Tỷ lệ Trước đó(&K) Keep previous zoom scale Giữ tỷ lệ thu phóng trước đó &Fit Window Vừa Cửa sổ(&F) Zoom follows window size Thu phóng theo kích thước cửa sổ Fit &Width Vừa Chiều rộng(&W) Zoom follows window width Thu phóng theo chiều rộng cửa sổ &Brightness Contrast Độ sáng Độ tương phản(&B) Adjust brightness and contrast Điều chỉnh độ sáng và độ tương phản &Edit Label Chỉnh sửa Nhãn(&E) Modify the label of the selected shape Sửa đổi nhãn của hình dạng đã chọn Fill Drawing Polygon Tô Đa giác Vẽ Fill polygon while drawing Tô đa giác khi vẽ &File Tệp(&F) &Edit Chỉnh sửa(&E) &View Xem(&V) &Help Trợ giúp(&H) Open &Recent Mở Gần đây(&R) %s started. %s đã khởi động. Invalid label Nhãn không hợp lệ Invalid label '{}' with validation type '{}' Nhãn không hợp lệ '{}' với loại xác thực '{}' Error saving label data Lỗi khi lưu dữ liệu nhãn <b>%s</b> <b>%s</b> Error opening file Lỗi khi mở tệp No such file: <b>%s</b> Không có tệp như vậy: <b>%s</b> Loading %s... Đang tải %s... Error reading %s Lỗi khi đọc %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>Đảm bảo <i>{0}</i> là tệp hình ảnh hợp lệ.<br/>Các định dạng hình ảnh được hỗ trợ: {1}</p> Loaded %s Đã tải %s Image & Label files (%s) Tệp Hình ảnh & Nhãn (%s) %s - Choose Image or Label file %s - Chọn tệp Hình ảnh hoặc Nhãn %s - Save/Load Annotations in Directory %s - Lưu/Tải Chú thích trong Thư mục %s . Annotations will be saved/loaded in %s %s . Chú thích sẽ được lưu/tải trong %s %s - Choose File %s - Chọn Tệp Label files (*%s) Tệp nhãn (*%s) Choose File Chọn Tệp You are about to permanently delete this label file, proceed anyway? Bạn sắp xóa vĩnh viễn tệp nhãn này, có tiếp tục không? Attention Chú ý Save annotations to "{}" before closing? Lưu chú thích vào "{}" trước khi đóng? Save annotations? Lưu chú thích? You are about to permanently delete {} shapes, proceed anyway? Bạn sắp xóa vĩnh viễn {} hình dạng, có tiếp tục không? %s - Open Directory %s - Mở Thư mục Toggle "keep previous annotation" mode Bật/tắt chế độ "giữ chú thích trước đó" Keep Previous Brightness/Contrast Giữ Độ sáng/Độ tương phản Trước đó Preferences… Tùy chọn… Open config file in text editor Mở tệp cấu hình trong trình soạn thảo văn bản Configuration Errors Lỗi Cấu hình Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. Đã tìm thấy lỗi khi tải cấu hình. Vui lòng xem lại các lỗi bên dưới và tải lại cấu hình hoặc bỏ qua các dòng bị lỗi. No Config File Không có Tệp Cấu hình Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc Cấu hình được cung cấp dưới dạng biểu thức YAML qua dòng lệnh. Để sử dụng trình chỉnh sửa tùy chọn, khởi động Labelme với tệp cấu hình: labelme --config ~/.labelmerc <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>Đảm bảo <i>%s</i> là tệp nhãn hợp lệ.</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>Đảm bảo <i>%s</i> là tệp hình ảnh hợp lệ.</p> Reset Layout Đặt lại bố cục ================================================ FILE: labelme/translate/zh_CN.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation AI辅助标注 AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI在'AI-Polygon'和'AI-Mask'模式下提供标注建议 Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation 选择「AI-Polygon」或「AI-Mask」模式以启用AI辅助标注 AiTextToAnnotationWidget AI Text-to-Annotation AI提示 e.g., dog,cat,bird 例如:狗,猫,鸟 Run 运行 Score 分数 IoU IoU AI creates annotations from the text prompt AI根据文本提示创建标注 Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable 选择 'Polygon'、'Rectangle'、'AI-Polygon' 或 'AI-Mask' 模式以启用 Canvas Click & drag to move point 点击并拖拽以移动控制点 Click & drag to move shape 点击并拖拽以移动形状 Creating %r 正在创建 %r ESC to cancel 按 ESC 取消 Enter or Space to finalize 按 Enter 或空格键完成 Editing shapes 编辑形状 Click points to include or Shift+Click to exclude for ai_polygon 点击以包含点或 Shift+点击以排除点(AI多边形) Click points to include or Shift+Click to exclude for ai_mask 点击以包含点或 Shift+点击以排除点(AI蒙版) Click start point for line 点击线段起点 Click end point for line 点击线段终点 Click start point for linestrip 点击折线起点 Click next point or finish by Ctrl/Cmd+Click for linestrip 点击下一个点或 Ctrl/Cmd+点击完成折线 Click center point for circle 点击圆形中心点 Click point on circumference for circle 点击圆周上的点 Click first corner for rectangle 点击矩形第一个角 Click to add point 点击以添加点 ALT + SHIFT + Click to delete point ALT + SHIFT + 点击以删除点 ALT + Click to create point on shape ALT + 点击在形状上创建点 Right-click & drag to copy shape 右键点击并拖拽以复制形状 Click opposite corner for rectangle (Shift for square) 点击矩形对角(Shift绘制正方形) MainWindow Flags 标记 Annotation List 批注列表 Select label to start annotating for it. Press 'Esc' to deselect. 选择标签类型并开始以其标注。按'Esc'取消选择。 Label List 标签列表 Search Filename 按文件名检索 File List 文件列表 &Quit 退出(&Q) Quit application 退出应用 &Open 打开(&O) Open image or label file 打开图像或标签文件 Open Dir 打开目录 &Next Image 下一幅(&N) Open next (hold Ctl+Shift to copy labels) 打开下一幅 (按Ctl+Shift拷贝标签) &Prev Image 上一幅(&P) Open prev (hold Ctl+Shift to copy labels) 打开上一幅 (按Ctl+Shift拷贝标签) &Save 保存(&S) Save labels to file 保存标签到文件 &Save As 另存为(&S) Save labels to a different file 保存标签到不同的文件 &Delete File 删除(&D) Delete current label file 删除当前标签文件 &Change Output Dir 更改输出路径(&C) Change where annotations are loaded/saved 更改载入、保存标注的路径 Save &Automatically 自动保存(&A) Save automatically 自动保存 Save With Image Data 同时保存图像数据 Save image data in label file 将图像数据保存到标签文件中 &Close 关闭(&C) Close current file 关闭当前文件 Keep Previous Annotation 保留最后的标注 Create Polygons 创建多边形 Start drawing polygons 开始绘制多边形 Create Rectangle 创建矩形 Start drawing rectangles 开始绘制矩形 Create Circle 创建圆形 Start drawing circles 开始绘制圆形 Create Line 创建直线 Start drawing lines 开始创建直线 Create Point 创建控制点 Start drawing points 开始绘制控制点 Create LineStrip 创建折线 Start drawing linestrip. Ctrl+LeftClick ends creation. 开始绘制折线。Ctrl+单击左键结束绘制。 Create AI-Polygon 创建AI多边形 Start drawing ai_polygon. Ctrl+LeftClick ends creation. 开始绘制AI多边形。Ctrl+单击左键结束绘制。 Create AI-Mask 创建AI蒙版 Start drawing ai_mask. Ctrl+LeftClick ends creation. 开始绘制AI蒙版。Ctrl+单击左键结束绘制。 Edit Shapes 编辑图形 Move and edit the selected shapes 移动、编辑选中的图形 Delete Shapes 删除图形 Delete the selected shapes 删除选中的图形 Duplicate Shapes 复制图形 Create a duplicate of the selected shapes 为选中的图形创建副本 Copy Shapes 复制图形 Copy selected shapes to clipboard 复制选中图形到剪贴板 Paste Shapes 粘贴图形 Paste copied shapes 粘贴已复制的图形 Undo last point 撤销最后的控制点 Undo last drawn point 撤销最后一次绘制的控制点 Remove Selected Point 移除选中的控制点 Remove selected point from polygon 从多边形中移除选中的控制点 Undo 撤销 Undo last add and edit of shape 撤销最近一次添加和编辑 &Hide Shapes 隐藏图形(&H) Hide all shapes 隐藏所有图形 &Show Shapes 显示图形(&S) Show all shapes 显示所有图形 &Toggle Shapes 开关图形(&S) Toggle all shapes 开关所有图形 &Tutorial 教程[&T] Show tutorial page 显示教程网页 Zoom 缩放 Zoom in or out of the image. Also accessible with {} and {} from the canvas. 缩放图像。亦可从画布的{}和{}访问 Ctrl+Wheel Ctrl+滚轮 Zoom &In 放大(&I) Increase zoom level 增加缩放水平 &Zoom Out 缩小(&Z) Decrease zoom level 减小缩放水平 &Original size 原始大小(&O) Zoom to original size 缩放至原始大小 &Keep Previous Scale 保留最后的比例(&K) Keep previous zoom scale 保留最后的缩放比例 &Fit Window 适应窗口(&F) Zoom follows window size 跟随窗口大小缩放 Fit &Width 适应宽度(&W) Zoom follows window width 跟随窗口宽度缩放 &Brightness Contrast 亮度 对比度(&B) Adjust brightness and contrast 调节亮度和对比度 &Edit Label 编辑标签(&E) Modify the label of the selected shape 修改选中图形的标签 Fill Drawing Polygon 填充所绘多边形 Fill polygon while drawing 绘制时填充多边形 &File 文件(&F) &Edit 编辑(&E) &View 视图(&V) &Help 帮助(&H) Open &Recent 最近打开(&R) %s started. %s 启动完了 Invalid label 无效的标签 Invalid label '{}' with validation type '{}' 无效的标签'{}',验证类型'{}' Error saving label data 保存标签发生错误 <b>%s</b> <b>%s</b> Error opening file 打开文件发生错误 No such file: <b>%s</b> 文件不存在: <b>%s</b> Loading %s... 正在载入 %s... Error reading %s 打开文件发生错误 %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> lt;p>请确认<i>{0}</i>是一个合法的图像文件。<br/>支持的格式包括: {1}</p> Loaded %s 已加载 %s Image & Label files (%s) 图像和标签文件(%s) %s - Choose Image or Label file %s - 选择图像或标签文件 %s - Save/Load Annotations in Directory %s - 保存和加载批注的路径 %s . Annotations will be saved/loaded in %s %s . 批注会被加载和保存在 %s %s - Choose File %s - 选择文件 Label files (*%s) 标签文件(*%s) Choose File 选择文件 You are about to permanently delete this label file, proceed anyway? 即将永久性删除此标签文件。还要继续吗? Attention 注意 Save annotations to "{}" before closing? 关闭前保存批注到"{}"吗? Save annotations? 保存批注吗? You are about to permanently delete {} shapes, proceed anyway? 即将永久性删除图形{}。还要继续吗? %s - Open Directory %s - 打开目录 Toggle "keep previous annotation" mode 切换「保留上一个标注」模式 Keep Previous Brightness/Contrast 保留之前的亮度/对比度 Preferences… 偏好设置… Open config file in text editor 在文本编辑器中打开配置文件 No Config File 没有配置文件 Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc 配置已通过命令行以YAML表达式的形式提供。 要使用偏好设置编辑器,请使用配置文件启动Labelme: labelme --config ~/.labelmerc Configuration Errors 配置错误 Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. 加载配置时发现错误。请检查以下错误并重新加载配置,或忽略错误的行。 <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>请确认<i>%s</i>是一个合法的标签文件。</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>请确认<i>%s</i>是一个合法的图像文件。</p> Reset Layout 重置布局 ================================================ FILE: labelme/translate/zh_TW.ts ================================================ AiAssistedAnnotationWidget AI-Assisted Annotation AI輔助標註 AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes AI在'AI-Polygon'和'AI-Mask'模式下提供標註建議 Select 'AI-Polygon' or 'AI-Mask' mode to enable AI-Assisted Annotation 選擇「AI-Polygon」或「AI-Mask」模式以啟用AI輔助標註 AiTextToAnnotationWidget AI Text-to-Annotation AI提示 e.g., dog,cat,bird 例如:狗,貓,鳥 Run 執行 Score 分數 IoU IoU AI creates annotations from the text prompt AI根據文字提示創建標註 Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' mode to enable 選擇 'Polygon'、'Rectangle'、'AI-Polygon' 或 'AI-Mask' 模式以啟用 Canvas Click & drag to move point 點擊並拖拽以移動控制點 Click & drag to move shape 點擊並拖拽以移動形狀 Creating %r 正在創建 %r ESC to cancel 按 ESC 取消 Enter or Space to finalize 按 Enter 或空格鍵完成 Editing shapes 編輯形狀 Click points to include or Shift+Click to exclude for ai_polygon 點擊以包含點或 Shift+點擊以排除點(AI多邊形) Click points to include or Shift+Click to exclude for ai_mask 點擊以包含點或 Shift+點擊以排除點(AI蒙版) Click start point for line 點擊線段起點 Click end point for line 點擊線段終點 Click start point for linestrip 點擊折線起點 Click next point or finish by Ctrl/Cmd+Click for linestrip 點擊下一個點或 Ctrl/Cmd+點擊完成折線 Click center point for circle 點擊圓形中心點 Click point on circumference for circle 點擊圓周上的點 Click first corner for rectangle 點擊矩形第一個角 Click to add point 點擊以添加點 ALT + SHIFT + Click to delete point ALT + SHIFT + 點擊以刪除點 ALT + Click to create point on shape ALT + 點擊在形狀上創建點 Right-click & drag to copy shape 右鍵點擊並拖拽以複製形狀 Click opposite corner for rectangle (Shift for square) 點擊矩形對角(Shift繪製正方形) MainWindow Flags 標記 Annotation List 批註列表 Select label to start annotating for it. Press 'Esc' to deselect. 選擇標籤類型並開始以其標註。按'Esc'取消選擇。 Label List 標籤列表 Search Filename 按文件名檢索 File List 文件列表 &Quit 退出(&Q) Quit application 退出應用 &Open 打開(&O) Open image or label file 打開圖像或標籤文件 Open Dir 打開目錄 &Next Image 下一幅(&N) Open next (hold Ctl+Shift to copy labels) 打開下一幅(按 Ctrl+Shift 複製標籤) &Prev Image 上一幅(&P) Open prev (hold Ctl+Shift to copy labels) 打開上一幅(按 Ctrl+Shift 複製標籤) &Save 保存(&S) Save labels to file 保存標籤到文件 &Save As 另存為(&S) Save labels to a different file 保存標籤到不同的文件 &Delete File 刪除(&D) Delete current label file 刪除當前標籤文件 &Change Output Dir 更改輸出路徑(&C) Change where annotations are loaded/saved 更改載入、保存標註的路徑 Save &Automatically 自動保存(&A) Save automatically 自動保存 Save With Image Data 同時保存圖像數據 Save image data in label file 將圖像數據保存到標籤文件中 &Close 關閉(&C) Close current file 關閉當前文件 Keep Previous Annotation 保留上一個標註 Create Polygons 創建多邊形 Start drawing polygons 開始繪製多邊形 Create Rectangle 創建矩形 Start drawing rectangles 開始繪製矩形 Create Circle 創建圓形 Start drawing circles 開始繪製圓形 Create Line 創建直線 Start drawing lines 開始繪製直線 Create Point 創建控制點 Start drawing points 開始繪製控制點 Create LineStrip 創建折線 Start drawing linestrip. Ctrl+LeftClick ends creation. 開始繪製折線。Ctrl+單擊左鍵結束繪製。 Create AI-Polygon 創建AI多邊形 Start drawing ai_polygon. Ctrl+LeftClick ends creation. 開始繪製AI多邊形。Ctrl+單擊左鍵結束繪製。 Create AI-Mask 創建AI蒙版 Start drawing ai_mask. Ctrl+LeftClick ends creation. 開始繪製AI蒙版。Ctrl+單擊左鍵結束繪製。 Edit Shapes 編輯圖形 Move and edit the selected shapes 移動、編輯選中的圖形 Delete Shapes 刪除圖形 Delete the selected shapes 刪除選中的圖形 Duplicate Shapes 複製圖形 Create a duplicate of the selected shapes 為選中的圖形創建副本 Copy Shapes 複製圖形 Copy selected shapes to clipboard 複製選中圖形到剪貼板 Paste Shapes 粘貼圖形 Paste copied shapes 粘貼已複製的圖形 Undo last point 撤銷最後的控制點 Undo last drawn point 撤銷最後一次繪製的控制點 Remove Selected Point 移除選中的控制點 Remove selected point from polygon 從多邊形中移除選中的控制點 Undo 撤銷 Undo last add and edit of shape 撤銷最近一次添加和編輯 &Hide Shapes 隱藏圖形(&H) Hide all shapes 隱藏所有圖形 &Show Shapes 顯示圖形(&S) Show all shapes 顯示所有圖形 &Toggle Shapes 開關圖形(&S) Toggle all shapes 開關所有圖形 &Tutorial 教程[&T] Show tutorial page 顯示教程網頁 Zoom 縮放 Zoom in or out of the image. Also accessible with {} and {} from the canvas. 縮放圖像。亦可從畫布的{}和{}訪問 Ctrl+Wheel Ctrl+滾輪 Zoom &In 放大(&I) Increase zoom level 增加縮放水平 &Zoom Out 縮小(&Z) Decrease zoom level 減小縮放水平 &Original size 原始大小(&O) Zoom to original size 縮放至原始大小 &Keep Previous Scale 保留上一個比例(&K) Keep previous zoom scale 保留上一個縮放比例 &Fit Window 適應窗口(&F) Zoom follows window size 跟隨窗口大小縮放 Fit &Width 適應寬度(&W) Zoom follows window width 跟隨窗口寬度縮放 &Brightness Contrast 亮度 對比度(&B) Adjust brightness and contrast 調節亮度和對比度 &Edit Label 編輯標籤(&E) Modify the label of the selected shape 修改選中圖形的標籤 Fill Drawing Polygon 填充所繪多邊形 Fill polygon while drawing 繪製時填充多邊形 &File 文件(&F) &Edit 編輯(&E) &View 視圖(&V) &Help 幫助(&H) Open &Recent 最近打開(&R) %s started. %s 已啟動 Invalid label 無效的標籤 Invalid label '{}' with validation type '{}' 無效的標籤'{}',驗證類型'{}' Error saving label data 保存標籤發生錯誤 <b>%s</b> <b>%s</b> Error opening file 打開文件發生錯誤 No such file: <b>%s</b> 文件不存在: <b>%s</b> Loading %s... 正在載入 %s... Error reading %s 打開文件發生錯誤 %s <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> <p>請確認<i>{0}</i>是一個合法的圖像文件。<br/>支援的格式包括: {1}</p> Loaded %s 已載入 %s Image & Label files (%s) 圖像和標籤文件(%s) %s - Choose Image or Label file %s - 選擇圖像或標籤文件 %s - Save/Load Annotations in Directory %s - 保存和載入批註的路徑 %s . Annotations will be saved/loaded in %s %s . 批註會被載入和保存在 %s %s - Choose File %s - 選擇文件 Label files (*%s) 標籤文件(*%s) Choose File 選擇文件 You are about to permanently delete this label file, proceed anyway? 即將永久性刪除此標籤文件。還要繼續嗎? Attention 注意 Save annotations to "{}" before closing? 關閉前保存批註到"{}"嗎? Save annotations? 保存批註嗎? You are about to permanently delete {} shapes, proceed anyway? 即將永久性刪除圖形{}。還要繼續嗎? %s - Open Directory %s - 打開目錄 Toggle "keep previous annotation" mode 開關「保留上一個標註」模式 Keep Previous Brightness/Contrast 保留之前的亮度/對比度 Preferences… 偏好設定… Open config file in text editor 在文字編輯器中開啟設定檔 No Config File 沒有設定檔 Configuration was provided as a YAML expression via command line. To use the preferences editor, start Labelme with a config file: labelme --config ~/.labelmerc 設定已透過命令列以YAML表達式的形式提供。 要使用偏好設定編輯器,請使用設定檔啟動Labelme: labelme --config ~/.labelmerc Configuration Errors 設定錯誤 Errors were found while loading the configuration. Please review the errors below and reload your configuration or ignore the erroneous lines. 載入設定時發現錯誤。請檢查以下錯誤並重新載入設定,或忽略錯誤的行。 <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file.</p> <p><b>%s</b></p><p>請確認<i>%s</i>是一個合法的標籤文件。</p> <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid image file.</p> <p><b>%s</b></p><p>請確認<i>%s</i>是一個合法的圖像文件。</p> Reset Layout 重置佈局 ================================================ FILE: labelme/utils/__init__.py ================================================ from ._io import lblsave from .image import apply_exif_orientation from .image import img_arr_to_b64 from .image import img_arr_to_data from .image import img_b64_to_arr from .image import img_data_to_arr from .image import img_data_to_pil from .image import img_data_to_png_data from .image import img_pil_to_data from .image import img_qt_to_arr from .qt import addActions from .qt import distance from .qt import distancetoline from .qt import fmtShortcut from .qt import labelValidator from .qt import newAction from .qt import newButton from .qt import newIcon from .shape import masks_to_bboxes from .shape import shape_to_mask from .shape import shapes_to_label ================================================ FILE: labelme/utils/_io.py ================================================ # MIT License # Copyright (c) Kentaro Wada import os.path as osp import numpy as np import PIL.Image def lblsave(filename, lbl): import imgviz if osp.splitext(filename)[1] != ".png": filename += ".png" # Assume label ranses [-1, 254] for int32, # and [0, 255] for uint8 as VOC. if lbl.min() >= -1 and lbl.max() < 255: lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode="P") colormap = imgviz.label_colormap() lbl_pil.putpalette(colormap.flatten()) lbl_pil.save(filename) else: raise ValueError( f"[{filename}] Cannot save the pixel-wise class label as PNG. " "Please consider using the .npy format." ) ================================================ FILE: labelme/utils/image.py ================================================ # MIT License # Copyright (c) Kentaro Wada import base64 import io import numpy as np import PIL.ExifTags import PIL.Image import PIL.ImageOps def img_data_to_pil(img_data): f = io.BytesIO() f.write(img_data) img_pil = PIL.Image.open(f) return img_pil def img_data_to_arr(img_data): img_pil = img_data_to_pil(img_data) img_arr = np.array(img_pil) return img_arr def img_b64_to_arr(img_b64): img_data = base64.b64decode(img_b64) img_arr = img_data_to_arr(img_data) return img_arr def img_pil_to_data(img_pil): f = io.BytesIO() img_pil.save(f, format="PNG") img_data = f.getvalue() return img_data def img_arr_to_b64(img_arr): img_data = img_arr_to_data(img_arr) img_b64 = base64.b64encode(img_data).decode("utf-8") return img_b64 def img_arr_to_data(img_arr): img_pil = PIL.Image.fromarray(img_arr) img_data = img_pil_to_data(img_pil) return img_data def img_data_to_png_data(img_data): with io.BytesIO() as f: f.write(img_data) img = PIL.Image.open(f) with io.BytesIO() as f: img.save(f, "PNG") f.seek(0) return f.read() def img_qt_to_arr(img_qt): w, h, d = img_qt.size().width(), img_qt.size().height(), img_qt.depth() bytes_ = img_qt.bits().asstring(w * h * d // 8) img_arr = np.frombuffer(bytes_, dtype=np.uint8).reshape((h, w, d // 8)) return img_arr def apply_exif_orientation(image): try: exif = image._getexif() except AttributeError: exif = None if exif is None: return image exif = {PIL.ExifTags.TAGS[k]: v for k, v in exif.items() if k in PIL.ExifTags.TAGS} orientation = exif.get("Orientation", None) if orientation == 1: # do nothing return image elif orientation == 2: # left-to-right mirror return PIL.ImageOps.mirror(image) elif orientation == 3: # rotate 180 return image.transpose(PIL.Image.ROTATE_180) elif orientation == 4: # top-to-bottom mirror return PIL.ImageOps.flip(image) elif orientation == 5: # top-to-left mirror return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_270)) elif orientation == 6: # rotate 270 return image.transpose(PIL.Image.ROTATE_270) elif orientation == 7: # top-to-right mirror return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_90)) elif orientation == 8: # rotate 90 return image.transpose(PIL.Image.ROTATE_90) else: return image ================================================ FILE: labelme/utils/qt.py ================================================ import os.path as osp from math import sqrt import numpy as np from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets here = osp.dirname(osp.abspath(__file__)) def newIcon(icon_file_name: str) -> QtGui.QIcon: if osp.splitext(icon_file_name)[1] == "": icon_file_name = f"{icon_file_name}.png" # XXX: convention icons_dir: str = osp.join(here, "../icons") return QtGui.QIcon(osp.join(":/", icons_dir, icon_file_name)) def newButton(text, icon=None, slot=None): b = QtWidgets.QPushButton(text) if icon is not None: b.setIcon(newIcon(icon)) if slot is not None: b.clicked.connect(slot) return b def newAction( parent, text, slot=None, shortcut=None, icon=None, tip=None, checkable=False, enabled=True, checked=False, ): """Create a new action and assign callbacks, shortcuts, etc.""" a = QtWidgets.QAction(text, parent) if icon is not None: a.setIconText(text.replace(" ", "\n")) a.setIcon(newIcon(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) a.setChecked(checked) return a def addActions(widget, actions): for action in actions: if action is None: widget.addSeparator() elif isinstance(action, QtWidgets.QMenu): widget.addMenu(action) else: widget.addAction(action) def labelValidator(): return QtGui.QRegExpValidator(QtCore.QRegExp(r"^[^ \t].+"), None) def distance(p): return sqrt(p.x() * p.x() + p.y() * p.y()) def distancetoline(point, line): p1, p2 = line p1 = np.array([p1.x(), p1.y()]) p2 = np.array([p2.x(), p2.y()]) p3 = np.array([point.x(), point.y()]) if np.dot((p3 - p1), (p2 - p1)) < 0: return np.linalg.norm(p3 - p1) if np.dot((p3 - p2), (p1 - p2)) < 0: return np.linalg.norm(p3 - p2) d = p2 - p1 if np.linalg.norm(d) == 0: return np.linalg.norm(p3 - p1) v = p1 - p3 cross = d[0] * v[1] - d[1] * v[0] return abs(cross) / np.linalg.norm(d) def fmtShortcut(text): mod, key = text.split("+", 1) return f"{mod}+{key}" ================================================ FILE: labelme/utils/shape.py ================================================ # MIT License # Copyright (c) Kentaro Wada import math import uuid import numpy as np import PIL.Image import PIL.ImageDraw from numpy.typing import NDArray def shape_to_mask( img_shape: tuple[int, ...], points: list[list[float]], shape_type: str | None = None, line_width: int = 10, point_size: int = 5, ) -> NDArray[np.bool_]: mask = PIL.Image.fromarray(np.zeros(img_shape[:2], dtype=np.uint8)) draw = PIL.ImageDraw.Draw(mask) xy = [tuple(point) for point in points] if shape_type == "circle": assert len(xy) == 2, "Shape of shape_type=circle must have 2 points" (cx, cy), (px, py) = xy d = math.sqrt((cx - px) ** 2 + (cy - py) ** 2) draw.ellipse([cx - d, cy - d, cx + d, cy + d], outline=1, fill=1) elif shape_type == "rectangle": assert len(xy) == 2, "Shape of shape_type=rectangle must have 2 points" (x0, y0), (x1, y1) = xy draw.rectangle( ((min(x0, x1), min(y0, y1)), (max(x0, x1), max(y0, y1))), outline=1, fill=1, ) elif shape_type == "line": assert len(xy) == 2, "Shape of shape_type=line must have 2 points" draw.line(xy=xy, fill=1, width=line_width) # type: ignore[arg-type] elif shape_type == "linestrip": draw.line(xy=xy, fill=1, width=line_width) # type: ignore[arg-type] elif shape_type == "point": assert len(xy) == 1, "Shape of shape_type=point must have 1 points" cx, cy = xy[0] r = point_size draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=1, fill=1) elif shape_type in [None, "polygon"]: assert len(xy) > 2, "Polygon must have points more than 2" draw.polygon(xy=xy, outline=1, fill=1) # type: ignore[arg-type] else: raise ValueError(f"shape_type={shape_type!r} is not supported.") return np.array(mask, dtype=bool) def shapes_to_label(img_shape, shapes, label_name_to_value): cls = np.zeros(img_shape[:2], dtype=np.int32) ins = np.zeros_like(cls) instances = [] for shape in shapes: points = shape["points"] label = shape["label"] group_id = shape.get("group_id") if group_id is None: group_id = uuid.uuid1() shape_type = shape.get("shape_type", None) cls_name = label instance = (cls_name, group_id) if instance not in instances: instances.append(instance) ins_id = instances.index(instance) + 1 cls_id = label_name_to_value[cls_name] mask: NDArray[np.bool_] if shape_type == "mask": if not isinstance(shape["mask"], np.ndarray): raise ValueError("shape['mask'] must be numpy.ndarray") mask = np.zeros(img_shape[:2], dtype=bool) (x1, y1), (x2, y2) = np.asarray(points).astype(int) mask[y1 : y2 + 1, x1 : x2 + 1] = shape["mask"] else: mask = shape_to_mask(img_shape[:2], points, shape_type) cls[mask] = cls_id ins[mask] = ins_id return cls, ins def masks_to_bboxes(masks): if masks.ndim != 3: raise ValueError(f"masks.ndim must be 3, but it is {masks.ndim}") if masks.dtype != bool: raise ValueError(f"masks.dtype must be bool type, but it is {masks.dtype}") bboxes = [] for mask in masks: where = np.argwhere(mask) (y1, x1), (y2, x2) = where.min(0), where.max(0) + 1 bboxes.append((y1, x1, y2, x2)) return np.asarray(bboxes, dtype=np.float32) ================================================ FILE: labelme/widgets/__init__.py ================================================ from ._ai_assisted_annotation_widget import AiAssistedAnnotationWidget from ._ai_text_to_annotation_widget import AiTextToAnnotationWidget from ._status import StatusStats from .brightness_contrast_dialog import BrightnessContrastDialog from .canvas import Canvas from .download import download_ai_model from .file_dialog_preview import FileDialogPreview from .label_dialog import LabelDialog from .label_dialog import LabelQLineEdit from .label_list_widget import LabelListWidget from .label_list_widget import LabelListWidgetItem from .tool_bar import ToolBar from .unique_label_qlist_widget import UniqueLabelQListWidget from .zoom_widget import ZoomWidget ================================================ FILE: labelme/widgets/_ai_assisted_annotation_widget.py ================================================ from collections.abc import Callable from loguru import logger from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from ._info_button import InfoButton class AiAssistedAnnotationWidget(QtWidgets.QWidget): _available_models: list[tuple[str, str]] = [ ("efficientsam:10m", "EfficientSam (speed)"), ("efficientsam:latest", "EfficientSam (accuracy)"), ("sam:100m", "Sam (speed)"), ("sam:300m", "Sam (balanced)"), ("sam:latest", "Sam (accuracy)"), ("sam2:small", "Sam2 (speed)"), ("sam2:latest", "Sam2 (balanced)"), ("sam2:large", "Sam2 (accuracy)"), ] _model_combo: QtWidgets.QComboBox _body: QtWidgets.QWidget def __init__( self, default_model: str, on_model_changed: Callable[[str], None], parent: QtWidgets.QWidget | None = None, ): super().__init__(parent=parent) self._init_ui(default_model=default_model, on_model_changed=on_model_changed) def _init_ui( self, default_model: str, on_model_changed: Callable[[str], None] ) -> None: layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(4, 4, 4, 4) layout.setSpacing(2) self.setLayout(layout) header_layout = QtWidgets.QHBoxLayout() header_layout.addStretch() label = QtWidgets.QLabel(self.tr("AI-Assisted Annotation")) header_layout.addWidget(label) info_button = InfoButton( tooltip=self.tr( "AI suggests annotation in 'AI-Polygon' and 'AI-Mask' modes" ) ) header_layout.addWidget(info_button) header_layout.addStretch() layout.addLayout(header_layout) self._body = body = QtWidgets.QWidget() body.installEventFilter(self) body_layout = QtWidgets.QVBoxLayout() body_layout.setContentsMargins(0, 0, 0, 0) body_layout.setSpacing(0) body.setLayout(body_layout) self._model_combo = QtWidgets.QComboBox() for model_id, model_display in self._available_models: self._model_combo.addItem(model_display, model_id) body_layout.addWidget(self._model_combo) layout.addWidget(body) model_ui_names = [model_display for _, model_display in self._available_models] if default_model in model_ui_names: model_index = model_ui_names.index(default_model) else: logger.warning("Default AI model is not found: {!r}", default_model) model_index = 0 self._model_combo.currentIndexChanged.connect( lambda index: on_model_changed(self._model_combo.itemData(index)) ) self._model_combo.setCurrentIndex(model_index) self.setMaximumWidth(200) def setEnabled(self, a0: bool) -> None: self._body.setEnabled(a0) def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: if a0 == self._body and not self._body.isEnabled(): if a1.type() == QtCore.QEvent.Enter: QtWidgets.QToolTip.showText( QtGui.QCursor.pos(), self.tr( "Select 'AI-Polygon' or 'AI-Mask' mode " "to enable AI-Assisted Annotation" ), self._body, ) return super().eventFilter(a0, a1) ================================================ FILE: labelme/widgets/_ai_text_to_annotation_widget.py ================================================ from collections.abc import Callable from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from ._info_button import InfoButton class AiTextToAnnotationWidget(QtWidgets.QWidget): _available_models: list[tuple[str, str]] = [ ("sam3:latest", "SAM3 (smart)"), ("yoloworld:latest", "YOLO-World (fast)"), ] _default_model_name: str = "yoloworld:latest" _default_score_threshold: float = 0.1 _default_iou_threshold: float = 0.5 _text_input: QtWidgets.QLineEdit _model_combo: QtWidgets.QComboBox _score_spinbox: QtWidgets.QDoubleSpinBox _iou_spinbox: QtWidgets.QDoubleSpinBox _body: QtWidgets.QWidget def __init__(self, on_submit, parent: QtWidgets.QWidget | None = None): super().__init__(parent=parent) self._init_ui(on_submit) def _init_ui(self, on_submit: Callable[[], None]) -> None: layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(4, 4, 4, 4) layout.setSpacing(2) self.setLayout(layout) header_layout = QtWidgets.QHBoxLayout() header_layout.addStretch() label = QtWidgets.QLabel(self.tr("AI Text-to-Annotation")) header_layout.addWidget(label) info_button = InfoButton( tooltip=self.tr("AI creates annotations from the text prompt") ) header_layout.addWidget(info_button) header_layout.addStretch() layout.addLayout(header_layout) self._body = body = QtWidgets.QWidget() body.installEventFilter(self) body_layout = QtWidgets.QVBoxLayout() body_layout.setContentsMargins(0, 0, 0, 0) body_layout.setSpacing(0) body.setLayout(body_layout) grid = QtWidgets.QGridLayout() grid.setContentsMargins(0, 0, 0, 0) grid.setSpacing(2) text_input = QtWidgets.QLineEdit() text_input.setPlaceholderText(self.tr("e.g., dog,cat,bird")) text_input.setFixedHeight(24) grid.addWidget(text_input, 0, 0) self._text_input = text_input run_button = QtWidgets.QToolButton() run_button.setText(self.tr("Run")) run_button.setFixedHeight(24) run_button.setCursor(QtCore.Qt.PointingHandCursor) run_button.clicked.connect(on_submit) grid.addWidget(run_button, 0, 1) settings_layout = QtWidgets.QHBoxLayout() settings_layout.setContentsMargins(0, 0, 0, 0) settings_layout.setSpacing(4) self._model_combo = model_combo = QtWidgets.QComboBox() for model_id, model_display in self._available_models: model_combo.addItem(model_display, model_id) model_index = next( ( i for i, (mid, _) in enumerate(self._available_models) if mid == self._default_model_name ), 0, ) model_combo.setCurrentIndex(model_index) settings_layout.addWidget(model_combo, stretch=1) score_label = QtWidgets.QLabel(self.tr("Score")) score_label.setStyleSheet("color: gray; font-size: 10px;") settings_layout.addWidget(score_label) # self._score_spinbox = score_spinbox = QtWidgets.QDoubleSpinBox() score_spinbox.setStyleSheet("font-size: 10px;") score_spinbox.setFixedWidth(50) score_spinbox.setRange(0, 1) score_spinbox.setSingleStep(0.05) score_spinbox.setValue(self._default_score_threshold) settings_layout.addWidget(score_spinbox) iou_label = QtWidgets.QLabel(self.tr("IoU")) iou_label.setStyleSheet("color: gray; font-size: 10px;") settings_layout.addWidget(iou_label) # self._iou_spinbox = iou_spinbox = QtWidgets.QDoubleSpinBox() iou_spinbox.setStyleSheet("font-size: 10px;") iou_spinbox.setFixedWidth(50) iou_spinbox.setRange(0, 1) iou_spinbox.setSingleStep(0.05) iou_spinbox.setValue(self._default_iou_threshold) settings_layout.addWidget(iou_spinbox) grid.addLayout(settings_layout, 1, 0, 1, 2) body_layout.addLayout(grid) layout.addWidget(body) self.setMaximumWidth(320) def get_text_prompt(self) -> str: return self._text_input.text() def get_model_name(self) -> str: return self._model_combo.currentData() def get_score_threshold(self) -> float: return self._score_spinbox.value() def get_iou_threshold(self) -> float: return self._iou_spinbox.value() def setEnabled(self, a0: bool) -> None: self._body.setEnabled(a0) def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: if a0 == self._body and not self._body.isEnabled(): if a1.type() == QtCore.QEvent.Enter: QtWidgets.QToolTip.showText( QtGui.QCursor.pos(), self.tr( "Select 'Polygon', 'Rectangle', 'AI-Polygon', or 'AI-Mask' " "mode to enable" ), self._body, ) return super().eventFilter(a0, a1) ================================================ FILE: labelme/widgets/_info_button.py ================================================ from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from labelme.utils.qt import newIcon class InfoButton(QtWidgets.QToolButton): def __init__(self, tooltip: str, parent: QtWidgets.QWidget | None = None): super().__init__(parent=parent) self.setIcon(newIcon("info.svg")) self.setIconSize(QtCore.QSize(16, 16)) self.setStyleSheet( """ QToolButton { border: none; border-radius: 8px; padding: 0px; } QToolButton:hover { background-color: rgba(0, 0, 0, 0.1); } """ ) self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.setToolTip(tooltip) def enterEvent(self, a0: QtCore.QEvent) -> None: super().enterEvent(a0) QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), self.toolTip()) ================================================ FILE: labelme/widgets/_status.py ================================================ from PyQt5 import QtGui from PyQt5 import QtWidgets class StatusStats(QtWidgets.QLabel): def __init__(self): super().__init__("") font = QtGui.QFont() font.setFamily("monospace") font.setStyleHint(QtGui.QFont.Monospace) self.setFont(font) ================================================ FILE: labelme/widgets/brightness_contrast_dialog.py ================================================ import PIL.Image import PIL.ImageEnhance from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from PyQt5.QtGui import QImage class BrightnessContrastDialog(QtWidgets.QDialog): _base_value = 50 img: PIL.Image.Image def __init__(self, img: PIL.Image.Image, callback, parent=None): super().__init__(parent) self.setModal(True) self.setWindowTitle("Brightness/Contrast") sliders = {} layouts = {} for title in ["Brightness:", "Contrast:"]: layout = QtWidgets.QHBoxLayout() title_label = QtWidgets.QLabel(self.tr(title)) title_label.setFixedWidth(75) layout.addWidget(title_label) # slider = QtWidgets.QSlider(Qt.Horizontal) slider.setRange(0, 3 * self._base_value) slider.setValue(self._base_value) layout.addWidget(slider) # value_label = QtWidgets.QLabel(f"{slider.value() / self._base_value:.2f}") value_label.setAlignment(Qt.AlignRight) layout.addWidget(value_label) # slider.valueChanged.connect(self.onNewValue) slider.valueChanged.connect( lambda _, value_label_=value_label, slider_=slider: value_label_.setText( f"{slider_.value() / self._base_value:.2f}" ) ) layouts[title] = layout sliders[title] = slider self.slider_brightness = sliders["Brightness:"] self.slider_contrast = sliders["Contrast:"] del sliders v_layout = QtWidgets.QVBoxLayout() v_layout.addLayout(layouts["Brightness:"]) v_layout.addLayout(layouts["Contrast:"]) del layouts self.setLayout(v_layout) self._alpha = None if "A" in img.getbands(): self._alpha = img.getchannel("A") if img.mode != "RGB": img = img.convert("RGB") self.img = img self.callback = callback def onNewValue(self, _): brightness = self.slider_brightness.value() / self._base_value contrast = self.slider_contrast.value() / self._base_value img: PIL.Image.Image = self.img if brightness != 1: img = PIL.ImageEnhance.Brightness(img).enhance(brightness) if contrast != 1: img = PIL.ImageEnhance.Contrast(img).enhance(contrast) fmt: QImage.Format if self._alpha is None: fmt = QImage.Format_RGB888 else: img = img.convert("RGBA") img.putalpha(self._alpha) fmt = QImage.Format_RGBA8888 qimage = QImage( img.tobytes(), img.width, img.height, img.width * len(img.getbands()), fmt ) self.callback(qimage) ================================================ FILE: labelme/widgets/canvas.py ================================================ from __future__ import annotations import enum from typing import Literal import imgviz import numpy as np import osam from loguru import logger from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from PyQt5.QtCore import QPoint from PyQt5.QtCore import QPointF from PyQt5.QtCore import Qt import labelme.utils from labelme._automation import OsamSession from labelme._automation import polygon_from_mask from labelme.shape import Shape from .download import download_ai_model CURSOR_DEFAULT = Qt.ArrowCursor CURSOR_POINT = Qt.PointingHandCursor CURSOR_DRAW = Qt.CrossCursor CURSOR_MOVE = Qt.ClosedHandCursor CURSOR_GRAB = Qt.OpenHandCursor MOVE_SPEED: float = 5.0 class CanvasMode(enum.Enum): CREATE = enum.auto() EDIT = enum.auto() class Canvas(QtWidgets.QWidget): pixmap: QtGui.QPixmap _pixmap_hash: int | None _cursor: QtCore.Qt.CursorShape shapes: list[Shape] shapesBackups: list[list[Shape]] movingShape: bool selectedShapes: list[Shape] selectedShapesCopy: list[Shape] current: Shape | None hShape: Shape | None _lasthShape: Shape | None hVertex: int | None _lasthVertex: int | None hEdge: int | None _lasthEdge: int | None zoomRequest = QtCore.pyqtSignal(int, QPointF) scrollRequest = QtCore.pyqtSignal(int, int) newShape = QtCore.pyqtSignal() selectionChanged = QtCore.pyqtSignal(list) shapeMoved = QtCore.pyqtSignal() drawingPolygon = QtCore.pyqtSignal(bool) vertexSelected = QtCore.pyqtSignal(bool) mouseMoved = QtCore.pyqtSignal(QPointF) statusUpdated = QtCore.pyqtSignal(str) mode: CanvasMode = CanvasMode.EDIT # polygon, rectangle, line, or point _createMode = "polygon" _fill_drawing = False prevPoint: QPointF prevMovePoint: QPointF offsets: tuple[QPointF, QPointF] _dragging_start_pos: QPointF _is_dragging: bool _is_dragging_enabled: bool _osam_session_model_name: str = "sam2:latest" _osam_session: OsamSession | None def __init__(self, *args, **kwargs): self.epsilon: float = kwargs.pop("epsilon", 10.0) self.double_click = kwargs.pop("double_click", "close") if self.double_click not in [None, "close"]: raise ValueError( f"Unexpected value for double_click event: {self.double_click}" ) self.num_backups = kwargs.pop("num_backups", 10) self._crosshair = kwargs.pop( "crosshair", { "polygon": False, "rectangle": True, "circle": False, "line": False, "point": False, "linestrip": False, "ai_polygon": False, "ai_mask": False, }, ) super().__init__(*args, **kwargs) self.resetState() # self.line represents: # - createMode == 'polygon': edge from last point to current # - createMode == 'rectangle': diagonal line of the rectangle # - createMode == 'line': the line # - createMode == 'point': the point self.line = Shape() self.prevPoint = QPointF() self.prevMovePoint = QPointF() self.offsets = QPointF(), QPointF() self.scale: float = 1.0 self._osam_session = None self.visible: dict = {} self._hideBackround: bool = False self.hideBackround: bool = False self.snapping = True self.hShapeIsSelected: bool = False self._painter = QtGui.QPainter() self._dragging_start_pos = QPointF() self._is_dragging = False self._is_dragging_enabled = False # Menus: # 0: right-click without selection and dragging of shapes # 1: right-click with selection and dragging of shapes self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu()) self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) def fillDrawing(self): return self._fill_drawing def setFillDrawing(self, value): self._fill_drawing = value @property def createMode(self): return self._createMode @createMode.setter def createMode(self, value): if value not in [ "polygon", "rectangle", "circle", "line", "point", "linestrip", "ai_polygon", "ai_mask", ]: raise ValueError(f"Unsupported createMode: {value}") self._createMode = value def set_ai_model_name(self, model_name: str) -> None: self._osam_session_model_name = model_name def _get_osam_session(self) -> OsamSession: if ( self._osam_session is None or self._osam_session.model_name != self._osam_session_model_name ): self._osam_session = OsamSession(model_name=self._osam_session_model_name) return self._osam_session def _update_shape_with_ai( self, points: list[QPointF], point_labels: list[int], shape: Shape ) -> None: image: np.ndarray = labelme.utils.img_qt_to_arr(img_qt=self.pixmap.toImage()) response: osam.types.GenerateResponse = self._get_osam_session().run( image=imgviz.asrgb(image), image_id=str(self._pixmap_hash), points=np.array([[p.x(), p.y()] for p in points]), point_labels=np.array(point_labels), ) _update_shape_with_ai_response( response=response, shape=shape, createMode=self.createMode, ) def storeShapes(self): shapesBackup = [] for shape in self.shapes: shapesBackup.append(shape.copy()) if len(self.shapesBackups) > self.num_backups: self.shapesBackups = self.shapesBackups[-self.num_backups - 1 :] self.shapesBackups.append(shapesBackup) @property def isShapeRestorable(self): # We save the state AFTER each edit (not before) so for an # edit to be undoable, we expect the CURRENT and the PREVIOUS state # to be in the undo stack. if len(self.shapesBackups) < 2: return False return True def restoreShape(self): # This does _part_ of the job of restoring shapes. # The complete process is also done in app.py::undoShapeEdit # and app.py::loadShapes and our own Canvas::loadShapes function. if not self.isShapeRestorable: return self.shapesBackups.pop() # latest # The application will eventually call Canvas.loadShapes which will # push this right back onto the stack. shapesBackup = self.shapesBackups.pop() self.shapes = shapesBackup self.selectedShapes = [] for shape in self.shapes: shape.selected = False self.update() def enterEvent(self, a0: QtCore.QEvent) -> None: self.overrideCursor(self._cursor) self._update_status() def leaveEvent(self, a0: QtCore.QEvent) -> None: if self._set_highlight(hShape=None, hEdge=None, hVertex=None): self.update() self.restoreCursor() self._update_status() def focusOutEvent(self, a0: QtGui.QFocusEvent) -> None: self.restoreCursor() self._update_status() def isVisible(self, shape: Shape) -> bool: # type: ignore[override] return self.visible.get(shape, True) def drawing(self) -> bool: return self.mode == CanvasMode.CREATE def editing(self) -> bool: return self.mode == CanvasMode.EDIT def setEditing(self, value=True): self.mode = CanvasMode.EDIT if value else CanvasMode.CREATE if self.mode == CanvasMode.EDIT: # CREATE -> EDIT self.repaint() # clear crosshair else: # EDIT -> CREATE need_update: bool = self._set_highlight( hShape=None, hEdge=None, hVertex=None ) need_update |= self.deSelectShape() if need_update: self.update() def _set_highlight( self, hShape: Shape | None, hEdge: int | None, hVertex: int | None ) -> bool: need_update: bool = hShape is not None if self.hShape: self.hShape.highlightClear() need_update = True # NOTE: Store last highlighted for adding/removing points. self._lasthShape = self.hShape if hShape is None else hShape self._lasthVertex = self.hVertex if hVertex is None else hVertex self._lasthEdge = self.hEdge if hEdge is None else hEdge self.hShape = hShape self.hVertex = hVertex self.hEdge = hEdge return need_update def selectedVertex(self) -> bool: return self.hVertex is not None def selectedEdge(self) -> bool: return self.hEdge is not None def _update_status(self, extra_messages: list[str] | None = None) -> None: messages: list[str] = [] if self.drawing(): messages.append(self.tr("Creating %r") % self.createMode) messages.append(self._get_create_mode_message()) if self.current: messages.append(self.tr("ESC to cancel")) if self.canCloseShape(): messages.append(self.tr("Enter or Space to finalize")) else: assert self.editing() messages.append(self.tr("Editing shapes")) if extra_messages: messages.extend(extra_messages) self.statusUpdated.emit(" • ".join(messages)) def _get_create_mode_message(self) -> str: assert self.drawing() isNew: bool = self.current is None if self.createMode == "ai_polygon": return self.tr( "Click points to include or Shift+Click to exclude for ai_polygon" ) if self.createMode == "ai_mask": return self.tr( "Click points to include or Shift+Click to exclude for ai_mask" ) if self.createMode == "line": if isNew: return self.tr("Click start point for line") else: return self.tr("Click end point for line") if self.createMode == "linestrip": if isNew: return self.tr("Click start point for linestrip") else: return self.tr( "Click next point or finish by Ctrl/Cmd+Click for linestrip" ) if self.createMode == "circle": if isNew: return self.tr("Click center point for circle") else: return self.tr("Click point on circumference for circle") if self.createMode == "rectangle": if isNew: return self.tr("Click first corner for rectangle") else: return self.tr("Click opposite corner for rectangle (Shift for square)") return self.tr("Click to add point") def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None: """Update line with last point and current coordinates.""" try: pos = self.transformPos(a0.localPos()) except AttributeError: return self.mouseMoved.emit(pos) self.prevMovePoint = pos is_shift_pressed = a0.modifiers() & Qt.ShiftModifier if self._is_dragging: self.overrideCursor(CURSOR_GRAB) delta: QPointF = pos - self._dragging_start_pos self.scrollRequest.emit(int(delta.x()), Qt.Horizontal) self.scrollRequest.emit(int(delta.y()), Qt.Vertical) return # Polygon drawing. if self.drawing(): if self.createMode in ["ai_polygon", "ai_mask"]: self.line.shape_type = "points" else: self.line.shape_type = self.createMode self.overrideCursor(CURSOR_DRAW) if not self.current: self.repaint() # draw crosshair self._update_status() return if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif ( self.snapping and len(self.current) > 1 and self.createMode == "polygon" and self.closeEnough(pos, self.current[0]) ): # Attract line to starting point and # colorise to alert the user. pos = self.current[0] self.overrideCursor(CURSOR_POINT) self.current.highlightVertex(0, Shape.NEAR_VERTEX) if self.createMode in ["polygon", "linestrip"]: self.line.points = [self.current[-1], pos] self.line.point_labels = [1, 1] elif self.createMode in ["ai_polygon", "ai_mask"]: self.line.points = [self.current.points[-1], pos] self.line.point_labels = [ self.current.point_labels[-1], 0 if is_shift_pressed else 1, ] elif self.createMode == "rectangle": if is_shift_pressed: self.prevMovePoint = pos = _snap_cursor_pos_for_square( # override pos=pos, opposite_vertex=self.current[0] ) self.line.points = [self.current[0], pos] self.line.point_labels = [1, 1] self.line.close() elif self.createMode == "circle": self.line.points = [self.current[0], pos] self.line.point_labels = [1, 1] self.line.shape_type = "circle" elif self.createMode == "line": self.line.points = [self.current[0], pos] self.line.point_labels = [1, 1] self.line.close() elif self.createMode == "point": self.line.points = [self.current[0]] self.line.point_labels = [1] self.line.close() assert len(self.line.points) == len(self.line.point_labels) self.repaint() self.current.highlightClear() self._update_status() return # Polygon copy moving. if Qt.RightButton & a0.buttons(): if self.selectedShapesCopy and self.prevPoint is not None: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShapes(self.selectedShapesCopy, pos) self.repaint() elif self.selectedShapes: self.selectedShapesCopy = [s.copy() for s in self.selectedShapes] self.repaint() self._update_status() return # Polygon/Vertex moving. if Qt.LeftButton & a0.buttons(): if self.selectedVertex(): self.boundedMoveVertex(pos, is_shift_pressed=is_shift_pressed) self.repaint() self.movingShape = True elif self.selectedShapes and self.prevPoint is not None: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShapes(self.selectedShapes, pos) self.repaint() self.movingShape = True return # Just hovering over the canvas, 2 possibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. status_messages: list[str] = [] self._highlight_hover_shape(pos=pos, status_messages=status_messages) self.vertexSelected.emit(self.hVertex is not None) self._update_status(extra_messages=status_messages) def _highlight_hover_shape(self, pos: QPointF, status_messages: list[str]) -> None: ordered_shapes: list[Shape] = ([self.hShape] if self.hShape else []) + [ s for s in reversed(self.shapes) if self.isVisible(s) and s != self.hShape ] for shape in ordered_shapes: index: int | None = shape.nearestVertex(pos, self.epsilon) if index is not None: self._set_highlight(hShape=shape, hEdge=None, hVertex=index) shape.highlightVertex(index, shape.MOVE_VERTEX) self.overrideCursor(CURSOR_POINT) status_messages.append(self.tr("Click & drag to move point")) if shape.canRemovePoint(): status_messages.append( self.tr("ALT + SHIFT + Click to delete point") ) self.update() return for shape in ordered_shapes: index_edge: int | None = shape.nearestEdge(pos, self.epsilon) if index_edge is not None and shape.canAddPoint(): self._set_highlight(hShape=shape, hEdge=index_edge, hVertex=None) self.overrideCursor(CURSOR_POINT) status_messages.append(self.tr("ALT + Click to create point on shape")) self.update() return for shape in ordered_shapes: if shape.containsPoint(pos): self._set_highlight(hShape=shape, hEdge=None, hVertex=None) status_messages.extend( [ self.tr("Click & drag to move shape"), self.tr("Right-click & drag to copy shape"), ] ) self.overrideCursor(CURSOR_GRAB) self.update() return self.restoreCursor() if self._set_highlight(hShape=None, hEdge=None, hVertex=None): self.update() def addPointToEdge(self): shape = self._lasthShape index = self._lasthEdge point = self.prevMovePoint if shape is None or index is None or point is None: return shape.insertPoint(index, point) shape.highlightVertex(index, shape.MOVE_VERTEX) self.hShape = shape self.hVertex = index self.hEdge = None self.movingShape = True def removeSelectedPoint(self): shape = self._lasthShape index = self._lasthVertex if shape is None or index is None: return shape.removePoint(index) shape.highlightClear() self.hShape = shape self._lasthVertex = None self.movingShape = True # Save changes def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: pos: QPointF = self.transformPos(a0.localPos()) is_shift_pressed = a0.modifiers() & Qt.ShiftModifier if a0.button() == Qt.LeftButton: if self.drawing(): if self.current: # Add point to existing shape. if self.createMode == "polygon": self.current.addPoint(self.line[1]) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalise() elif self.createMode in ["rectangle", "circle", "line"]: assert len(self.current.points) == 1 self.current.points = self.line.points self.finalise() elif self.createMode == "linestrip": self.current.addPoint(self.line[1]) self.line[0] = self.current[-1] if int(a0.modifiers()) == Qt.ControlModifier: self.finalise() elif self.createMode in ["ai_polygon", "ai_mask"]: self.current.addPoint( self.line.points[1], label=self.line.point_labels[1], ) self.line.points[0] = self.current.points[-1] self.line.point_labels[0] = self.current.point_labels[-1] if a0.modifiers() & Qt.ControlModifier: self.finalise() elif not self.outOfPixmap(pos): if self.createMode in ["ai_polygon", "ai_mask"]: if not download_ai_model( model_name=self._osam_session_model_name, parent=self ): return # Create new shape. self.current = Shape( shape_type="points" if self.createMode in ["ai_polygon", "ai_mask"] else self.createMode ) self.current.addPoint(pos, label=0 if is_shift_pressed else 1) if self.createMode == "point": self.finalise() elif ( self.createMode in ["ai_polygon", "ai_mask"] and a0.modifiers() & Qt.ControlModifier ): self.finalise() else: if self.createMode == "circle": self.current.shape_type = "circle" self.line.points = [pos, pos] if ( self.createMode in ["ai_polygon", "ai_mask"] and is_shift_pressed ): self.line.point_labels = [0, 0] else: self.line.point_labels = [1, 1] self.setHiding() self.drawingPolygon.emit(True) self.update() elif self.editing(): if self.selectedEdge() and a0.modifiers() == Qt.AltModifier: self.addPointToEdge() elif self.selectedVertex() and a0.modifiers() == ( Qt.AltModifier | Qt.ShiftModifier ): self.removeSelectedPoint() group_mode = int(a0.modifiers()) == Qt.ControlModifier self.selectShapePoint(pos, multiple_selection_mode=group_mode) self.prevPoint = pos self.repaint() elif a0.button() == Qt.RightButton and self.editing(): group_mode = int(a0.modifiers()) == Qt.ControlModifier if not self.selectedShapes or ( self.hShape is not None and self.hShape not in self.selectedShapes ): self.selectShapePoint(pos, multiple_selection_mode=group_mode) self.repaint() self.prevPoint = pos elif a0.button() == Qt.MiddleButton and self._is_dragging_enabled: self.overrideCursor(CURSOR_GRAB) self._dragging_start_pos = pos self._is_dragging = True self._update_status() def mouseReleaseEvent(self, a0: QtGui.QMouseEvent) -> None: if a0.button() == Qt.RightButton: menu = self.menus[len(self.selectedShapesCopy) > 0] self.restoreCursor() if not menu.exec_(self.mapToGlobal(a0.pos())) and self.selectedShapesCopy: # type: ignore # Cancel the move by deleting the shadow copy. self.selectedShapesCopy = [] self.repaint() elif a0.button() == Qt.LeftButton: if self.editing(): if ( self.hShape is not None and self.hShapeIsSelected and not self.movingShape ): self.selectionChanged.emit( [x for x in self.selectedShapes if x != self.hShape] ) elif a0.button() == Qt.MiddleButton: self._is_dragging = False self.restoreCursor() if self.movingShape and self.hShape and self.hShape in self.shapes: index = self.shapes.index(self.hShape) if self.shapesBackups[-1][index].points != self.shapes[index].points: self.storeShapes() self.shapeMoved.emit() self.movingShape = False self._update_status() def endMove(self, copy): assert self.selectedShapes and self.selectedShapesCopy assert len(self.selectedShapesCopy) == len(self.selectedShapes) if copy: for i, shape in enumerate(self.selectedShapesCopy): self.shapes.append(shape) self.selectedShapes[i].selected = False self.selectedShapes[i] = shape else: for i, shape in enumerate(self.selectedShapesCopy): self.selectedShapes[i].points = shape.points self.selectedShapesCopy = [] self.repaint() self.storeShapes() return True def hideBackroundShapes(self, value): self.hideBackround = value if self.selectedShapes: # Only hide other shapes if there is a current selection. # Otherwise the user will not be able to select a shape. self.setHiding(True) self.update() def setHiding(self, enable=True): self._hideBackround = self.hideBackround if enable else False def canCloseShape(self) -> bool: if not self.drawing(): return False if not self.current: return False if self.createMode in ["ai_polygon", "ai_mask"]: return True if self.createMode == "linestrip": return len(self.current) >= 2 return len(self.current) >= 3 def mouseDoubleClickEvent(self, a0: QtGui.QMouseEvent) -> None: if self.double_click != "close": return if self.canCloseShape(): self.finalise() def selectShapes(self, shapes): self.setHiding() self.selectionChanged.emit(shapes) self.update() def selectShapePoint(self, point, multiple_selection_mode): """Select the first shape created which contains this point.""" if self.hVertex is not None: assert self.hShape is not None self.hShape.highlightVertex(i=self.hVertex, action=self.hShape.MOVE_VERTEX) else: shape: Shape for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): self.setHiding() if shape not in self.selectedShapes: if multiple_selection_mode: self.selectionChanged.emit(self.selectedShapes + [shape]) else: self.selectionChanged.emit([shape]) self.hShapeIsSelected = False else: self.hShapeIsSelected = True self.calculateOffsets(point) return if self.deSelectShape(): self.update() def calculateOffsets(self, point: QPointF) -> None: left = self.pixmap.width() - 1 right = 0 top = self.pixmap.height() - 1 bottom = 0 for s in self.selectedShapes: rect = s.boundingRect() if rect.left() < left: left = rect.left() if rect.right() > right: right = rect.right() if rect.top() < top: top = rect.top() if rect.bottom() > bottom: bottom = rect.bottom() x1 = left - point.x() y1 = top - point.y() x2 = right - point.x() y2 = bottom - point.y() self.offsets = QPointF(x1, y1), QPointF(x2, y2) def boundedMoveVertex(self, pos: QPointF, is_shift_pressed: bool) -> None: if self.hVertex is None: logger.warning("hVertex is None, so cannot move vertex: pos={!r}", pos) return assert self.hShape is not None if self.hVertex >= len(self.hShape.points): logger.warning( "hVertex is out of range: hVertex={:d}, len(points)={:d}", self.hVertex, len(self.hShape.points), ) return if self.outOfPixmap(pos): pos = self.intersectionPoint(self.hShape[self.hVertex], pos) if is_shift_pressed and self.hShape.shape_type == "rectangle": pos = _snap_cursor_pos_for_square( pos=pos, opposite_vertex=self.hShape[1 - self.hVertex] ) self.hShape.moveVertex(i=self.hVertex, pos=pos) def boundedMoveShapes(self, shapes, pos): if self.outOfPixmap(pos): return False # No need to move o1 = pos + self.offsets[0] if self.outOfPixmap(o1): pos -= QPointF(min(0, o1.x()), min(0, o1.y())) o2 = pos + self.offsets[1] if self.outOfPixmap(o2): pos += QPointF( min(0, self.pixmap.width() - o2.x()), min(0, self.pixmap.height() - o2.y()), ) # XXX: 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. # self.calculateOffsets(self.selectedShapes, pos) dp = pos - self.prevPoint if dp: for shape in shapes: shape.moveBy(dp) self.prevPoint = pos return True return False def deSelectShape(self) -> bool: need_update: bool = False if self.selectedShapes: self.setHiding(False) self.selectionChanged.emit([]) self.hShapeIsSelected = False need_update = True return need_update def deleteSelected(self): deleted_shapes = [] if self.selectedShapes: for shape in self.selectedShapes: self.shapes.remove(shape) deleted_shapes.append(shape) self.storeShapes() self.selectedShapes = [] self.update() return deleted_shapes def deleteShape(self, shape): if shape in self.selectedShapes: self.selectedShapes.remove(shape) if shape in self.shapes: self.shapes.remove(shape) self.storeShapes() self.update() def paintEvent(self, a0: QtGui.QPaintEvent) -> None: if not self.pixmap: return super().paintEvent(a0) p = self._painter p.begin(self) p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) p.scale(1 / self.scale, 1 / self.scale) # draw crosshair if ( self._crosshair[self._createMode] and self.drawing() and self.prevMovePoint is not None and not self.outOfPixmap(self.prevMovePoint) ): p.setPen(QtGui.QColor(0, 0, 0)) p.drawLine( 0, int(self.prevMovePoint.y() * self.scale), int(self.pixmap.width() * self.scale) - 1, int(self.prevMovePoint.y() * self.scale), ) p.drawLine( int(self.prevMovePoint.x() * self.scale), 0, int(self.prevMovePoint.x() * self.scale), int(self.pixmap.height() * self.scale) - 1, ) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) assert len(self.line.points) == len(self.line.point_labels) self.line.paint(p) if self.selectedShapesCopy: for s in self.selectedShapesCopy: s.paint(p) if not self.current or self.createMode not in [ "polygon", "ai_polygon", "ai_mask", ]: p.end() return drawing_shape: Shape = self.current.copy() if self.createMode == "polygon": if self.fillDrawing() and len(self.current.points) >= 2: assert drawing_shape.fill_color is not None if drawing_shape.fill_color.getRgb()[3] == 0: logger.warning( "fill_drawing=true, but fill_color is transparent," " so forcing to be opaque." ) drawing_shape.fill_color.setAlpha(64) drawing_shape.addPoint(self.line[1]) elif self.createMode in ["ai_polygon", "ai_mask"]: drawing_shape.addPoint( point=self.line.points[1], label=self.line.point_labels[1], ) self._update_shape_with_ai( points=drawing_shape.points, point_labels=drawing_shape.point_labels, shape=drawing_shape, ) drawing_shape.fill = self.fillDrawing() drawing_shape.selected = self.fillDrawing() drawing_shape.paint(p) p.end() def transformPos(self, point: QPointF) -> QPointF: """Convert from widget-logical coordinates to painter-logical ones.""" return point / self.scale - self.offsetToCenter() def enableDragging(self, enabled: bool): self._is_dragging_enabled = enabled def offsetToCenter(self) -> QPointF: s = self.scale area = super().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 outOfPixmap(self, p: QPointF) -> bool: 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.createMode in ["ai_polygon", "ai_mask"]: self._update_shape_with_ai( points=self.current.points, point_labels=self.current.point_labels, shape=self.current, ) self.current.close() self.shapes.append(self.current) self.storeShapes() self.current = None self.setHiding(False) self.newShape.emit() self.update() def closeEnough(self, p1, p2): # d = distance(p1 - p2) # m = (p1-p2).manhattanLength() # print "d %.2f, m %d, %.2f" % (d, m, d - m) # divide by scale to allow more precision when zoomed in return labelme.utils.distance(p1 - p2) < (self.epsilon / self.scale) def intersectionPoint(self, p1: QPointF, p2: QPointF) -> QPointF: # Cycle through each image edge in clockwise fashion, # and find the one intersecting the current line segment. # http://paulbourke.net/geometry/lineline2d/ size = self.pixmap.size() points = [ (0, 0), (size.width(), 0), (size.width(), size.height()), (0, size.height()), ] # x1, y1 should be in the pixmap, x2, y2 should be out of the pixmap x1 = min(max(p1.x(), 0), size.width()) y1 = min(max(p1.y(), 0), size.height()) x2, y2 = p2.x(), p2.y() d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points)) x3, y3 = points[i] x4, y4 = points[(i + 1) % 4] if (x, y) == (x1, y1): # Handle cases where previous point is on one of the edges. if x3 == x4: return QPointF(x3, min(max(0, y2), max(y3, y4))) else: # y3 == y4 return QPointF(min(max(0, x2), max(x3, x4)), y3) return QPointF(x, y) def intersectingEdges(self, point1, point2, points): """Find intersecting edges. For each edge formed by `points', yield the intersection with the line segment `(x1,y1) - (x2,y2)`, if it exists. Also return the distance of `(x2,y2)' to the middle of the edge along with its index, so that the one closest can be chosen. """ (x1, y1) = point1 (x2, y2) = point2 for i in range(4): x3, y3 = points[i] x4, y4 = points[(i + 1) % 4] denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) if denom == 0: # This covers two cases: # nua == nub == 0: Coincident # otherwise: Parallel continue ua, ub = nua / denom, nub / denom if 0 <= ua <= 1 and 0 <= ub <= 1: x = x1 + ua * (x2 - x1) y = y1 + ua * (y2 - y1) m = QPointF((x3 + x4) / 2, (y3 + y4) / 2) d = labelme.utils.distance(m - QPointF(x2, y2)) yield d, i, (x, y) # These two, along with a call to adjustSize are required for the # scroll area. def sizeHint(self): return self.minimumSizeHint() def minimumSizeHint(self): if not self.pixmap: return super().minimumSizeHint() min_size = self.scale * self.pixmap.size() if self._is_dragging_enabled: # When drag buffer should be enabled, add a bit of buffer around the image # This lets dragging the image around have a bit of give on the edges min_size = 1.167 * min_size return min_size def wheelEvent(self, a0: QtGui.QWheelEvent) -> None: mods: Qt.KeyboardModifiers = a0.modifiers() delta: QPoint = a0.angleDelta() if Qt.ControlModifier == int(mods): # with Ctrl/Command key # zoom self.zoomRequest.emit(delta.y(), a0.posF()) else: # scroll self.scrollRequest.emit(delta.x(), Qt.Horizontal) self.scrollRequest.emit(delta.y(), Qt.Vertical) a0.accept() def moveByKeyboard(self, offset): if self.selectedShapes: self.boundedMoveShapes(self.selectedShapes, self.prevPoint + offset) self.repaint() self.movingShape = True def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None: modifiers = a0.modifiers() key = a0.key() if self.drawing(): if key == Qt.Key_Escape and self.current: self.current = None self.drawingPolygon.emit(False) self.update() elif ( key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Space) and self.canCloseShape() ): self.finalise() elif modifiers == Qt.AltModifier: self.snapping = False elif self.editing(): if key == Qt.Key_Up: self.moveByKeyboard(QPointF(0.0, -MOVE_SPEED)) elif key == Qt.Key_Down: self.moveByKeyboard(QPointF(0.0, MOVE_SPEED)) elif key == Qt.Key_Left: self.moveByKeyboard(QPointF(-MOVE_SPEED, 0.0)) elif key == Qt.Key_Right: self.moveByKeyboard(QPointF(MOVE_SPEED, 0.0)) self._update_status() def keyReleaseEvent(self, a0: QtGui.QKeyEvent) -> None: modifiers = a0.modifiers() if self.drawing(): if int(modifiers) == 0: self.snapping = True elif self.editing(): if ( self.movingShape and self.selectedShapes and self.selectedShapes[0] in self.shapes ): index = self.shapes.index(self.selectedShapes[0]) if self.shapesBackups[-1][index].points != self.shapes[index].points: self.storeShapes() self.shapeMoved.emit() self.movingShape = False def setLastLabel(self, text, flags): assert text self.shapes[-1].label = text self.shapes[-1].flags = flags self.shapesBackups.pop() self.storeShapes() return self.shapes[-1] def undoLastLine(self): assert self.shapes self.current = self.shapes.pop() self.current.setOpen() self.current.restoreShapeRaw() if self.createMode in ["polygon", "linestrip"]: self.line.points = [self.current[-1], self.current[0]] elif self.createMode in ["rectangle", "line", "circle"]: self.current.points = self.current.points[0:1] elif self.createMode == "point": self.current = None self.drawingPolygon.emit(True) def undoLastPoint(self): if not self.current or self.current.isClosed(): return self.current.popPoint() if len(self.current) > 0: self.line[0] = self.current[-1] else: self.current = None self.drawingPolygon.emit(False) self.update() def loadPixmap(self, pixmap, clear_shapes=True): self.pixmap = pixmap self._pixmap_hash = hash( labelme.utils.img_qt_to_arr(img_qt=self.pixmap.toImage()).tobytes() ) if clear_shapes: self.shapes = [] self.update() def loadShapes(self, shapes, replace=True): if replace: self.shapes = list(shapes) else: self.shapes.extend(shapes) self.storeShapes() self.current = None self.hShape = None self.hVertex = None self.hEdge = None self.update() def setShapeVisible(self, shape, value): self.visible[shape] = value self.update() def overrideCursor(self, cursor): if cursor == self._cursor: return self.restoreCursor() self._cursor = cursor QtWidgets.QApplication.setOverrideCursor(cursor) def restoreCursor(self): self._cursor = CURSOR_DEFAULT QtWidgets.QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() self.pixmap = QtGui.QPixmap() self._pixmap_hash = None self.shapes = [] self.shapesBackups = [] self.movingShape = False self.selectedShapes = [] self.selectedShapesCopy = [] self.current = None self.hShape = None self._lasthShape = None self.hVertex = None self._lasthVertex = None self.hEdge = None self._lasthEdge = None self.update() def _update_shape_with_ai_response( response: osam.types.GenerateResponse, shape: Shape, createMode: Literal["ai_polygon", "ai_mask"], ) -> None: if createMode not in ["ai_polygon", "ai_mask"]: raise ValueError( f"createMode must be 'ai_polygon' or 'ai_mask', not {createMode}" ) if not response.annotations: logger.warning("No annotations returned") return if createMode == "ai_mask": y1: int x1: int y2: int x2: int if response.annotations[0].bounding_box is None: y1, x1, y2, x2 = imgviz.masks_to_bboxes( masks=[response.annotations[0].mask] )[0].astype(int) else: y1 = response.annotations[0].bounding_box.ymin x1 = response.annotations[0].bounding_box.xmin y2 = response.annotations[0].bounding_box.ymax x2 = response.annotations[0].bounding_box.xmax shape.setShapeRefined( shape_type="mask", points=[QPointF(x1, y1), QPointF(x2, y2)], point_labels=[1, 1], mask=response.annotations[0].mask[y1 : y2 + 1, x1 : x2 + 1], ) elif createMode == "ai_polygon": points = polygon_from_mask.compute_polygon_from_mask( mask=response.annotations[0].mask ) if len(points) < 2: return shape.setShapeRefined( shape_type="polygon", points=[QPointF(point[0], point[1]) for point in points], point_labels=[1] * len(points), ) def _snap_cursor_pos_for_square(pos: QPointF, opposite_vertex: QPointF) -> QPointF: pos_from_opposite: QPointF = pos - opposite_vertex square_size: float = min(abs(pos_from_opposite.x()), abs(pos_from_opposite.y())) return opposite_vertex + QPointF( np.sign(pos_from_opposite.x()) * square_size, np.sign(pos_from_opposite.y()) * square_size, ) ================================================ FILE: labelme/widgets/download.py ================================================ from __future__ import annotations import types import osam from loguru import logger from PyQt5 import QtWidgets from PyQt5.QtCore import QObject from PyQt5.QtCore import QRunnable from PyQt5.QtCore import Qt from PyQt5.QtCore import QThreadPool from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QProgressDialog class _AiModelDownloadSignals(QObject): finished = pyqtSignal() error = pyqtSignal(Exception) class _AiModelDownloadWorker(QRunnable): def __init__(self, model_type, signals: _AiModelDownloadSignals): super().__init__() self.model_type = model_type self.signals = signals def run(self): try: self.model_type.pull() self.signals.finished.emit() except Exception as e: self.signals.error.emit(e) def download_ai_model(model_name: str, parent: QtWidgets.QWidget) -> bool: model_type = osam.apis.get_model_type_by_name(model_name) if _is_already_downloaded := model_type.get_size() is not None: return True dialog: QProgressDialog = QProgressDialog( "Downloading AI model...\n(requires internet connection)", None, # type: ignore 0, 0, parent, ) # type: ignore[call-overload] dialog.setWindowModality(Qt.WindowModal) dialog.setMinimumDuration(0) signals: _AiModelDownloadSignals = _AiModelDownloadSignals() worker: _AiModelDownloadWorker = _AiModelDownloadWorker(model_type, signals) pool = QThreadPool.globalInstance() handle_error_attrs = types.SimpleNamespace(e=None) def handle_error(e: Exception): logger.error("Exception occurred: {}", e) handle_error_attrs.e = e # QtWidgets.QApplication.setOverrideCursor(Qt.ArrowCursor) dialog.setRange(0, 1) # pause busy mode dialog.setLabelText("Failed to download AI model.\n(check internet connection)") dialog.setCancelButtonText("Close") signals.finished.connect(dialog.close) signals.error.connect(handle_error) dialog.show() pool.start(worker) dialog.exec_() return handle_error_attrs.e is None ================================================ FILE: labelme/widgets/file_dialog_preview.py ================================================ import json from typing import cast from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets class ScrollAreaPreview(QtWidgets.QScrollArea): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWidgetResizable(True) content = QtWidgets.QWidget(self) self.setWidget(content) lay = QtWidgets.QVBoxLayout(content) self.label = QtWidgets.QLabel(content) self.label.setWordWrap(True) lay.addWidget(self.label) def setText(self, text): self.label.setText(text) def setPixmap(self, pixmap): self.label.setPixmap(pixmap) def clear(self): self.label.clear() class FileDialogPreview(QtWidgets.QFileDialog): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setOption(self.DontUseNativeDialog, True) self.labelPreview = ScrollAreaPreview(self) self.labelPreview.setFixedSize(300, 300) self.labelPreview.setHidden(True) box = QtWidgets.QVBoxLayout() box.addWidget(self.labelPreview) box.addStretch() self.setFixedSize(self.width() + 300, self.height()) layout = self.layout() layout = cast(QtWidgets.QGridLayout, layout) layout.addLayout(box, 1, 3, 1, 1) self.currentChanged.connect(self.onChange) def onChange(self, path): if path.lower().endswith(".json"): with open(path) as f: data = json.load(f) self.labelPreview.setText(json.dumps(data, indent=4, sort_keys=False)) self.labelPreview.label.setAlignment( QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop ) self.labelPreview.setHidden(False) else: pixmap = QtGui.QPixmap(path) if pixmap.isNull(): self.labelPreview.clear() self.labelPreview.setHidden(True) else: self.labelPreview.setPixmap( pixmap.scaled( self.labelPreview.width() - 30, self.labelPreview.height() - 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation, ) ) self.labelPreview.label.setAlignment(QtCore.Qt.AlignCenter) self.labelPreview.setHidden(False) ================================================ FILE: labelme/widgets/label_dialog.py ================================================ import re from typing import cast from loguru import logger from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets import labelme.utils # TODO(unknown): # - Calculate optimal position so as not to go out of screen area. class LabelQLineEdit(QtWidgets.QLineEdit): def setListWidget(self, list_widget): self.list_widget = list_widget def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None: if a0.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Down]: self.list_widget.keyPressEvent(a0) else: super().keyPressEvent(a0) class LabelDialog(QtWidgets.QDialog): def __init__( self, text="Enter object label", parent=None, labels=None, sort_labels=True, show_text_field=True, completion="startswith", fit_to_content=None, flags=None, ): if fit_to_content is None: fit_to_content = {"row": False, "column": True} self._fit_to_content = fit_to_content super().__init__(parent) self.edit = LabelQLineEdit() self.edit.setPlaceholderText(text) self.edit.setValidator(labelme.utils.labelValidator()) self.edit.editingFinished.connect(self.postProcess) if flags: self.edit.textChanged.connect(self.updateFlags) self.edit_group_id = QtWidgets.QLineEdit() self.edit_group_id.setPlaceholderText("Group ID") self.edit_group_id.setValidator( QtGui.QRegExpValidator(QtCore.QRegExp(r"\d*"), None) ) layout = QtWidgets.QVBoxLayout() if show_text_field: layout_edit = QtWidgets.QHBoxLayout() layout_edit.addWidget(self.edit, 6) layout_edit.addWidget(self.edit_group_id, 2) layout.addLayout(layout_edit) # buttons self.buttonBox = bb = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, QtCore.Qt.Horizontal, self, ) bb.accepted.connect(self.validate) bb.rejected.connect(self.reject) layout.addWidget(bb) # label_list self.labelList = QtWidgets.QListWidget() if self._fit_to_content["row"]: self.labelList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) if self._fit_to_content["column"]: self.labelList.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self._sort_labels = sort_labels if labels: self.labelList.addItems(labels) if self._sort_labels: self.labelList.sortItems() else: self.labelList.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) self.labelList.currentItemChanged.connect(self.labelSelected) self.labelList.itemDoubleClicked.connect(self.labelDoubleClicked) self.labelList.setFixedHeight(150) self.edit.setListWidget(self.labelList) layout.addWidget(self.labelList) # label_flags if flags is None: flags = {} self._flags = flags self.flagsLayout = QtWidgets.QVBoxLayout() self.resetFlags() layout.addItem(self.flagsLayout) self.edit.textChanged.connect(self.updateFlags) # text edit self.editDescription = QtWidgets.QTextEdit() self.editDescription.setPlaceholderText("Label description") self.editDescription.setFixedHeight(50) layout.addWidget(self.editDescription) self.setLayout(layout) # completion completer = QtWidgets.QCompleter() if completion == "startswith": completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion) # Default settings. # completer.setFilterMode(QtCore.Qt.MatchStartsWith) elif completion == "contains": completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) completer.setFilterMode(QtCore.Qt.MatchContains) else: raise ValueError(f"Unsupported completion: {completion}") completer.setModel(self.labelList.model()) self.edit.setCompleter(completer) def addLabelHistory(self, label): if self.labelList.findItems(label, QtCore.Qt.MatchExactly): return self.labelList.addItem(label) if self._sort_labels: self.labelList.sortItems() def labelSelected(self, item): self.edit.setText(item.text()) def validate(self): if not self.edit.isEnabled(): self.accept() return if self._get_stripped_text(): self.accept() def _get_stripped_text(self) -> str: text = self.edit.text() if hasattr(text, "strip"): return str(text.strip()) if hasattr(text, "trimmed"): return str(text.trimmed()) return str(text) def labelDoubleClicked(self, item): self.validate() def postProcess(self): self.edit.setText(self._get_stripped_text()) def updateFlags(self, label_new): # keep state of shared flags flags_old = self.getFlags() flags_new = {} for pattern, keys in self._flags.items(): if re.match(pattern, label_new): for key in keys: flags_new[key] = flags_old.get(key, False) self.setFlags(flags_new) def deleteFlags(self): for i in reversed(range(self.flagsLayout.count())): item = self.flagsLayout.itemAt(i).widget() self.flagsLayout.removeWidget(item) item.setParent(QtWidgets.QWidget()) def resetFlags(self, label=""): flags = {} for pattern, keys in self._flags.items(): if re.match(pattern, label): for key in keys: flags[key] = False self.setFlags(flags) def setFlags(self, flags): self.deleteFlags() for key in flags: item = QtWidgets.QCheckBox(key, self) item.setChecked(flags[key]) self.flagsLayout.addWidget(item) item.show() def getFlags(self): flags = {} for i in range(self.flagsLayout.count()): item = self.flagsLayout.itemAt(i).widget() item = cast(QtWidgets.QCheckBox, item) flags[item.text()] = item.isChecked() return flags def getGroupId(self): group_id = self.edit_group_id.text() if group_id: return int(group_id) return None def popUp( self, text=None, move=True, flags=None, group_id=None, description=None, flags_disabled: bool = False, ): if self._fit_to_content["row"]: self.labelList.setMinimumHeight( self.labelList.sizeHintForRow(0) * self.labelList.count() + 2 ) if self._fit_to_content["column"]: self.labelList.setMinimumWidth(self.labelList.sizeHintForColumn(0) + 2) # if text is None, the previous label in self.edit is kept if text is None: text = self.edit.text() # description is always initialized by empty text c.f., self.edit.text if description is None: description = "" self.editDescription.setPlainText(description) if flags: self.setFlags(flags) else: self.resetFlags(text) if flags_disabled: for i in range(self.flagsLayout.count()): self.flagsLayout.itemAt(i).widget().setDisabled(True) self.edit.setText(text) self.edit.setSelection(0, len(text)) if group_id is None: self.edit_group_id.clear() else: self.edit_group_id.setText(str(group_id)) items = self.labelList.findItems(text, QtCore.Qt.MatchFixedString) if items: if len(items) != 1: logger.warning(f"Label list has duplicate '{text}'") self.labelList.setCurrentItem(items[0]) row = self.labelList.row(items[0]) self.edit.completer().setCurrentRow(row) self.edit.setFocus(QtCore.Qt.PopupFocusReason) if move: self.move(QtGui.QCursor.pos()) if self.exec_(): return ( self.edit.text(), self.getFlags(), self.getGroupId(), self.editDescription.toPlainText(), ) else: return None, None, None, None ================================================ FILE: labelme/widgets/label_list_widget.py ================================================ from typing import cast from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QStyle # https://stackoverflow.com/a/2039745/4158863 class HTMLDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent=None): super().__init__() self.doc = QtGui.QTextDocument(self) def paint(self, painter, option, index): painter.save() options = QtWidgets.QStyleOptionViewItem(option) self.initStyleOption(options, index) self.doc.setHtml(options.text) options.text = "" style = ( QtWidgets.QApplication.style() if options.widget is None else options.widget.style() ) style.drawControl(QStyle.CE_ItemViewItem, options, painter) ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() if option.state & QStyle.State_Selected: ctx.palette.setColor( QPalette.Text, option.palette.color(QPalette.Active, QPalette.HighlightedText), ) else: ctx.palette.setColor( QPalette.Text, option.palette.color(QPalette.Active, QPalette.Text), ) textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options) if index.column() != 0: textRect.adjust(5, 0, 0, 0) thefuckyourshitup_constant = 4 margin = (option.rect.height() - options.fontMetrics.height()) // 2 margin = margin - thefuckyourshitup_constant textRect.setTop(textRect.top() + margin) painter.translate(textRect.topLeft()) painter.setClipRect(textRect.translated(-textRect.topLeft())) self.doc.documentLayout().draw(painter, ctx) painter.restore() def sizeHint(self, option, index): thefuckyourshitup_constant = 4 return QtCore.QSize( int(self.doc.idealWidth()), int(self.doc.size().height() - thefuckyourshitup_constant), ) class LabelListWidgetItem(QtGui.QStandardItem): def __init__(self, text=None, shape=None): super().__init__() self.setText(text or "") self.setShape(shape) self.setCheckable(True) self.setCheckState(Qt.Checked) self.setEditable(False) self.setTextAlignment(Qt.AlignBottom) def clone(self): return LabelListWidgetItem(self.text(), self.shape()) def setShape(self, shape): self.setData(shape, Qt.UserRole) def shape(self): return self.data(Qt.UserRole) def __hash__(self): return id(self) def __repr__(self): return f'{self.__class__.__name__}("{self.text()}")' class _ItemModel(QtGui.QStandardItemModel): itemDropped = QtCore.pyqtSignal() def removeRows(self, *args, **kwargs): ret = super().removeRows(*args, **kwargs) self.itemDropped.emit() return ret def dropMimeData(self, data, action, row: int, column: int, parent): # NOTE: By default, PyQt will overwrite items when dropped on them, so we need # to adjust the row/parent to insert after the item instead. # If row is -1, we're dropping on an item (which would overwrite) # Instead, we want to insert after it if row == -1 and parent.isValid(): row = parent.row() + 1 parent = parent.parent() # If still -1, append to end if row == -1: row = self.rowCount(parent) return super().dropMimeData(data, action, row, column, parent) class LabelListWidget(QtWidgets.QListView): itemDoubleClicked = QtCore.pyqtSignal(LabelListWidgetItem) itemSelectionChanged = QtCore.pyqtSignal(list, list) def __init__(self): super().__init__() self._selectedItems = [] self.setWindowFlags(Qt.Window) self._model: _ItemModel = _ItemModel() self._model.setItemPrototype(LabelListWidgetItem()) self.setModel(self._model) self.setItemDelegate(HTMLDelegate()) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) self.setDefaultDropAction(Qt.MoveAction) self.doubleClicked.connect(self.itemDoubleClickedEvent) self.selectionModel().selectionChanged.connect(self.itemSelectionChangedEvent) def __len__(self): return self._model.rowCount() def __getitem__(self, i): return self._model.item(i) def __iter__(self): for i in range(len(self)): yield self[i] @property def itemDropped(self): return self._model.itemDropped @property def itemChanged(self): return self._model.itemChanged def itemSelectionChangedEvent(self, selected, deselected): selected = [self._model.itemFromIndex(i) for i in selected.indexes()] deselected = [self._model.itemFromIndex(i) for i in deselected.indexes()] self.itemSelectionChanged.emit(selected, deselected) def itemDoubleClickedEvent(self, index): self.itemDoubleClicked.emit(self._model.itemFromIndex(index)) def selectedItems(self): return [self._model.itemFromIndex(i) for i in self.selectedIndexes()] def scrollToItem(self, item): self.scrollTo(self._model.indexFromItem(item)) def addItem(self, item): if not isinstance(item, LabelListWidgetItem): raise TypeError("item must be LabelListWidgetItem") self._model.setItem(self._model.rowCount(), 0, item) item.setSizeHint(self.itemDelegate().sizeHint(None, None)) # type: ignore[arg-type,union-attr] def removeItem(self, item): index = self._model.indexFromItem(item) self._model.removeRows(index.row(), 1) def selectItem(self, item): index = self._model.indexFromItem(item) self.selectionModel().select(index, QtCore.QItemSelectionModel.Select) def findItemByShape(self, shape): for row in range(self._model.rowCount()): item = self._model.item(row, 0) item = cast(LabelListWidgetItem, item) if item.shape() == shape: return item raise ValueError(f"cannot find shape: {shape}") def clear(self): self._model.clear() ================================================ FILE: labelme/widgets/tool_bar.py ================================================ from __future__ import annotations from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from .. import utils class ToolBar(QtWidgets.QToolBar): def __init__( self, title: str, actions: list[QtWidgets.QAction | None], orientation: Qt.Orientation = Qt.Horizontal, button_style: Qt.ToolButtonStyle = Qt.ToolButtonTextUnderIcon, font_base: QtGui.QFont | None = None, ) -> None: super().__init__(title) if font_base: font = QtGui.QFont(font_base) font.setPointSizeF(font_base.pointSizeF() * 0.875) self.setFont(font) layout = self.layout() m = (0, 0, 0, 0) layout.setSpacing(0) layout.setContentsMargins(*m) self.setContentsMargins(*m) self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) self.setMovable(False) self.setFloatable(False) self.setObjectName(f"{title}ToolBar") self.setOrientation(orientation) self.setToolButtonStyle(button_style) utils.addActions(widget=self, actions=actions) def addAction(self, action): # type: ignore[override] if isinstance(action, QtWidgets.QWidgetAction): return super().addAction(action) btn = QtWidgets.QToolButton() btn.setDefaultAction(action) btn.setToolButtonStyle(self.toolButtonStyle()) self.addWidget(btn) # center align for i in range(self.layout().count()): if isinstance(self.layout().itemAt(i).widget(), QtWidgets.QToolButton): self.layout().itemAt(i).setAlignment(QtCore.Qt.AlignCenter) ================================================ FILE: labelme/widgets/unique_label_qlist_widget.py ================================================ import html from PyQt5 import QtGui from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from .label_list_widget import HTMLDelegate class _EscapableQListWidget(QtWidgets.QListWidget): def keyPressEvent(self, keyEvent: QtGui.QKeyEvent) -> None: # type: ignore super().keyPressEvent(keyEvent) if keyEvent.key() == Qt.Key_Escape: self.clearSelection() class UniqueLabelQListWidget(_EscapableQListWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setItemDelegate(HTMLDelegate(parent=self)) def mousePressEvent(self, mouseEvent: QtGui.QMouseEvent) -> None: # type: ignore super().mousePressEvent(mouseEvent) if not self.indexAt(mouseEvent.pos()).isValid(): self.clearSelection() def find_label_item(self, label: str) -> QtWidgets.QListWidgetItem | None: for row in range(self.count()): item = self.item(row) if item and item.data(Qt.UserRole) == label: return item return None def add_label_item(self, label: str, color: tuple[int, int, int]) -> None: if self.find_label_item(label): raise ValueError(f"Item for label '{label}' already exists") item = QtWidgets.QListWidgetItem() item.setData(Qt.UserRole, label) # for find_label_item item.setText( f"{html.escape(label)} " f"" ) self.addItem(item) ================================================ FILE: labelme/widgets/zoom_widget.py ================================================ from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets class ZoomWidget(QtWidgets.QSpinBox): def __init__(self, value=100): super().__init__() self.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) self.setRange(1, 1000) self.setSuffix(" %") self.setValue(value) self.setToolTip("Zoom Level") self.setStatusTip(self.toolTip()) self.setAlignment(QtCore.Qt.AlignCenter) def minimumSizeHint(self): height = super().minimumSizeHint().height() fm = QtGui.QFontMetrics(self.font()) width = fm.width(str(self.maximum())) return QtCore.QSize(width, height) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["hatchling>=1.20.0", "hatch-vcs", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] name = "labelme" description = "Image annotation with Python." license = { text = "GPL-3.0-only" } requires-python = ">=3.10" authors = [{ name = "Kentaro Wada", email = "www.kentaro.wada@gmail.com" }] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] dependencies = [ "imgviz>=2.0.0", "loguru", "matplotlib", "natsort>=7.1.0", "numpy", "osam>=0.3.1", "pillow>=2.8", "pyqt5>=5.14.0", "pyqt5-qt5!=5.15.13 ; sys_platform == 'linux'", "pyqt5-qt5!=5.15.11,!=5.15.12,!=5.15.13,!=5.15.14,!=5.15.15,!=5.15.16 ; sys_platform == 'win32'", "pyyaml", "scikit-image", "tifffile", ] dynamic = ["readme", "version"] [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" fragments = [{ path = "README.md" }] [tool.hatch.version] source = "vcs" [dependency-groups] dev = [ "ipython", "pyqt5-stubs>=5.15.6.0", "pytest>=8.3.4", "pytest-qt>=4.4.0", "ruff~=0.12.11", "twine>=6.1.0", "ty>=0.0.1a29", "types-pillow>=10.2.0.20240822", "types-pyyaml>=6.0.12.20241230", ] [project.scripts] labelme = "labelme.__main__:main" [tool.pytest.ini_options] qt_api = "pyqt5" markers = [ "gui: mark a test as a GUI test.", ] [tool.ruff.lint] select = [ "E", # pycodestyle "F", # pyflakes "I", # isort "UP", # pyupgrade ] [tool.ruff.lint.isort] force-single-line = true [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] ================================================ FILE: tests/conftest.py ================================================ import json import shutil from pathlib import Path import pytest def _create_annotated_nested(data_path: Path) -> None: dst_dir: Path = data_path / "annotated_nested" dst_dir.mkdir() (dst_dir / "images").mkdir() for image_file in (data_path / "annotated").glob("*.jpg"): shutil.copy(image_file, dst_dir / "images" / image_file.name) (dst_dir / "annotations").mkdir() for json_file in (data_path / "annotated").glob("*.json"): dst_json_file = dst_dir / "annotations" / json_file.name shutil.copy(json_file, dst_json_file) with open(dst_json_file) as f: json_data = json.load(f) json_data["imagePath"] = str(Path("..") / "images" / json_data["imagePath"]) with open(dst_json_file, "w") as f: json.dump(json_data, f, indent=2) @pytest.fixture(scope="function") def data_path(tmp_path: Path) -> Path: data_path: Path = tmp_path / "data" shutil.copytree(Path(__file__).parent / "data", data_path) _create_annotated_nested(data_path=data_path) return data_path ================================================ FILE: tests/e2e/__init__.py ================================================ ================================================ FILE: tests/e2e/annotation_test.py ================================================ from __future__ import annotations from pathlib import Path import pytest from PyQt5.QtCore import QPoint from PyQt5.QtCore import QSize from PyQt5.QtCore import Qt from PyQt5.QtCore import QTimer from pytestqt.qtbot import QtBot import labelme.app import labelme.testing from .conftest import show_window_and_wait_for_imagedata @pytest.mark.gui def test_MainWindow_annotate_jpg( qtbot: QtBot, data_path: Path, tmp_path: Path, ) -> None: input_file: str = str(data_path / "raw/2011_000003.jpg") out_file: str = str(tmp_path / "2011_000003.json") win: labelme.app.MainWindow = labelme.app.MainWindow( filename=input_file, config_overrides=dict(auto_save=True), output_dir=str(tmp_path), ) qtbot.addWidget(win) show_window_and_wait_for_imagedata(qtbot=qtbot, win=win) label: str = "whole" canvas_size: QSize = win._canvas_widgets.canvas.size() points: list[tuple[float, float]] = [ (canvas_size.width() * 0.25, canvas_size.height() * 0.25), (canvas_size.width() * 0.75, canvas_size.height() * 0.25), (canvas_size.width() * 0.75, canvas_size.height() * 0.75), (canvas_size.width() * 0.25, canvas_size.height() * 0.75), ] win._switch_canvas_mode(edit=False, createMode="polygon") qtbot.wait(50) def click(xy: tuple[float, float]) -> None: qtbot.mouseMove(win._canvas_widgets.canvas, pos=QPoint(int(xy[0]), int(xy[1]))) qtbot.wait(50) qtbot.mousePress( win._canvas_widgets.canvas, Qt.LeftButton, pos=QPoint(int(xy[0]), int(xy[1])), ) qtbot.wait(50) for xy in points: click(xy=xy) def interact() -> None: qtbot.keyClicks(win._label_dialog.edit, label) qtbot.wait(50) qtbot.keyClick(win._label_dialog.edit, Qt.Key_Enter) qtbot.wait(50) QTimer.singleShot(100, interact) click(xy=points[0]) assert len(win._canvas_widgets.canvas.shapes) == 1 assert len(win._canvas_widgets.canvas.shapes[0].points) == 4 assert win._canvas_widgets.canvas.shapes[0].label == "whole" assert win._canvas_widgets.canvas.shapes[0].shape_type == "polygon" assert win._canvas_widgets.canvas.shapes[0].group_id is None assert win._canvas_widgets.canvas.shapes[0].mask is None assert win._canvas_widgets.canvas.shapes[0].flags == {} win.saveFile() labelme.testing.assert_labelfile_sanity(out_file) win.close() ================================================ FILE: tests/e2e/config_test.py ================================================ from __future__ import annotations from pathlib import Path import pytest from PyQt5 import QtWidgets from pytestqt.qtbot import QtBot import labelme.app @pytest.mark.gui @pytest.mark.parametrize( "with_config_file", [ pytest.param(True, id="with_config_file"), pytest.param(False, id="without_config_file"), ], ) def test_MainWindow_config( with_config_file: bool, qtbot: QtBot, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: config_file: Path | None = None auto_save: bool = True if with_config_file: config_file = tmp_path / "labelmerc.yaml" config_file.write_text("auto_save: false\nlabels: [cat, dog]\n") auto_save = False win: labelme.app.MainWindow = labelme.app.MainWindow( config_file=config_file, config_overrides={"labels": ["bird"]}, ) qtbot.addWidget(win) win.show() assert win._config["auto_save"] is auto_save assert win._config["labels"] == ["bird"] assert win._config_file == config_file if not with_config_file: message_box_shown: list[bool] = [False] def mock_information(parent, title, message): message_box_shown[0] = True assert "No Config File" in title return QtWidgets.QMessageBox.Ok monkeypatch.setattr(QtWidgets.QMessageBox, "information", mock_information) win._open_config_file() assert message_box_shown[0] is True win.close() ================================================ FILE: tests/e2e/conftest.py ================================================ from __future__ import annotations from collections.abc import Generator from pathlib import Path import pytest from PyQt5.QtCore import QSettings from pytestqt.qtbot import QtBot import labelme.app @pytest.fixture(autouse=True) def _isolated_qtsettings( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> Generator[None, None, None]: settings_file = tmp_path / "qtsettings.ini" settings: QSettings = QSettings(str(settings_file), QSettings.IniFormat) monkeypatch.setattr( labelme.app.QtCore, "QSettings", lambda *args, **kwargs: settings ) yield def show_window_and_wait_for_imagedata( qtbot: QtBot, win: labelme.app.MainWindow ) -> None: win.show() def check_imageData() -> None: assert hasattr(win, "imageData") assert win.imageData is not None qtbot.waitUntil(check_imageData) ================================================ FILE: tests/e2e/file_loading_test.py ================================================ from __future__ import annotations from pathlib import Path from typing import Literal import pytest from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from pytestqt.qtbot import QtBot import labelme.app import labelme.testing from .conftest import show_window_and_wait_for_imagedata @pytest.mark.gui def test_MainWindow_open_img( qtbot: QtBot, data_path: Path, ) -> None: image_file: str = str(data_path / "raw/2011_000003.jpg") win: labelme.app.MainWindow = labelme.app.MainWindow(filename=image_file) qtbot.addWidget(win) show_window_and_wait_for_imagedata(qtbot=qtbot, win=win) win.close() @pytest.mark.gui def test_MainWindow_open_json( qtbot: QtBot, data_path: Path, ) -> None: json_files: list[str] = [ str(data_path / "annotated_with_data/apc2016_obj3.json"), str(data_path / "annotated/2011_000003.json"), ] json_file: str for json_file in json_files: labelme.testing.assert_labelfile_sanity(json_file) win: labelme.app.MainWindow = labelme.app.MainWindow(filename=json_file) qtbot.addWidget(win) show_window_and_wait_for_imagedata(qtbot=qtbot, win=win) win.close() @pytest.mark.gui @pytest.mark.parametrize("scenario", ["raw", "annotated", "annotated_nested"]) def test_MainWindow_open_dir( qtbot: QtBot, scenario: Literal["raw", "annotated", "annotated_nested"], data_path: Path, ) -> None: directory: str output_dir: str | None if scenario == "annotated_nested": directory = str(data_path / "annotated_nested" / "images") output_dir = str(data_path / "annotated_nested" / "annotations") else: directory = str(data_path / scenario) output_dir = None win: labelme.app.MainWindow = labelme.app.MainWindow( filename=directory, output_dir=output_dir ) qtbot.addWidget(win) show_window_and_wait_for_imagedata(qtbot=qtbot, win=win) first_image_name: str = "2011_000003.jpg" second_image_name: str = "2011_000006.jpg" assert win._image_path assert Path(win._image_path).name == first_image_name win._open_prev_image() qtbot.wait(100) assert Path(win._image_path).name == first_image_name win._open_next_image() qtbot.wait(100) assert Path(win._image_path).name == second_image_name win._open_prev_image() qtbot.wait(100) assert Path(win._image_path).name == first_image_name assert win._docks.file_list.count() == 3 expected_check_state = ( Qt.Checked if scenario.startswith("annotated") else Qt.Unchecked ) for index in range(win._docks.file_list.count()): item: QtWidgets.QListWidgetItem | None = win._docks.file_list.item(index) assert item assert item.checkState() == expected_check_state ================================================ FILE: tests/e2e/navigation_test.py ================================================ from __future__ import annotations from pathlib import Path import pytest from PyQt5.QtCore import QPoint from PyQt5.QtCore import Qt from pytestqt.qtbot import QtBot import labelme.app from .conftest import show_window_and_wait_for_imagedata @pytest.mark.gui def test_image_navigation_while_selecting_shape( qtbot: QtBot, data_path: Path, ) -> None: win: labelme.app.MainWindow = labelme.app.MainWindow( filename=str(data_path / "annotated") ) qtbot.addWidget(win) show_window_and_wait_for_imagedata(qtbot=qtbot, win=win) # Incident: https://github.com/wkentaro/labelme/pull/1716 {{ point = QPoint(250, 200) qtbot.mouseMove(win._canvas_widgets.canvas, pos=point) qtbot.mouseClick(win._canvas_widgets.canvas, Qt.LeftButton, pos=point) qtbot.wait(100) qtbot.mouseClick(win._docks.file_list, Qt.LeftButton) qtbot.wait(100) qtbot.keyClick(win._docks.file_list, Qt.Key_Down) qtbot.wait(100) qtbot.keyClick(win._canvas_widgets.canvas, Qt.Key_Down) qtbot.wait(100) # }} win.close() ================================================ FILE: tests/e2e/smoke_test.py ================================================ from __future__ import annotations from pathlib import Path import pytest from pytestqt.qtbot import QtBot import labelme.app @pytest.mark.gui def test_MainWindow_open(qtbot: QtBot) -> None: win: labelme.app.MainWindow = labelme.app.MainWindow() qtbot.addWidget(win) win.show() win.close() @pytest.mark.gui def test_file_search_config_filters_on_startup(qtbot: QtBot, data_path: Path) -> None: raw_dir = data_path / "raw" all_images = list(raw_dir.glob("*.jpg")) assert len(all_images) == 3 win = labelme.app.MainWindow( config_overrides={"file_search": "2011_000003"}, filename=str(raw_dir), ) qtbot.addWidget(win) win.show() assert win._docks.file_search.text() == "2011_000003" assert win._docks.file_list.count() == 1 win.close() ================================================ FILE: tests/unit/__init__.py ================================================ ================================================ FILE: tests/unit/_label_file_test.py ================================================ from __future__ import annotations import json import shutil from pathlib import Path from labelme._label_file import LabelFile def test_LabelFile_load_windows_path(data_path: Path, tmp_path: Path) -> None: """Test that LabelFile can load JSON with Windows-style backslash paths. Regression test for https://github.com/wkentaro/labelme/issues/1725 """ (tmp_path / "images").mkdir() shutil.copy( data_path / "annotated" / "2011_000003.jpg", tmp_path / "images" / "2011_000003.jpg", ) json_file = tmp_path / "annotations" / "2011_000003.json" json_file.parent.mkdir() with open(data_path / "annotated" / "2011_000003.json") as f: json_data = json.load(f) json_data["imagePath"] = "..\\images\\2011_000003.jpg" with open(json_file, "w") as f: json.dump(json_data, f) label_file = LabelFile(str(json_file)) assert label_file.imagePath == "../images/2011_000003.jpg" assert label_file.imageData is not None ================================================ FILE: tests/unit/config_test.py ================================================ from pathlib import Path import pytest import yaml from labelme.config import _migrate_config_from_file from labelme.config import get_user_config_file from labelme.config import load_config def test_get_user_config_file_creates_sparse(tmp_path, monkeypatch): monkeypatch.setenv("HOME", str(tmp_path)) config_file = get_user_config_file() content = Path(config_file).read_text() assert content.startswith("# Labelme config file") parsed = yaml.safe_load(content) assert parsed is None def test_get_user_config_file_does_not_overwrite(tmp_path, monkeypatch): monkeypatch.setenv("HOME", str(tmp_path)) config_path = tmp_path / ".labelmerc" config_path.write_text("auto_save: true\n") config_file = get_user_config_file() content = Path(config_file).read_text() assert content == "auto_save: true\n" def test_get_user_config_file_skip_creation(tmp_path, monkeypatch): monkeypatch.setenv("HOME", str(tmp_path)) config_file = get_user_config_file(create_if_missing=False) assert not Path(config_file).exists() @pytest.mark.parametrize("old_value", [True, False]) def test_migrate_store_data_to_with_image_data(tmp_path, old_value): config_file = tmp_path / "config.yaml" config_file.write_text(f"store_data: {str(old_value).lower()}\n") config = load_config(config_file=config_file, config_overrides={}) assert config["with_image_data"] is old_value assert "store_data" not in config @pytest.mark.parametrize( "input_name, expected_name", [ ("SegmentAnything (balanced)", "Sam (balanced)"), ("SegmentAnything (tiny)", "Sam (tiny)"), ("Sam (balanced)", "Sam (balanced)"), ("Sam (large)", "Sam (large)"), ("Sam2 (balanced)", "Sam2 (balanced)"), ], ) def test_migrate_ai_model_name(input_name: str, expected_name: str) -> None: config: dict = {"ai": {"default": input_name}} _migrate_config_from_file(config) assert config["ai"]["default"] == expected_name _POLYGON_TO_SHAPE_RENAMES = { "edit_polygon": "edit_shape", "delete_polygon": "delete_shape", "duplicate_polygon": "duplicate_shape", "copy_polygon": "copy_shape", "paste_polygon": "paste_shape", "show_all_polygons": "show_all_shapes", "hide_all_polygons": "hide_all_shapes", "toggle_all_polygons": "toggle_all_shapes", } @pytest.mark.parametrize( "old_key, new_key", list(_POLYGON_TO_SHAPE_RENAMES.items()), ids=list(_POLYGON_TO_SHAPE_RENAMES.keys()), ) def test_migrate_polygon_shortcut_to_shape(old_key, new_key): config = {"shortcuts": {old_key: "Ctrl+X"}} _migrate_config_from_file(config) assert old_key not in config["shortcuts"] assert config["shortcuts"][new_key] == "Ctrl+X" def test_migrate_polygon_shortcuts_no_shortcuts_key(): config = {} _migrate_config_from_file(config) assert "shortcuts" not in config def test_migrate_polygon_shortcut_skips_when_new_key_exists(): config = {"shortcuts": {"edit_polygon": "Ctrl+X", "edit_shape": "Ctrl+Y"}} _migrate_config_from_file(config) assert config["shortcuts"]["edit_shape"] == "Ctrl+Y" assert "edit_polygon" in config["shortcuts"] ================================================ FILE: tests/unit/load_image_file_test.py ================================================ from __future__ import annotations import io from pathlib import Path import numpy as np import PIL.Image import tifffile from labelme._label_file import LabelFile def _make_image(tmp_path: Path, filename: str, mode: str = "RGB", size=(100, 100)): channels = 4 if mode == "RGBA" else 3 arr = np.random.randint(0, 255, (size[1], size[0], channels), dtype=np.uint8) path = tmp_path / filename PIL.Image.fromarray(arr, mode=mode).save(str(path)) return path def test_tiff_without_alpha_encoded_as_jpeg(tmp_path): path = _make_image(tmp_path, "test.tiff") data = LabelFile.load_image_file(str(path)) assert data[:2] == b"\xff\xd8" def test_tiff_with_alpha_encoded_as_png(tmp_path): path = _make_image(tmp_path, "test.tiff", mode="RGBA") data = LabelFile.load_image_file(str(path)) assert data[:4] == b"\x89PNG" def test_jpeg_returns_raw_bytes(tmp_path): path = _make_image(tmp_path, "test.jpg") data = LabelFile.load_image_file(str(path)) assert data == path.read_bytes() def test_png_returns_raw_bytes(tmp_path): path = _make_image(tmp_path, "test.png") data = LabelFile.load_image_file(str(path)) assert data == path.read_bytes() def test_multispectral_tiff_float32(tmp_path): arr = np.random.rand(64, 64, 5).astype(np.float32) * 0.5 path = tmp_path / "multispectral.tif" tifffile.imwrite(str(path), arr) data = LabelFile.load_image_file(str(path)) assert data[:2] == b"\xff\xd8" img = PIL.Image.open(io.BytesIO(data)) assert img.mode == "RGB" assert img.size == (64, 64) def test_grayscale_tiff_float32(tmp_path): arr = np.random.rand(64, 64).astype(np.float32) path = tmp_path / "grayscale.tif" tifffile.imwrite(str(path), arr) data = LabelFile.load_image_file(str(path)) img = PIL.Image.open(io.BytesIO(data)) assert img.size == (64, 64) def test_constant_value_tiff_returns_black(tmp_path): arr = np.full((64, 64), 42.0, dtype=np.float32) path = tmp_path / "constant.tif" tifffile.imwrite(str(path), arr) data = LabelFile.load_image_file(str(path)) img = PIL.Image.open(io.BytesIO(data)) assert img.size == (64, 64) assert np.array(img).max() == 0 def test_two_band_tiff_falls_back_to_first_band(tmp_path): arr = np.random.rand(64, 64, 2).astype(np.float32) path = tmp_path / "twoband.tif" tifffile.imwrite(str(path), arr) data = LabelFile.load_image_file(str(path)) img = PIL.Image.open(io.BytesIO(data)) assert img.size == (64, 64) ================================================ FILE: tests/unit/shape_contains_point_test.py ================================================ from __future__ import annotations from PyQt5 import QtCore from labelme.shape import Shape def _make_point_shape(x: float, y: float) -> Shape: """Create a point shape with a single point at (x, y).""" shape = Shape(shape_type="point") shape.addPoint(QtCore.QPointF(x, y)) return shape def test_point_shape_contains_center(): """Clicking exactly on a point shape should return True.""" shape = _make_point_shape(100.0, 200.0) assert shape.containsPoint(QtCore.QPointF(100.0, 200.0)) is True def test_point_shape_contains_within_radius(): """Clicking within point_size/2 of the center should return True.""" shape = _make_point_shape(100.0, 200.0) # point_size defaults to 8, so radius = 4. A point 3px away should hit. assert shape.containsPoint(QtCore.QPointF(103.0, 200.0)) is True def test_point_shape_at_exact_boundary(): """Clicking exactly at point_size/2 distance should return True (inclusive).""" shape = _make_point_shape(100.0, 200.0) # point_size defaults to 8, so radius = 4. Exactly 4px away should hit. assert shape.containsPoint(QtCore.QPointF(104.0, 200.0)) is True def test_point_shape_outside_radius(): """Clicking more than point_size/2 away should return False.""" shape = _make_point_shape(100.0, 200.0) # 10px away, well outside the radius of 4 assert shape.containsPoint(QtCore.QPointF(110.0, 200.0)) is False def test_point_shape_empty_points(): """A point shape with no points should return False, not raise.""" shape = Shape(shape_type="point") assert shape.containsPoint(QtCore.QPointF(0.0, 0.0)) is False ================================================ FILE: tests/unit/shape_test.py ================================================ import numpy as np import pytest from numpy.typing import NDArray from PyQt5 import QtCore from labelme.shape import Shape def _make_mask_shape( mask: NDArray[np.bool_], origin_x: float, origin_y: float ) -> Shape: shape = Shape(shape_type="mask") h, w = mask.shape shape.addPoint(QtCore.QPointF(origin_x, origin_y)) shape.addPoint(QtCore.QPointF(origin_x + w, origin_y + h)) shape.mask = mask return shape @pytest.mark.parametrize( "point, expected", [ ((4, 3), True), # inside True region ((4, 4), True), # last valid row/column ((5, 2), False), # one pixel past right boundary ((2, 5), False), # one pixel past bottom boundary ((5, 5), False), # past both boundaries ], ) def test_mask_contains_point(point: tuple[int, int], expected: bool) -> None: mask = np.ones((5, 5), dtype=bool) shape = _make_mask_shape(mask, origin_x=0, origin_y=0) assert shape.containsPoint(QtCore.QPointF(*point)) is expected ================================================ FILE: tests/unit/utils/__init__.py ================================================ ================================================ FILE: tests/unit/utils/image_test.py ================================================ import numpy as np import PIL.Image from labelme.utils import image as image_module from .util import data_dir from .util import get_img_and_data def test_img_b64_to_arr(): img, _ = get_img_and_data() assert img.dtype == np.uint8 assert img.shape == (907, 1210, 3) def test_img_arr_to_b64(): img_file = data_dir / "annotated_with_data/apc2016_obj3.jpg" img_arr = np.asarray(PIL.Image.open(img_file)) img_b64 = image_module.img_arr_to_b64(img_arr) img_arr2 = image_module.img_b64_to_arr(img_b64) np.testing.assert_allclose(img_arr, img_arr2) def test_img_data_to_png_data(): img_file = data_dir / "annotated_with_data/apc2016_obj3.jpg" with open(img_file, "rb") as f: img_data = f.read() png_data = image_module.img_data_to_png_data(img_data) assert isinstance(png_data, bytes) ================================================ FILE: tests/unit/utils/qt_test.py ================================================ from PyQt5.QtCore import QPointF from labelme.utils.qt import distancetoline def test_distancetoline(): line = (QPointF(0, 0), QPointF(10, 0)) assert distancetoline(QPointF(5, 0), line) == 0 assert distancetoline(QPointF(5, 5), line) == 5 assert distancetoline(QPointF(0, 0), line) == 0 assert distancetoline(QPointF(-5, 0), line) == 5 assert distancetoline(QPointF(15, 0), line) == 5 ================================================ FILE: tests/unit/utils/shape_test.py ================================================ import numpy as np from labelme.utils import shape as shape_module from .util import get_img_and_data def test_shapes_to_label(): img, data = get_img_and_data() label_name_to_value = {} for shape in data["shapes"]: label_name = shape["label"] label_value = len(label_name_to_value) label_name_to_value[label_name] = label_value cls, _ = shape_module.shapes_to_label( img.shape, data["shapes"], label_name_to_value ) assert cls.shape == img.shape[:2] def test_shape_to_mask(): img, data = get_img_and_data() for shape in data["shapes"]: points = shape["points"] mask = shape_module.shape_to_mask(img.shape[:2], points) assert mask.shape == img.shape[:2] def test_shape_to_mask_rectangle_reversed_coords(): img_shape = (100, 100) mask_tl_br = shape_module.shape_to_mask( img_shape, [[10, 10], [50, 50]], shape_type="rectangle" ) mask_br_tl = shape_module.shape_to_mask( img_shape, [[50, 50], [10, 10]], shape_type="rectangle" ) mask_tr_bl = shape_module.shape_to_mask( img_shape, [[50, 10], [10, 50]], shape_type="rectangle" ) mask_bl_tr = shape_module.shape_to_mask( img_shape, [[10, 50], [50, 10]], shape_type="rectangle" ) assert np.array_equal(mask_tl_br, mask_br_tl) assert np.array_equal(mask_tl_br, mask_tr_bl) assert np.array_equal(mask_tl_br, mask_bl_tr) assert mask_tl_br.sum() > 0 ================================================ FILE: tests/unit/utils/util.py ================================================ import json from pathlib import Path from labelme.utils import image as image_module from labelme.utils import shape as shape_module here = Path(__file__).parent data_dir = here.parent.parent / "data" def get_img_and_data(): json_file = data_dir / "annotated_with_data/apc2016_obj3.json" with open(json_file) as f: data = json.load(f) img_b64 = data["imageData"] img = image_module.img_b64_to_arr(img_b64) return img, data def get_img_and_lbl(): img, data = get_img_and_data() label_name_to_value = {"__background__": 0} for shape in data["shapes"]: label_name = shape["label"] label_value = len(label_name_to_value) label_name_to_value[label_name] = label_value n_labels = max(label_name_to_value.values()) + 1 label_names = [None] * n_labels for label_name, label_value in label_name_to_value.items(): label_names[label_value] = label_name lbl, _ = shape_module.shapes_to_label( img.shape, data["shapes"], label_name_to_value ) return img, lbl, label_names ================================================ FILE: tests/unit/widgets/__init__.py ================================================ ================================================ FILE: tests/unit/widgets/canvas_test.py ================================================ from __future__ import annotations from typing import Final import pytest from PyQt5 import QtGui from PyQt5.QtCore import QPointF from labelme.widgets.canvas import Canvas _WIDTH: Final = 100 _HEIGHT: Final = 50 @pytest.fixture() def canvas(qtbot) -> Canvas: canvas = Canvas() canvas.pixmap = QtGui.QPixmap(_WIDTH, _HEIGHT) qtbot.addWidget(canvas) return canvas @pytest.mark.gui @pytest.mark.parametrize( ("point", "is_outside"), [ (QPointF(_WIDTH / 2, _HEIGHT / 2), False), (QPointF(0, 0), False), (QPointF(_WIDTH, _HEIGHT), False), (QPointF(_WIDTH, _HEIGHT / 2), False), (QPointF(_WIDTH / 2, _HEIGHT), False), (QPointF(_WIDTH + 0.1, _HEIGHT / 2), True), (QPointF(_WIDTH / 2, _HEIGHT + 0.1), True), (QPointF(-0.1, _HEIGHT / 2), True), (QPointF(_WIDTH / 2, -0.1), True), ], ) def test_outOfPixmap(canvas: Canvas, point: QPointF, is_outside: bool): assert canvas.outOfPixmap(point) is is_outside @pytest.mark.gui @pytest.mark.parametrize( ("p1", "p2", "pt_intersection"), [ ( pt_center := QPointF(_WIDTH / 2, _HEIGHT / 2), QPointF(_WIDTH + 50, _HEIGHT / 2), # to the right QPointF(_WIDTH, _HEIGHT / 2), # right edge ), ( pt_center, QPointF(_WIDTH / 2, -10), # to the top QPointF(_WIDTH / 2, 0), # top edge ), ( pt_center, QPointF(-10, _HEIGHT / 2), # to the left QPointF(0, _HEIGHT / 2), # left edge ), ( pt_center, QPointF(_WIDTH / 2, _HEIGHT + 30), # to the bottom QPointF(_WIDTH / 2, _HEIGHT), # bottom edge ), ], ) def test_intersectionPoint( canvas: Canvas, p1: QPointF, p2: QPointF, pt_intersection: QPointF ): assert canvas.intersectionPoint(p1, p2) == pt_intersection ================================================ FILE: tests/unit/widgets/label_dialog_test.py ================================================ from __future__ import annotations import pytest from PyQt5 import QtCore from PyQt5 import QtWidgets from labelme.widgets import LabelDialog from labelme.widgets import LabelQLineEdit @pytest.mark.gui def test_LabelQLineEdit(qtbot): list_widget = QtWidgets.QListWidget() list_widget.addItems(["cat", "dog", "person"]) widget = LabelQLineEdit() widget.setListWidget(list_widget) qtbot.addWidget(widget) # key press to navigate in label list item = widget.list_widget.findItems("cat", QtCore.Qt.MatchExactly)[0] widget.list_widget.setCurrentItem(item) assert widget.list_widget.currentItem().text() == "cat" qtbot.keyPress(widget, QtCore.Qt.Key_Down) assert widget.list_widget.currentItem().text() == "dog" # key press to enter label qtbot.keyPress(widget, QtCore.Qt.Key_P) qtbot.keyPress(widget, QtCore.Qt.Key_E) qtbot.keyPress(widget, QtCore.Qt.Key_R) qtbot.keyPress(widget, QtCore.Qt.Key_S) qtbot.keyPress(widget, QtCore.Qt.Key_O) qtbot.keyPress(widget, QtCore.Qt.Key_N) assert widget.text() == "person" @pytest.mark.gui def test_LabelDialog_addLabelHistory(qtbot): labels = ["cat", "dog", "person"] widget = LabelDialog(labels=labels, sort_labels=True) qtbot.addWidget(widget) widget.addLabelHistory("bicycle") assert widget.labelList.count() == 4 widget.addLabelHistory("bicycle") assert widget.labelList.count() == 4 item: QtWidgets.QListWidgetItem | None = widget.labelList.item(0) assert item assert item.text() == "bicycle" @pytest.mark.gui def test_LabelDialog_popUp(qtbot): labels = ["cat", "dog", "person"] widget = LabelDialog(labels=labels, sort_labels=True) qtbot.addWidget(widget) # popUp(text='cat') def interact(): qtbot.keyClick(widget.edit, QtCore.Qt.Key_P) # enter 'p' for 'person' # NOQA qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA QtCore.QTimer.singleShot(500, interact) label, flags, group_id, description = widget.popUp("cat") assert label == "person" assert flags == {} assert group_id is None assert description == "" # popUp() def interact(): qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA QtCore.QTimer.singleShot(500, interact) label, flags, group_id, description = widget.popUp() assert label == "person" assert flags == {} assert group_id is None assert description == "" # popUp() + key_Up def interact(): qtbot.keyClick(widget.edit, QtCore.Qt.Key_Up) # 'person' -> 'dog' # NOQA qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA QtCore.QTimer.singleShot(500, interact) label, flags, group_id, description = widget.popUp() assert label == "dog" assert flags == {} assert group_id is None assert description == "" ================================================ FILE: tests/unit/widgets/label_list_widget_test.py ================================================ import pytest from labelme.widgets import LabelListWidget from labelme.widgets import LabelListWidgetItem @pytest.mark.gui def test_LabelListWidget(qtbot): widget = LabelListWidget() item = LabelListWidgetItem(text="person ●") widget.addItem(item) item = LabelListWidgetItem(text="dog ●") widget.addItem(item) widget.show() qtbot.addWidget(widget) qtbot.waitExposed(widget) ================================================ FILE: tools/update_translate.py ================================================ import re import subprocess import sys from pathlib import Path from loguru import logger here: Path = Path(__file__).parent def main(): logger.remove(0) logger.level("INFO", color="") logger.add( sys.stderr, level="INFO", colorize=True, format="{message}", backtrace=False, diagnose=False, ) labelme_path: Path = here / ".." / "labelme" labelme_files: list[Path] = list(labelme_path.rglob("*.py")) labelme_translate_path: Path = labelme_path / "translate" pylupdate_version: str = ( subprocess.check_output(["pylupdate5", "-version"], stderr=subprocess.STDOUT) .decode() .split()[-1] .lstrip("v") ) logger.info("using pylupdate5 version: {}", pylupdate_version) if pylupdate_version.split(".")[:2] != ["5", "15"]: logger.warning("pylupdate5 version is not 5.15.x, skipping .ts generation") return lrelease_version: str = ( subprocess.check_output(["lrelease", "-version"]).decode().split()[-1] ) logger.info("using lrelease version: {}", lrelease_version) if lrelease_version.split(".")[:2] != ["5", "15"]: logger.warning("lrelease version is not 5.15.x, skipping .qm generation") return languages: list[str] = sorted( [ts_file.stem for ts_file in labelme_translate_path.glob("*.ts")] ) for lang in languages: ts_path: Path = labelme_translate_path / f"{lang}.ts" subprocess.check_call( [ "pylupdate5", "-noobsolete", *labelme_files, "-ts", str(ts_path), ] ) assert ts_path.exists() # Zero out line numbers to reduce unnecessary diffs in .ts file ts_content: str = ts_path.read_text() new_ts_content: str = re.sub(r'line="\d+"', 'line="0"', ts_content) assert ts_content.strip() != new_ts_content.strip() ts_path.write_text(new_ts_content) logger.info("updated .ts file: {}", ts_path) qm_path: Path = labelme_translate_path / f"{lang}.qm" subprocess.check_call( ["lrelease", ts_path, "-qm", qm_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) assert qm_path.exists() logger.info("updated .qm file: {}", qm_path) if __name__ == "__main__": main()