diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index c05c076cc5..a9b57b3a02 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -26,7 +26,7 @@ jobs: steps: - name: CLA Assistant if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I sign the CLA') || github.event_name == 'pull_request_target' - uses: contributor-assistant/github-action@v2.4.0 + uses: contributor-assistant/github-action@v2.5.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Must be repository secret PAT diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1545674c78..8dc0858b9c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -76,6 +76,10 @@ jobs: cp -R ../site/* . echo "$INDEXNOW_KEY" > "$INDEXNOW_KEY.txt" git add . - LATEST_HASH=$(git rev-parse --short=7 HEAD) - git commit -m "Update Docs for 'ultralytics ${{ steps.check_pypi.outputs.version }} - $LATEST_HASH'" - git push https://$PERSONAL_ACCESS_TOKEN@github.com/ultralytics/docs.git gh-pages + if git diff --staged --quiet; then + echo "No changes to commit" + else + LATEST_HASH=$(git rev-parse --short=7 HEAD) + git commit -m "Update Docs for 'ultralytics ${{ steps.check_pypi.outputs.version }} - $LATEST_HASH'" + git push https://$PERSONAL_ACCESS_TOKEN@github.com/ultralytics/docs.git gh-pages + fi diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2b9c5c877..61a6cc0ca6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,12 +16,12 @@ jobs: publish: if: github.repository == 'ultralytics/ultralytics' && github.actor == 'glenn-jocher' name: Publish - runs-on: macos-14 + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: "0" # pulls all commits (needed correct last updated dates in Docs) + token: ${{ secrets.PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} # use your PAT here - name: Git config run: | git config --global user.name "UltralyticsAssistant" @@ -29,45 +29,72 @@ jobs: - name: Set up Python environment uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.x" cache: "pip" # caching pip dependencies - name: Install dependencies run: | - python -m pip install --upgrade pip wheel build twine - pip install -e ".[dev]" openai --extra-index-url https://download.pytorch.org/whl/cpu + python -m pip install --upgrade pip wheel + pip install openai requests build twine toml - name: Check PyPI version shell: python run: | import os - import ultralytics - from ultralytics.utils.checks import check_latest_pypi_version - latest_pypi_version = check_latest_pypi_version() - v_local = tuple(map(int, ultralytics.__version__.split('.'))) - v_pypi = tuple(map(int, latest_pypi_version.split('.'))) - print(f'Local version is {v_local}') - print(f'PyPI version is {v_pypi}') - d = [a - b for a, b in zip(v_local, v_pypi)] # diff - increment_patch = (d[0] == d[1] == 0) and (0 < d[2] < 3) # publish if patch version increments by 1 or 2 - increment_minor = (d[0] == 0) and (d[1] == 1) and v_local[2] == 0 # publish if minor version increments - increment = increment_patch or increment_minor - os.system(f'echo "increment={increment}" >> $GITHUB_OUTPUT') - os.system(f'echo "version={ultralytics.__version__}" >> $GITHUB_OUTPUT') - os.system(f'echo "previous_version={latest_pypi_version}" >> $GITHUB_OUTPUT') - if increment: - print('Local version is higher than PyPI version. Publishing new version to PyPI ✅.') + import requests + import toml + + # Load version and package name from pyproject.toml + pyproject = toml.load('pyproject.toml') + package_name = pyproject['project']['name'] + local_version = pyproject['project'].get('version', 'dynamic') + + # If version is dynamic, extract it from the specified file + if local_version == 'dynamic': + version_attr = pyproject['tool']['setuptools']['dynamic']['version']['attr'] + module_path, attr_name = version_attr.rsplit('.', 1) + with open(f"{module_path.replace('.', '/')}/__init__.py") as f: + local_version = next(line.split('=')[1].strip().strip("'\"") for line in f if line.startswith(attr_name)) + + print(f"Local Version: {local_version}") + + # Get online version from PyPI + response = requests.get(f"https://pypi.org/pypi/{package_name}/json") + online_version = response.json()['info']['version'] if response.status_code == 200 else None + print(f"Online Version: {online_version or 'Not Found'}") + + # Determine if a new version should be published + publish = False + if online_version: + local_ver = tuple(map(int, local_version.split('.'))) + online_ver = tuple(map(int, online_version.split('.'))) + major_diff = local_ver[0] - online_ver[0] + minor_diff = local_ver[1] - online_ver[1] + patch_diff = local_ver[2] - online_ver[2] + + publish = ( + (major_diff == 0 and minor_diff == 0 and 0 < patch_diff <= 2) or + (major_diff == 0 and minor_diff == 1 and local_ver[2] == 0) or + (major_diff == 1 and local_ver[1] == 0 and local_ver[2] == 0) + ) + else: + publish = True # First release + + os.system(f'echo "increment={publish}" >> $GITHUB_OUTPUT') + os.system(f'echo "version={local_version}" >> $GITHUB_OUTPUT') + os.system(f'echo "previous_version={online_version or "N/A"}" >> $GITHUB_OUTPUT') + + if publish: + print('Ready to publish new version to PyPI ✅.') id: check_pypi - name: Publish new tag if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True' run: | - git tag -a "v${{ steps.check_pypi.outputs.version }}" -m "$(git log -1 --pretty=%B)" || true # i.e. "v0.1.2 commit message" - git push origin "v${{ steps.check_pypi.outputs.version }}" || true + git tag -a "v${{ steps.check_pypi.outputs.version }}" -m "$(git log -1 --pretty=%B)" # i.e. "v0.1.2 commit message" + git push origin "v${{ steps.check_pypi.outputs.version }}" - name: Publish new release if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True' env: - OPENAI_AZURE_API_KEY: ${{ secrets.OPENAI_AZURE_API_KEY }} - OPENAI_AZURE_ENDPOINT: ${{ secrets.OPENAI_AZURE_ENDPOINT }} - OPENAI_AZURE_API_VERSION: ${{ secrets.OPENAI_AZURE_API_VERSION }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} CURRENT_TAG: ${{ steps.check_pypi.outputs.version }} PREVIOUS_TAG: ${{ steps.check_pypi.outputs.previous_version }} shell: python @@ -79,26 +106,18 @@ jobs: import subprocess # Retrieve environment variables - OPENAI_AZURE_API_KEY = os.getenv('OPENAI_AZURE_API_KEY') - OPENAI_AZURE_ENDPOINT = os.getenv('OPENAI_AZURE_ENDPOINT') - OPENAI_AZURE_API_VERSION = os.getenv('OPENAI_AZURE_API_VERSION') + OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') GITHUB_TOKEN = os.getenv('GITHUB_TOKEN') CURRENT_TAG = os.getenv('CURRENT_TAG') PREVIOUS_TAG = os.getenv('PREVIOUS_TAG') # Check for required environment variables - if not all([OPENAI_AZURE_API_KEY, OPENAI_AZURE_ENDPOINT, OPENAI_AZURE_API_VERSION, GITHUB_TOKEN, CURRENT_TAG, PREVIOUS_TAG]): - print(OPENAI_AZURE_API_KEY) - print(OPENAI_AZURE_ENDPOINT) - print(OPENAI_AZURE_API_VERSION) - print(GITHUB_TOKEN) - print(CURRENT_TAG) - print(PREVIOUS_TAG) + if not all([OPENAI_API_KEY, GITHUB_TOKEN, CURRENT_TAG, PREVIOUS_TAG]): raise ValueError("One or more required environment variables are missing.") latest_tag = f"v{CURRENT_TAG}" previous_tag = f"v{PREVIOUS_TAG}" - repo = 'ultralytics/ultralytics' + repo = os.getenv('GITHUB_REPOSITORY') headers = {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3.diff"} # Get the diff between the tags @@ -106,29 +125,23 @@ jobs: response = requests.get(url, headers=headers) diff = response.text if response.status_code == 200 else f"Failed to get diff: {response.content}" - # Set up client - client = openai.AzureOpenAI( - api_key=OPENAI_AZURE_API_KEY, - api_version=OPENAI_AZURE_API_VERSION, - azure_endpoint=OPENAI_AZURE_ENDPOINT - ) - + # Get summary messages = [ { "role": "system", - "content": "You are an Ultralytics AI assistant skilled in software development and technical communication. Your task is to summarize GitHub releases from Ultralytics in a way that is detailed, accurate, and understandable to both expert developers and non-expert users. Focus on highlighting the key changes and their impact in simple and intuitive terms." + "content": "You are an Ultralytics AI assistant skilled in software development and technical communication. Your task is to summarize GitHub releases in a way that is detailed, accurate, and understandable to both expert developers and non-expert users. Focus on highlighting the key changes and their impact in simple and intuitive terms." }, { "role": "user", - "content": f"Summarize the updates made in the [Ultralytics](https://ultralytics.com) '{latest_tag}' tag, focusing on major changes, their purpose, and potential impact. Keep the summary clear and suitable for a broad audience. Add emojis to enliven the summary. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n" + "content": f"Summarize the updates made in the '{latest_tag}' tag, focusing on major changes, their purpose, and potential impact. Keep the summary clear and suitable for a broad audience. Add emojis to enliven the summary. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n" f"## 🌟 Summary (single-line synopsis)\n" f"## 📊 Key Changes (bullet points highlighting any major changes)\n" f"## 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n" f"\n\nHere's the release diff:\n\n{diff[:300000]}", } ] - - completion = client.chat.completions.create(model="gpt-4o-2024-05-13", messages=messages) + client = openai.OpenAI(api_key=OPENAI_API_KEY) + completion = client.chat.completions.create(model="gpt-4o-2024-08-06", messages=messages) summary = completion.choices[0].message.content.strip() # Get the latest commit message @@ -177,7 +190,7 @@ jobs: uses: slackapi/slack-github-action@v1.26.0 with: payload: | - {"text": " GitHub Actions success for ${{ github.workflow }} ✅\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* NEW 'ultralytics ${{ steps.check_pypi.outputs.version }}' pip package published 😃\n*Job Status:* ${{ job.status }}\n*Pull Request:* ${{ env.PR_TITLE }}\n"} + {"text": " GitHub Actions success for ${{ github.workflow }} ✅\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* NEW '${{ github.repository }} v${{ steps.check_pypi.outputs.version }}' pip package published 😃\n*Job Status:* ${{ job.status }}\n*Pull Request:* ${{ env.PR_TITLE }}\n"} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }} - name: Notify on Slack (Failure) diff --git a/docs/build_docs.py b/docs/build_docs.py index 8399296f7b..829801b5e4 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -214,7 +214,7 @@ def convert_plaintext_links_to_html(content): def remove_macros(): - # Delete the /macros directory and sitemap.xml.gz from the built site + """Removes the /macros directory and related entries in sitemap.xml from the built site.""" shutil.rmtree(SITE / "macros", ignore_errors=True) (SITE / "sitemap.xml.gz").unlink(missing_ok=True) diff --git a/docs/en/datasets/detect/visdrone.md b/docs/en/datasets/detect/visdrone.md index 5f485af9d4..00d84b10e9 100644 --- a/docs/en/datasets/detect/visdrone.md +++ b/docs/en/datasets/detect/visdrone.md @@ -8,6 +8,17 @@ keywords: VisDrone, drone dataset, computer vision, object detection, object tra The [VisDrone Dataset](https://github.com/VisDrone/VisDrone-Dataset) is a large-scale benchmark created by the AISKYEYE team at the Lab of Machine Learning and Data Mining, Tianjin University, China. It contains carefully annotated ground truth data for various computer vision tasks related to drone-based image and video analysis. +

+
+ +
+ Watch: How to Train Ultralytics YOLO Models on the VisDrone Dataset for Drone Image Analysis +

+ VisDrone is composed of 288 video clips with 261,908 frames and 10,209 static images, captured by various drone-mounted cameras. The dataset covers a wide range of aspects, including location (14 different cities across China), environment (urban and rural), objects (pedestrians, vehicles, bicycles, etc.), and density (sparse and crowded scenes). The dataset was collected using various drone platforms under different scenarios and weather and lighting conditions. These frames are manually annotated with over 2.6 million bounding boxes of targets such as pedestrians, cars, bicycles, and tricycles. Attributes like scene visibility, object class, and occlusion are also provided for better data utilization. ## Dataset Structure diff --git a/examples/YOLOv8-CPP-Inference/main.cpp b/examples/YOLOv8-CPP-Inference/main.cpp index 6d1ba988f5..fe040c8634 100644 --- a/examples/YOLOv8-CPP-Inference/main.cpp +++ b/examples/YOLOv8-CPP-Inference/main.cpp @@ -24,7 +24,7 @@ int main(int argc, char **argv) // // Note that in this example the classes are hard-coded and 'classes.txt' is a place holder. - Inference inf(projectBasePath + "/yolov8s.onnx", cv::Size(640, 480), "classes.txt", runOnGPU); + Inference inf(projectBasePath + "/yolov8s.onnx", cv::Size(640, 640), "classes.txt", runOnGPU); std::vector imageNames; imageNames.push_back(projectBasePath + "/ultralytics/assets/bus.jpg"); diff --git a/ultralytics/__init__.py b/ultralytics/__init__.py index 8636ceb1e1..93ca50b1ac 100644 --- a/ultralytics/__init__.py +++ b/ultralytics/__init__.py @@ -1,6 +1,6 @@ # Ultralytics YOLO 🚀, AGPL-3.0 license -__version__ = "8.2.78" +__version__ = "8.2.79" import os diff --git a/ultralytics/nn/modules/head.py b/ultralytics/nn/modules/head.py index 2add5654aa..0d658197d3 100644 --- a/ultralytics/nn/modules/head.py +++ b/ultralytics/nn/modules/head.py @@ -8,6 +8,7 @@ import torch import torch.nn as nn from torch.nn.init import constant_, xavier_uniform_ +from ultralytics.utils import MACOS from ultralytics.utils.tal import TORCH_1_10, dist2bbox, dist2rbox, make_anchors from .block import DFL, BNContrastiveHead, ContrastiveHead, Proto @@ -151,13 +152,16 @@ class Detect(nn.Module): boxes = torch.gather(boxes, dim=1, index=index.repeat(1, 1, boxes.shape[-1])) scores = torch.gather(scores, dim=1, index=index.repeat(1, 1, scores.shape[-1])) - # NOTE: simplify but result slightly lower mAP + # NOTE: simplify result but slightly lower mAP # scores, labels = scores.max(dim=-1) # return torch.cat([boxes, scores.unsqueeze(-1), labels.unsqueeze(-1)], dim=-1) scores, index = torch.topk(scores.flatten(1), max_det, axis=-1) labels = index % nc index = index // nc + # Set int64 dtype for MPS and CoreML compatibility to avoid 'gather_along_axis' ops error + if MACOS: + index = index.to(torch.int64) boxes = boxes.gather(dim=1, index=index.unsqueeze(-1).repeat(1, 1, boxes.shape[-1])) return torch.cat([boxes, scores.unsqueeze(-1), labels.unsqueeze(-1).to(boxes.dtype)], dim=-1)