diff --git a/.github/workflows/auto_bump_pr_merge.yml b/.github/workflows/auto_bump_pr_merge.yml new file mode 100644 index 000000000..03be19fe4 --- /dev/null +++ b/.github/workflows/auto_bump_pr_merge.yml @@ -0,0 +1,157 @@ +name: Auto Merge + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + BUMP: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Define allowed and ignored files + run: | + ALLOWED_FILES="package.xml CMakeLists.txt CHANGELOG.rst setup.py" + echo "ALLOWED_FILES=$ALLOWED_FILES" >> $GITHUB_ENV + + IGNORED_FILES=".github .jenkins" + echo "IGNORED_FILES=$IGNORED_FILES" >> $GITHUB_ENV + + - name: Check if PR should be merged + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + BASE_BRANCH="${{ github.event.pull_request.base.ref }}" + HEAD_BRANCH="${{ github.event.pull_request.head.ref }}" + CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only) + + mapfile -t change_files < <(echo "$CHANGED_FILES") + + ALLOWED_FILES_STRING="${{ env.ALLOWED_FILES }}" + IFS=' ' read -r -a allowed_files <<< "$ALLOWED_FILES_STRING" + + IGNORED_FILES_STRING="${{ env.IGNORED_FILES }}" + IFS=' ' read -r -a ignored_files <<< "$IGNORED_FILES_STRING" + + filtered_change_files=() + for change_file in "${change_files[@]}"; do + exclude_item=false + for ignored_file in "${ignored_files[@]}"; do + if [[ "$change_file" == *"$ignored_file"* ]]; then + exclude_item=true + break; + fi + done + if [ "$exclude_item" = false ]; then + filtered_change_files+=("$change_file") + fi + done + + only_allowed_files=true + for filtered_change_file in "${filtered_change_files[@]}"; do + allowed_change_file=false + filtered_change_file_name=$(basename "$filtered_change_file") + for allowed_file in "${allowed_files[@]}"; do + if [ "$filtered_change_file_name" = "$allowed_file" ]; then + allowed_change_file=true + break; + fi + done + if [ "$allowed_change_file" = false ]; then + only_allowed_files=false + break; + fi + done + + if [[ "$BASE_BRANCH" == "main" && "$HEAD_BRANCH" == "develop" && "$only_allowed_files" == true ]]; then + echo "should_merge=true" >> $GITHUB_ENV + else + echo "should_merge=false" >> $GITHUB_ENV + fi + env: + GITHUB_TOKEN: ${{ secrets.SECRETS_GITHUB_TOKEN }} + + - name: Wait and Check PR status + if: env.should_merge == 'true' + run: | + PR_NUMBER=$(jq -r '.pull_request.number' < $GITHUB_EVENT_PATH) + COMMIT_SHA=$(gh pr view $PR_NUMBER --json commits --jq '.commits[-1].oid') + + echo "pr_status_all_success=false" >> $GITHUB_ENV + for i in {1..30}; do + echo "Checking status checks... (Attempt $i)" + + STATUS_CHECKS=$(gh api graphql -f query=' + query($owner: String!, $repo: String!, $commit: String!) { + repository(owner: $owner, name: $repo) { + object(expression: $commit) { + ... on Commit { + status { + contexts { + context + state + } + } + } + } + } + }' -f owner="${{ github.repository_owner }}" -f repo="${{ github.event.repository.name }}" -f commit="$COMMIT_SHA" --jq '.data.repository.object.status.contexts') + + SUCCESSFUL_CHECKS=0 + TOTAL_CHECKS=0 + for status in $(echo "$STATUS_CHECKS" | jq -r '.[] | @base64'); do + _jq() { + echo "${status}" | base64 --decode | jq -r ${1} + } + + CONTEXT=$(_jq '.context') + STATE=$(_jq '.state') + + if [[ "$CONTEXT" == "automerge" ]]; then + continue + fi + + TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) + + if [[ "$STATE" == "SUCCESS" ]]; then + SUCCESSFUL_CHECKS=$((SUCCESSFUL_CHECKS + 1)) + fi + done + + if [[ "$SUCCESSFUL_CHECKS" -eq "$TOTAL_CHECKS" && "$TOTAL_CHECKS" -gt 0 ]]; then + echo "pr_status_all_success=true" >> $GITHUB_ENV + break + fi + sleep 60 + done + env: + GITHUB_TOKEN: ${{ secrets.SECRETS_GITHUB_TOKEN }} + + - name: Check all checkboxes + if: env.should_merge == 'true' && env.pr_status_all_success == 'true' + run: | + gh pr view ${{ github.event.pull_request.number }} --json body --jq .body > pr_body.txt + + PR_BODY=$(cat pr_body.txt) + + CHECKED_BODY=$(echo "$PR_BODY" | sed 's/\[ \]/[x]/g') + + if [[ "$PR_BODY" != "$CHECKED_BODY" ]]; then + gh pr edit ${{ github.event.pull_request.number }} --body "$CHECKED_BODY" + echo "All checkboxes have been checked" + else + echo "No checkboxes found or all checkboxes are already checked" + fi + env: + GITHUB_TOKEN: ${{ secrets.SECRETS_GITHUB_TOKEN }} + + - name: Merged PR + if: success() && env.should_merge == 'true' && env.pr_status_all_success == 'true' + run: | + COMMENT="본 패키지는 변경 사항이 없는 리포지토리 / 패키지임을 확인하여 병합되었습니다!" + gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT" + gh pr merge ${{ github.event.pull_request.number }} --merge --admin + env: + GITHUB_TOKEN: ${{ secrets.SECRETS_GITHUB_TOKEN }} + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index dfecb4eba..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,122 +0,0 @@ -name: ci - -on: - push: - branches: - - main - pull_request: - -jobs: - build: - - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ['3.9'] - PYTEST_QT_API: [pyqt5, pyside2] - - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - - - name: Install system dependencies - shell: bash -l {0} - run: | - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - sudo apt-get install -y coreutils - sudo apt-get install -y xvfb herbstluftwm - elif [ "${{ matrix.os }}" = "macos-latest" ]; then - brew install coreutils - brew install --cask xquartz - fi - - - name: Set up Python - shell: bash -l {0} - run: | - conda install -q -y python=${{ matrix.python-version }} - which python - python --version - pip --version - - - name: Install dependencies - shell: bash -l {0} - run: | - if [ "${{ matrix.PYTEST_QT_API }}" = "pyside2" ]; then - conda install -q -y pyside2 -c conda-forge - else - conda install -q -y pyqt=5 - fi - pip install pytest 'pytest-qt<4' - - - name: Install main - shell: bash -l {0} - run: | - pip install . - - - name: Lint with flake8 - shell: bash -l {0} - if: matrix.os != 'windows-latest' - run: | - pip install hacking==4.1.0 - flake8 . - - - name: Black - shell: bash -l {0} - if: matrix.os != 'windows-latest' - run: | - pip install black==22.1.0 - black --line-length 79 --check --diff labelme/ - - - name: Test with pytest - shell: bash -l {0} - if: matrix.os != 'windows-latest' && !(matrix.os == 'macos-latest' && matrix.PYTEST_QT_API == 'pyside2') - env: - PYTEST_QT_API: ${{ matrix.PYTEST_QT_API }} - run: | - # open virtual display - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - export DISPLAY=:99.0 - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset - (herbstluftwm )& - else - (sudo Xvfb :99 -ac -screen 0 1024x768x8 )& - fi - # run test - MPLBACKEND='agg' pytest tests/ - - - name: Run examples - shell: bash -l {0} - if: matrix.os != 'windows-latest' - env: - MPLBACKEND: agg - run: | - labelme --help - labelme --version - (cd examples/primitives && labelme_json_to_dataset primitives.json && rm -rf primitives_json) - (cd examples/tutorial && rm -rf apc2016_obj3_json && labelme_json_to_dataset 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 -- .) - - pip install lxml # 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 -- .) - - pip install cython && 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: Run pyinstaller - shell: bash -l {0} - if: matrix.PYTEST_QT_API == 'pyqt5' - run: | - # Build the standalone executable - pip install PyQt5 -I --use-feature=2020-resolver # https://stackoverflow.com/a/68784578 - pip install pyinstaller - pyinstaller labelme.spec - dist/labelme --version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 650a54776..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: release - -on: - push: - tags: - - 'v*' - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - - name: Create release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - draft: false - prerelease: false - - - name: Create release url file - run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt - - - name: Save release url file for publish - uses: actions/upload-artifact@v1 - with: - name: release_url - path: release_url.txt - - publish: - needs: [release] - - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: '3.7' - - - name: Install main - shell: bash -l {0} - run: | - pip install . - - - name: Run pyinstaller - shell: bash -l {0} - run: | - pip install pyinstaller - pyinstaller labelme.spec - - - name: Load release url file from release job - uses: actions/download-artifact@v1 - with: - name: release_url - - - name: Get release file name & upload url - id: get_release_info - run: | - echo "::set-output name=upload_url::$(cat release_url/release_url.txt)" - - - name: Upload release executable on macOS & Linux - id: upload_release_executable_macos_linux - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.get_release_info.outputs.upload_url }} - asset_path: ./dist/labelme - asset_name: labelme-${{ runner.os }} - asset_content_type: application/octet-stream - if: runner.os != 'Windows' - - - name: Upload release executable on Windows - id: upload_release_executable_windows - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.get_release_info.outputs.upload_url }} - asset_path: ./dist/labelme.exe - asset_name: Labelme.exe - asset_content_type: application/octet-stream - if: runner.os == 'Windows' - - - name: Create dmg for macOS - run: | - npm install -g create-dmg - cd dist - create-dmg Labelme.app || test -f Labelme\ 0.0.0.dmg - mv Labelme\ 0.0.0.dmg Labelme.dmg - if: runner.os == 'macOS' - - - name: Upload release app on macOS - id: upload_release_app_macos - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.get_release_info.outputs.upload_url }} - asset_path: ./dist/Labelme.dmg - asset_name: Labelme.dmg - asset_content_type: application/octet-stream - if: runner.os == 'macOS' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3cbc23c8c..07c432eca 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Changelog for package labelme ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +5.23.0 (2024-08-08) +------------------- +* Added a version checker and enhanced the logging mode for labeling both polygons and bounding boxes +* Contributors: Dongyun Kim + 5.22.0 (2024-07-18) ------------------- diff --git a/labelme/app.py b/labelme/app.py index 0318bee9d..17a313b97 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -38,6 +38,7 @@ from labelme.widgets import UniqueLabelQListWidget from labelme.widgets import ZoomWidget from labelme.widgets import WorkerNameWindow +from labelme.widgets import InvalidVersionWindow # FIXME # - [medium] Set max zoom value to something big enough for FitWidth/Window @@ -61,6 +62,22 @@ def __init__( output_file=None, output_dir=None, ): + version_checker = utils.VersionChecker() + version_checker.check_version() + if not version_checker.internet_status: + version_popup = InvalidVersionWindow( + 0, + version_checker.local_version, + version_checker.github_version) + version_popup.setModal(True) + version_popup.exec_() + if not version_checker.version_result: + version_popup = InvalidVersionWindow( + 1, + version_checker.local_version, + version_checker.github_version) + version_popup.setModal(True) + version_popup.exec_() self._last_label_names = [] if output is not None: logger.warning( @@ -1044,14 +1061,12 @@ def setClean(self): self.actions.createRectangleMode.setEnabled(True) self.actions.createMode.setEnabled(True) else: - if 'detection' in self._classType or self._classType is None: + if 'indoor' in self._classType: self.actions.createRectangleMode.setEnabled(True) - else: - self.actions.createRectangleMode.setEnabled(False) - if 'segmentation' in self._classType or self._classType is None: self.actions.createMode.setEnabled(True) else: - self.actions.createMode.setEnabled(False) + self.actions.createRectangleMode.setEnabled('detection' in self._classType) + self.actions.createMode.setEnabled('segmentation' in self._classType) self.actions.createCircleMode.setEnabled(False) self.actions.createLineMode.setEnabled(False) self.actions.createPointMode.setEnabled(False) @@ -1064,7 +1079,7 @@ def setClean(self): # if self.hasLabelFile(): # self.actions.deleteFile.setEnabled(True) # else: - # self.actions.deleteFile.setEnabled(False) + # self.actions.deleteFile.setEnabled(False) self.actions.deleteFile.setEnabled(False) def toggleActions(self, value=True): @@ -1171,69 +1186,70 @@ def toggleDrawMode(self, edit=True, createMode="polygon"): self.canvas.setEditing(edit) self.canvas.createMode = createMode if edit: - if self._classType is None or 'segmentation' in self._classType: + if 'indoor' in self._classType: self.actions.createMode.setEnabled(True) - else: - self.actions.createMode.setEnabled(False) - if self._classType is None or 'detection' in self._classType: self.actions.createRectangleMode.setEnabled(True) else: - self.actions.createRectangleMode.setEnabled(False) + self.actions.createRectangleMode.setEnabled( + self._classType is None or 'segmentation' in self._classType) + self.actions.createMode.setEnabled( + self._classType is None or 'detection' in self._classType) self.actions.createCircleMode.setEnabled(False) self.actions.createLineMode.setEnabled(False) self.actions.createPointMode.setEnabled(False) self.actions.createLineStripMode.setEnabled(False) else: - if createMode == "polygon": - self.actions.createMode.setEnabled(False) - if self._classType is None or 'detection' in self._classType: - self.actions.createRectangleMode.setEnabled(True) - else: - self.actions.createRectangleMode.setEnabled(False) - self.actions.createCircleMode.setEnabled(False) - self.actions.createLineMode.setEnabled(False) - self.actions.createPointMode.setEnabled(False) - self.actions.createLineStripMode.setEnabled(False) - elif createMode == "rectangle": - if self._classType is None or 'segmentation' in self._classType: - self.actions.createMode.setEnabled(True) - else: - self.actions.createMode.setEnabled(False) - self.actions.createRectangleMode.setEnabled(False) - self.actions.createCircleMode.setEnabled(False) - self.actions.createLineMode.setEnabled(False) - self.actions.createPointMode.setEnabled(False) - self.actions.createLineStripMode.setEnabled(False) - elif createMode == "line": - self.actions.createMode.setEnabled(True) - self.actions.createRectangleMode.setEnabled(True) - self.actions.createCircleMode.setEnabled(True) - self.actions.createLineMode.setEnabled(False) - self.actions.createPointMode.setEnabled(True) - self.actions.createLineStripMode.setEnabled(True) - elif createMode == "point": - self.actions.createMode.setEnabled(True) - self.actions.createRectangleMode.setEnabled(True) - self.actions.createCircleMode.setEnabled(True) - self.actions.createLineMode.setEnabled(True) - self.actions.createPointMode.setEnabled(False) - self.actions.createLineStripMode.setEnabled(True) - elif createMode == "circle": - self.actions.createMode.setEnabled(True) - self.actions.createRectangleMode.setEnabled(True) - self.actions.createCircleMode.setEnabled(False) - self.actions.createLineMode.setEnabled(True) - self.actions.createPointMode.setEnabled(True) - self.actions.createLineStripMode.setEnabled(True) - elif createMode == "linestrip": + if 'indoor' in self._classType: self.actions.createMode.setEnabled(True) self.actions.createRectangleMode.setEnabled(True) - self.actions.createCircleMode.setEnabled(True) - self.actions.createLineMode.setEnabled(True) - self.actions.createPointMode.setEnabled(True) - self.actions.createLineStripMode.setEnabled(False) else: - raise ValueError("Unsupported createMode: %s" % createMode) + if createMode == "polygon": + self.actions.createMode.setEnabled(False) + self.actions.createRectangleMode.setEnabled( + self._classType is None or 'detection' in self._classType) + self.actions.createCircleMode.setEnabled(False) + self.actions.createLineMode.setEnabled(False) + self.actions.createPointMode.setEnabled(False) + self.actions.createLineStripMode.setEnabled(False) + elif createMode == "rectangle": + self.actions.createMode.setEnabled( + self._classType is None or 'segmentation' in self._classType + ) + self.actions.createRectangleMode.setEnabled(False) + self.actions.createCircleMode.setEnabled(False) + self.actions.createLineMode.setEnabled(False) + self.actions.createPointMode.setEnabled(False) + self.actions.createLineStripMode.setEnabled(False) + elif createMode == "line": + self.actions.createMode.setEnabled(True) + self.actions.createRectangleMode.setEnabled(True) + self.actions.createCircleMode.setEnabled(True) + self.actions.createLineMode.setEnabled(False) + self.actions.createPointMode.setEnabled(True) + self.actions.createLineStripMode.setEnabled(True) + elif createMode == "point": + self.actions.createMode.setEnabled(True) + self.actions.createRectangleMode.setEnabled(True) + self.actions.createCircleMode.setEnabled(True) + self.actions.createLineMode.setEnabled(True) + self.actions.createPointMode.setEnabled(False) + self.actions.createLineStripMode.setEnabled(True) + elif createMode == "circle": + self.actions.createMode.setEnabled(True) + self.actions.createRectangleMode.setEnabled(True) + self.actions.createCircleMode.setEnabled(False) + self.actions.createLineMode.setEnabled(True) + self.actions.createPointMode.setEnabled(True) + self.actions.createLineStripMode.setEnabled(True) + elif createMode == "linestrip": + self.actions.createMode.setEnabled(True) + self.actions.createRectangleMode.setEnabled(True) + self.actions.createCircleMode.setEnabled(True) + self.actions.createLineMode.setEnabled(True) + self.actions.createPointMode.setEnabled(True) + self.actions.createLineStripMode.setEnabled(False) + else: + raise ValueError("Unsupported createMode: %s" % createMode) self.actions.editMode.setEnabled(not edit) def setEditMode(self): @@ -1449,7 +1465,7 @@ def loadLabels(self, shapes): ) for x, y in points: shape.addPoint(QtCore.QPointF(x, y)) - if len(shape.points) == 2 and "detection" in self.labelFile.classType: + if len(shape.points) == 2: shape.align_points() shape.updateCorners() shape.close() @@ -1767,7 +1783,8 @@ def loadFile(self, filename=None): if "outdoor" in self.labelFile.classType: size_weight = 50 Shape.label_font_size = size_weight * self.labelFile.imageHeight / 2160 - if self.labelFile.classType == "indoor_detection-ev_state" or self.labelFile.classType == "indoor_detection-ev_button": + if (self.labelFile.classType == "indoor_detection-ev_state" or + self.labelFile.classType == "indoor_detection-ev_button"): Shape.point_size = 3 self.labelDialog.default_completion_mode() self.canvas.updateType(self.labelFile.classType) @@ -1917,6 +1934,9 @@ def enableSaveImageWithData(self, enabled): self.actions.saveWithImageData.setChecked(enabled) def closeEvent(self, event): + self.canvas.measureWorkingTime.measure_time() + self.canvas.measureWorkingTime.working_count += 1 + self.canvas.measureWorkingTime.write_crypt_description(self.imagePath) if not self.mayContinue(): event.ignore() self.settings.setValue( @@ -1957,7 +1977,9 @@ def loadRecent(self, filename): def openPrevImg(self, _value=False): if self.canvas.drawing() and self.canvas.current: return - + self.canvas.measureWorkingTime.measure_time() + self.canvas.measureWorkingTime.working_count += 1 + self.canvas.measureWorkingTime.write_crypt_description(self.imagePath) keep_prev = self._config["keep_prev"] if QtWidgets.QApplication.keyboardModifiers() == ( Qt.ControlModifier | Qt.ShiftModifier @@ -1987,6 +2009,8 @@ def openNextImg(self, _value=False, load=True): if self.canvas.drawing() and self.canvas.current: return if self.imagePath: + self.canvas.measureWorkingTime.measure_time() + self.canvas.measureWorkingTime.working_count += 1 self.canvas.measureWorkingTime.write_crypt_description(self.imagePath) if self._config["auto_save"] or self.actions.saveAuto.isChecked(): label_file = osp.splitext(self.imagePath)[0] + ".json" diff --git a/labelme/cli/draw_object_label.py b/labelme/cli/draw_object_label.py index 308232dd0..b2c0b2200 100644 --- a/labelme/cli/draw_object_label.py +++ b/labelme/cli/draw_object_label.py @@ -41,7 +41,7 @@ def __init__(self, CONFIG, input_dir, popup=None): else: num_core = 1 - if not popup == None: + if popup is not None: popup.show() print('===========================================') @@ -58,8 +58,8 @@ def __init__(self, CONFIG, input_dir, popup=None): for i in range(process_num): pool.map( self.convert_bounding_box, - json_list[i * num_core : (i+1) * num_core]) - if not popup == None: + json_list[i * num_core:(i+1) * num_core]) + if popup is not None: popup.set_progress(i / process_num * 100) pool.close() @@ -114,7 +114,7 @@ def convert_bounding_box(self, json_file): class_name = shape['label'] - if not class_name in class_names: + if class_name not in class_names: print('Generating dataset from : {0}'.format(json_file)) print('wrong class name : {0}'.format(class_name)) check_wrong_class = True @@ -140,12 +140,13 @@ def convert_objects(input_dir, popup=None): print('Opening data file : {0}'.format(class_data_yaml)) f = open(class_data_yaml, 'r') CONFIG = yaml.load(f, Loader=yaml.FullLoader) - except: - print('Error opening data yaml file!') + except Exception as e: + print('Error opening data yaml file! {0}'.format(e)) sys.exit() Convertor(CONFIG, input_dir, popup) + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('input_dir', help='input annotated directory') diff --git a/labelme/cli/draw_segment_label.py b/labelme/cli/draw_segment_label.py index 4f7b3e984..97f01a3a7 100644 --- a/labelme/cli/draw_segment_label.py +++ b/labelme/cli/draw_segment_label.py @@ -41,7 +41,7 @@ def __init__(self, CONFIG, input_dir, popup=None): else: num_core = 1 - if not popup == None: + if popup is not None: popup.show() print('===========================================') @@ -59,8 +59,8 @@ def __init__(self, CONFIG, input_dir, popup=None): for i in range(process_num): pool.map( self.multi_convert_json_to_mask, - json_list[i * num_core : (i+1) * num_core]) - if not popup == None: + json_list[i * num_core: (i+1) * num_core]) + if popup is not None: popup.set_progress(int(i / process_num * 100)) pool.close() @@ -68,7 +68,7 @@ def __init__(self, CONFIG, input_dir, popup=None): print('Completed convert [{0}] folder'.format(self.folder_name)) - if not popup == None: + if popup is not None: popup.close() def get_class(self, class_type): @@ -170,7 +170,8 @@ def multi_convert_json_to_mask(self, json_file): self.overlayed_image_dir, image_name)) - except: + except Exception as e: + print('Unexpected error: {0}'.format(e)) pass @@ -181,8 +182,8 @@ def convert_segments(input_dir, popup=None): print('Opening data file : {0}'.format(class_data_yaml)) f = open(class_data_yaml, 'r') CONFIG = yaml.load(f, Loader=yaml.FullLoader) - except: - print('Error opening data yaml file!') + except Exception as e: + print('Error opening data yaml file! {0}'.format(e)) sys.exit() Convertor(CONFIG, input_dir, popup) diff --git a/labelme/config/__init__.py b/labelme/config/__init__.py index 052b7690d..967bf204a 100644 --- a/labelme/config/__init__.py +++ b/labelme/config/__init__.py @@ -42,7 +42,7 @@ def get_default_config(): else: create_time = osp.getmtime(user_config_file) target_time = time.mktime( - datetime.datetime.strptime('2024-03-11 18:00:00', '%Y-%m-%d %H:%M:%S').timetuple()) + datetime.datetime.strptime('2024-07-29 21:00:00', '%Y-%m-%d %H:%M:%S').timetuple()) if create_time < target_time: try: shutil.copy(config_file, user_config_file) diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml index c8e1d59df..61147654e 100644 --- a/labelme/config/default_config.yaml +++ b/labelme/config/default_config.yaml @@ -34,8 +34,7 @@ labels_class: ['stop', 'walk', 'light-out', 'countdown-walk', 'countdown-light-out'] indoor_segmentation: default: - ['in_panel', 'out_panel', 'rect-btn', 'circle-btn', 'ellipse-btn', 'rect-circle-btn', 'rect-rect-btn', 'special-btn', - 'sticker', 'admin-key', 'indicator', 'handi-symbol', 'screw', 'others'] + ['rect-btn', 'circle-btn', 'robot', 'sticker', 'admin-key', 'handi-symbol', 'screw', 'others'] indoor_detection-ev_state: default: ['Door_open', 'Door_close', 'Door_moving', 'Door_sticker', @@ -47,11 +46,7 @@ labels_class: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'A', 'B', 'C', 'E', 'F', 'G', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'U', 'alarm', 'call', 'star', '-', 'key', 'etc', - 'open_←|→', 'close_→|←', 'open_◀▶', 'close_▶◀', 'open_<>', 'close_><', 'open_◀||▶', 'close_▶||◀', 'open_◁▷', 'close_▷◁', 'open_◀|▶', 'close_▶|◀', 'open_◁|▷', 'close_▷|◁', 'open_<|>', 'close_>|<', - 'up_∧', 'down_v', 'up_∧∧', 'down_vv', 'up_↑', 'down_↓', 'up_♠', 'down_♥', 'up_♤', 'down_♡', 'up_▲', 'down_▼', 'up_△', 'down_▽', - 'edge_red', 'edge_green', 'edge_blue', 'edge_yellow', 'edge_white', 'number_red', 'number_green', 'number_blue', 'number_yellow', 'number_white', 'number_black', - 'edge_circle', 'edge_rect', 'edge_ellipse', 'edge_line', 'edge_dot', - 'edge_on', 'edge_off', 'number_on', 'number_off'] + 'open', 'close', 'on', 'off'] default_shape_color: [0, 0, 255] shape_color: manual # null, 'auto', 'manual' shift_auto_shape_color: 0 diff --git a/labelme/shape.py b/labelme/shape.py index 002be1443..018da9e8b 100644 --- a/labelme/shape.py +++ b/labelme/shape.py @@ -263,6 +263,24 @@ def paint(self, painter): # for the 1st vertex, and make it non-filled, which # may be desirable. # self.drawVertex(vrtx_path, 0) + # Draw text at the top-left + if self.paint_label: + min_y_label = int(1.0 * self.label_font_size) + (center_x, center_y) = self.calculate_polygon_center(self.points) + if center_x != sys.maxsize and center_y != sys.maxsize: + font = QtGui.QFont() + font.setPointSize(int(self.label_font_size)) + font.setBold(True) + painter.setFont(font) + if self.label is None: + self.label = "" + if center_y < min_y_label: + center_y += min_y_label + center_ratio = 0.5 + width_ratio = 0.8 + x_offset = int( + len(self.label) * center_ratio * self.label_font_size * width_ratio) + painter.drawText(int(center_x) - x_offset, int(center_y), self.label) for i, p in enumerate(self.points): line_path.lineTo(p) @@ -281,6 +299,28 @@ def paint(self, painter): ) painter.fillPath(line_path, color) + def calculate_polygon_center(self, qt_points): + # Polygon 데이터의 center를계산 + points = [(point.x(), point.y()) for point in qt_points] + n = len(points) + area = 0.0001 + for i in range(n): + x0, y0 = points[i] + x1, y1 = points[(i + 1) % n] + area += x0 * y1 - x1 * y0 + area *= 0.5 + cx = 0 + cy = 0 + for i in range(n): + x0, y0 = points[i] + x1, y1 = points[(i + 1) % n] + factor = (x0 * y1 - x1 * y0) + cx += (x0 + x1) * factor + cy += (y0 + y1) * factor + cx /= (6 * area) + cy /= (6 * area) + return (cx, cy) + def drawVertex(self, path, i, point): d = self.point_size / self.scale shape = self.point_type diff --git a/labelme/utils/__init__.py b/labelme/utils/__init__.py index b6bd866f6..5325e2af1 100644 --- a/labelme/utils/__init__.py +++ b/labelme/utils/__init__.py @@ -30,3 +30,5 @@ from .qt import distance from .qt import distancetoline from .qt import fmtShortcut + +from .version_checker import VersionChecker diff --git a/labelme/utils/_io.py b/labelme/utils/_io.py index 3cf7ca721..dd785d6f0 100644 --- a/labelme/utils/_io.py +++ b/labelme/utils/_io.py @@ -22,6 +22,7 @@ def lblsave(filename, lbl): "Please consider using the .npy format." % filename ) + def lblsave_old(filename, lbl, names, class_rgb, segmentation_class): here = osp.dirname(osp.abspath(__file__)) diff --git a/labelme/utils/draw.py b/labelme/utils/draw.py index 64260517b..910fa41ca 100644 --- a/labelme/utils/draw.py +++ b/labelme/utils/draw.py @@ -1,4 +1,5 @@ import io +import math import os.path as osp import numpy as np @@ -49,7 +50,7 @@ def getClsId(label, names, seg_class, file_name=''): cls_id = 1 names.append(label) else: - if not label in names: + if label not in names: names.append(label) cls_id = len(names) # print("get new class") @@ -74,7 +75,7 @@ def shapes_to_label(img_shape, shapes, label_name_to_value, seg_class, type='cla points = shape['points'] label = shape['label'] shape_type = shape.get('shape_type', None) - class_names=[] + class_names = [] if type == 'class': cls_name = label # print(cls_name) @@ -86,14 +87,13 @@ def shapes_to_label(img_shape, shapes, label_name_to_value, seg_class, type='cla cls_id, names = getClsId(label, names, seg_class, file_name) - if names == None: + if names is None: return None, None else: mask = shape_to_mask(img_shape[:2], points, shape_type) cls[mask] = cls_id if type == 'instance': ins[mask] = ins_id - if type == 'instance': return cls, ins return cls, names @@ -147,7 +147,7 @@ def label2rgb( # print("lbl", list(lbl[1000])) # print("test :", colormap) lbl_viz = colormap[lbl] - + # print("lbl_viz :", lbl_viz) lbl_viz[lbl == -1] = (0, 0, 0) # unlabeled # lbl_viz[lbl == 1] = (68, 68, 128) @@ -162,7 +162,15 @@ def label2rgb( return lbl_viz -def draw_label(label, img=None, label_names=None, names=None, class_rgb=None, segmentation_class=None, colormap=None, **kwargs): +def draw_label( + label, + img=None, + label_names=None, + names=None, + class_rgb=None, + segmentation_class=None, + colormap=None, + **kwargs): """Draw pixel-wise label with colorization and label names. label: ndarray, (H, W) @@ -173,12 +181,12 @@ def draw_label(label, img=None, label_names=None, names=None, class_rgb=None, se List of label names. """ import matplotlib.pyplot as plt - + if label_names is None: label_names = [str(l) for l in range(label.max() + 1)] colormap = _validate_colormap(colormap, len(label_names)) - + class_rgb = np.array(class_rgb, np.uint8) for index, labels in enumerate(names): @@ -209,7 +217,7 @@ def draw_label(label, img=None, label_names=None, names=None, class_rgb=None, se # plt.close() # plt.switch_backend(backend_org) - + out_size = (label_viz.shape[1], label_viz.shape[0]) out = PIL.Image.open(f).resize(out_size, PIL.Image.BILINEAR).convert('RGB') out = np.asarray(out) @@ -242,13 +250,22 @@ def draw_instances( font = PIL.ImageFont.truetype(font_path, font_size) colormap = label_colormap(255) - + for bbox, label, caption in zip(bboxes, labels, captions): color = colormap[label] color = tuple((color * 255).astype(np.uint8).tolist()) xmin, ymin, xmax, ymax = bbox - draw.line([(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin)], width=5 ,fill=color) + draw.line( + [ + (xmin, ymin), + (xmin, ymax), + (xmax, ymax), + (xmax, ymin), + (xmin, ymin) + ], + width=5, + fill=color) # draw.rectangle((xmin, ymin, xmax, ymax), outline=color) draw.text((xmin, ymin), caption, font=font) diff --git a/labelme/utils/measure_working_time.py b/labelme/utils/measure_working_time.py index 283b59efe..a7ee29b8d 100644 --- a/labelme/utils/measure_working_time.py +++ b/labelme/utils/measure_working_time.py @@ -2,16 +2,20 @@ import os import datetime as dt import sys -import platform + class MeasureTime(): - def __init__(self, crypto_mode = True): + def __init__(self, crypto_mode=True): self.crypto_mode = crypto_mode self.working_total_time = 0 self.break_total_time = 0 - self.pre_interaction_time = dt.datetime.now().hour * 3600 + dt.datetime.now().minute * 60 + dt.datetime.now().second + dt.datetime.now().microsecond * 0.000001 + self.pre_interaction_time = ( + dt.datetime.now().hour * 3600 + + dt.datetime.now().minute * 60 + + dt.datetime.now().second + + dt.datetime.now().microsecond * 0.000001) self.break_standard_time = 10 - self.limit_time = 3600 * 24 # 24시 이후 diff time이 - 값이 나오는 현상 방지 + self.limit_time = 3600 * 24 # 24시 이후 diff time이 - 값이 나오는 현상 방지 self.working_count = 0 self.worker_name = '' self.init_write_worker_name = True @@ -40,19 +44,24 @@ def read_crypt_description(self, file_path): return decode_text def write_crypt_description(self, save_path): - folder_path , img_name = os.path.split(save_path) + folder_path, img_name = os.path.split(save_path) with open(os.path.join(folder_path, 'Cache.txt'), "a") as f: - description_text = img_name + ' - working_time : ' + str(self.working_total_time) + ', break_time : ' + str(self.break_total_time) + ', working_count : ' + str(self.working_count) + description_text = ( + img_name + + ' - working_time : ' + + str(self.working_total_time) + + ', break_time : ' + str(self.break_total_time) + + ', working_count : ' + str(self.working_count)) if self.init_write_worker_name: text = self.worker_name + '\n' + description_text self.init_write_worker_name = False - else : + else: text = description_text if self.crypto_mode: encrypted_text = self.encrypt_text(text) f.write(str(encrypted_text) + '\n') - else : + else: f.write(text + '\n') self.working_total_time = 0 self.break_total_time = 0 @@ -70,12 +79,19 @@ def decrypt_text(key, encrypted_text): def measure_time(self): cur_interaction_time_date = dt.datetime.now() - cur_interaction_time = cur_interaction_time_date.hour * 3600 + cur_interaction_time_date.minute * 60 + cur_interaction_time_date.second + cur_interaction_time_date.microsecond * 0.000001 + cur_interaction_time = ( + cur_interaction_time_date.hour * 3600 + + cur_interaction_time_date.minute * 60 + + cur_interaction_time_date.second + + cur_interaction_time_date.microsecond * 0.000001) diff_interaction_time = cur_interaction_time - self.pre_interaction_time if diff_interaction_time < 0: - diff_interaction_time = cur_interaction_time + self.limit_time - self.pre_interaction_time + diff_interaction_time = ( + cur_interaction_time + + self.limit_time - + self.pre_interaction_time) self.pre_interaction_time = cur_interaction_time if diff_interaction_time > self.break_standard_time: self.break_total_time += diff_interaction_time - else : + else: self.working_total_time += diff_interaction_time diff --git a/labelme/utils/version_checker.py b/labelme/utils/version_checker.py new file mode 100644 index 000000000..3e30565f4 --- /dev/null +++ b/labelme/utils/version_checker.py @@ -0,0 +1,46 @@ +import os +import requests +import xml.etree.ElementTree as ET + + +class VersionChecker: + def __init__(self): + self.url = 'https://raw.githubusercontent.com/ROBOTIS-move/labelme/develop/version.xml' + self.current_path = os.path.dirname(os.path.abspath(__file__)) + self.local_path = self.current_path + '/../../version.xml' + + def fetch_file(self, mode): + if mode == 'github': + try: + response = requests.get(self.url) + return True, response.content + + except requests.exceptions.ConnectionError: + print('INTERNET CONNECTION Fail') + return False, None + elif mode == 'local': + try: + with open(self.local_path, 'r', encoding='utf-8') as file: + return True, file.read() + except Exception as e: + print('Error:', e) + return False, None + + def get_version(self, mode): + file_open_check, file_content = self.fetch_file(mode) + if mode == 'github' and not file_open_check: + return False, None + root = ET.fromstring(file_content) + version = root.find('version').text + return True, version + + def check_version(self): + internet_status, github_version = self.get_version('github') + _, local_version = self.get_version('local') + self.github_version = github_version + self.local_version = local_version + if github_version != local_version: + self.version_result = False + else: + self.version_result = True + self.internet_status = internet_status diff --git a/labelme/widgets/__init__.py b/labelme/widgets/__init__.py index 535f21ab3..8cbb8fe83 100644 --- a/labelme/widgets/__init__.py +++ b/labelme/widgets/__init__.py @@ -24,3 +24,5 @@ from .convert_label_popup import ConvertLabelPopup from .worker_name_dialog import WorkerNameWindow + +from .invalid_version_popup import InvalidVersionWindow diff --git a/labelme/widgets/canvas.py b/labelme/widgets/canvas.py index 6da85a6ec..8000a121a 100644 --- a/labelme/widgets/canvas.py +++ b/labelme/widgets/canvas.py @@ -134,7 +134,7 @@ def storeShapes(self): 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 = self.shapesBackups[-self.num_backups - 1:] self.shapesBackups.append(shapesBackup) @property @@ -586,14 +586,26 @@ def calculateOffsets(self, point): def boundedMoveVertex(self, pos): index, shape = self.hVertex, self.hShape shape_index = index - if "segmentation" in self.labelType: - point = shape.points[shape_index] - else: - if shape_index > 1: - shape_index -= 2 - point = shape.corners[shape_index] + if "indoor" in self.labelType: + if len(shape.points) > 2: + # Polygon points + point = shape.points[shape_index] else: + # Box Points + if shape_index > 1: + shape_index -= 2 + point = shape.corners[shape_index] + else: + point = shape.points[shape_index] + else: + if "segmentation" in self.labelType: point = shape.points[shape_index] + else: + if shape_index > 1: + shape_index -= 2 + point = shape.corners[shape_index] + else: + point = shape.points[shape_index] if self.outOfPixmap(pos): pos = self.intersectionPoint(point, pos) shape.moveVertexBy(index, pos - point) diff --git a/labelme/widgets/image_popup.py b/labelme/widgets/image_popup.py index 3b3b67901..b53543c76 100644 --- a/labelme/widgets/image_popup.py +++ b/labelme/widgets/image_popup.py @@ -7,7 +7,6 @@ import PIL.Image from qtpy import QT_VERSION -from qtpy import QtCore from qtpy import QtGui from qtpy import QtWidgets diff --git a/labelme/widgets/invalid_version_popup.py b/labelme/widgets/invalid_version_popup.py new file mode 100644 index 000000000..6c40ea6f9 --- /dev/null +++ b/labelme/widgets/invalid_version_popup.py @@ -0,0 +1,49 @@ +from PyQt5.QtWidgets import QLabel, QPushButton, QVBoxLayout, QDialog +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont + + +class InvalidVersionWindow(QDialog): + def __init__(self, mode, local_version, github_version): + super().__init__() + self.mode = mode + print(self.mode) + self.alarm_text = { + 0: [ + 'Internet Checker', + ( + '!!! 버전 정보를 확인할 수 없습니다. !!!\n' + '네트워크 상태를 확인해주세요. !!!' + )], + 1: [ + 'Version Checker', + ( + f'!!! 버전 정보가 맞지 않습니다. !!!\n' + '최신 버전을 다운로드 해주세요.\n' + f'현재 버전 : {local_version}, 최신 버전 : {github_version}' + )]} + self.initUI() + + def initUI(self): + self.setWindowTitle(self.alarm_text[self.mode][0]) + self.setGeometry(300, 300, 500, 500) + + layout = QVBoxLayout() + + label = QLabel(self.alarm_text[self.mode][1]) + font = QFont() + font.setPointSize(20) # 원하는 폰트 크기로 설정 + label.setFont(font) + label.setAlignment(Qt.AlignCenter) # 중앙 정렬 + label.setStyleSheet("color: red;") + + confirm_button = QPushButton('확인', self) + confirm_button.clicked.connect(self.onConfirm) + + layout.addWidget(label) + layout.addWidget(confirm_button) + + self.setLayout(layout) + + def onConfirm(self): + exit(0) diff --git a/labelme/widgets/worker_name_dialog.py b/labelme/widgets/worker_name_dialog.py index 9250ef29f..293af8f80 100644 --- a/labelme/widgets/worker_name_dialog.py +++ b/labelme/widgets/worker_name_dialog.py @@ -1,7 +1,8 @@ import sys -from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QDialog +from PyQt5.QtWidgets import QLabel, QLineEdit, QPushButton, QVBoxLayout, QDialog import os + class WorkerNameWindow(QDialog): def __init__(self): super().__init__() diff --git a/package.xml b/package.xml index a157914a6..7accedc76 100644 --- a/package.xml +++ b/package.xml @@ -2,7 +2,7 @@ labelme - 5.22.0 + 5.23.0 Labeling tools. diff --git a/setup.py b/setup.py index e2ae88b5f..dfe4d0410 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,8 @@ def get_install_requires(): "qtpy!=1.11.2", "termcolor", "cryptography", - "lxml" + "lxml", + "requests" ] # Find python binding for qt with priority: diff --git a/version.xml b/version.xml new file mode 100644 index 000000000..473e3bcc5 --- /dev/null +++ b/version.xml @@ -0,0 +1,11 @@ + + + + 1.1.0 + Pyo + SungHyeon Oh + GPLv3 + Jongsub Yu + Jaehun Park + Dongyun Kim +