Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[flake8]
max-line-length = 88
extend-ignore = E203,E501,W503,E402,F401,F403,F841,B006,B007,B008,B009,C416,E262
exclude =
.git,
__pycache__,
.venv,
venv,
build,
dist,
*.egg-info,
.mypy_cache,
.pytest_cache
per-file-ignores =
__init__.py:F401,F403,E402
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ contact_links:
about: Browse code examples and tutorials
- name: πŸ” Existing Issues
url: https://github.com/DeepKnowledge1/AnomaVision/issues?q=is%3Aissue
about: Search existing issues to see if your question was already answered
about: Search existing issues to see if your question was already answered
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,44 @@ concurrency:
cancel-in-progress: true

jobs:
# NEW JOB: Code Quality Enforcement
code-quality:
name: Code Quality Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Install dependencies with dev extras
run: uv sync --extra dev --extra cpu --locked

- name: Check formatting with Black
run: uv run black --check --diff .

# - name: Check import sorting with isort
# run: uv run --with isort isort --check-only --diff .

- name: Lint with flake8
run: |
uv run flake8 \
--max-line-length=88 \
--extend-ignore=E203,E501,W503,E402,F401,F403,F841,B006,B007,B008,B009,C416,E262 \
anomavision/ anodet/ apps/ tests/

tests:
name: Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
# Run tests only after code quality passes
needs: code-quality
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
Expand Down Expand Up @@ -44,6 +79,8 @@ jobs:
cuda-matrix-check:
name: Verify CUDA Resolvers
runs-on: ubuntu-latest
# Run CUDA checks only after code quality passes
needs: code-quality
steps:
- uses: actions/checkout@v4

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,4 @@ images/
!docs/images/archti.png
!docs/AnomaVision_vs_Anomalib.pdf
poetry publish --build --username __toke.txt
fix_flake.py
50 changes: 27 additions & 23 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
repos:
# Black: auto-format Python code (fixes E501 line too long)
# Black: auto-format Python code
- repo: https://github.com/psf/black
rev: 24.3.0
rev: 26.3.1
hooks:
- id: black
args: [--line-length=88]
args: [--line-length=88, --target-version=py310]

# isort: automatically sort and group imports (fixes E402)
# isort: automatically sort and group imports
- repo: https://github.com/pycqa/isort
rev: 5.13.2
rev: 8.0.1 # FIXED: was indented too far
hooks:
- id: isort
args: [--profile=black]
args:
- --profile=black
- --line-length=88
- --force-single-line-imports

# # flake8: linting for style & errors
# - repo: https://github.com/pycqa/flake8
# rev: 7.0.0
# hooks:
# - id: flake8
# additional_dependencies: [
# flake8-bugbear,
# flake8-comprehensions
# ]

# # mypy: static type checking
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.8.0
# hooks:
# - id: mypy
# flake8: ONLY block on critical errors (F811 redefinitions)
# All other warnings are ignored to allow legacy code to pass
- repo: https://github.com/pycqa/flake8
rev: 7.3.0
hooks:
- id: flake8
args:
- --max-line-length=88
- --extend-ignore=E203,E501,W503,E402,F401,F403,F841,B006,B007,B008,B009,C416,E262
additional_dependencies:
- flake8-bugbear
- flake8-comprehensions

# Basic cleanup hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v6.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace

# Run pytest LAST
- repo: local
hooks:
- id: pytest
name: Run pytest
# entry: poetry run pytest
entry: uv run pytest
language: system
pass_filenames: false

# Stop on first failure (don't run pytest if flake8 fails)
fail_fast: true
2 changes: 1 addition & 1 deletion anodet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@
except ImportError as e:
print(f"❌ FATAL: Failed to import from anomavision: {e}")
raise ImportError(
f"Failed to import from 'anomavision' package. "
"Failed to import from 'anomavision' package. "
f"Please ensure AnomaVision is properly installed: {e}"
) from e

Expand Down
1 change: 0 additions & 1 deletion anomavision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from .datasets.mvtec_dataset import MVTecDataset
from .feature_extraction import ResnetEmbeddingsExtractor
from .padim import Padim

from .sampling_methods.kcenter_greedy import kCenterGreedy
from .test import optimal_threshold, visualize_eval_data, visualize_eval_pair
from .utils import get_logger # Export for users
Expand Down
12 changes: 12 additions & 0 deletions anomavision/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def create_parser() -> argparse.ArgumentParser:

try:
from anomavision import __version__

version_str = f"AnomaVision {__version__}"
except ImportError:
version_str = "AnomaVision"
Expand Down Expand Up @@ -87,8 +88,10 @@ def create_parser() -> argparse.ArgumentParser:
# works immediately with no changes needed here.
# ============================================================


def _add_train_parser(subparsers) -> None:
from anomavision.train import create_parser as _cp

subparsers.add_parser(
"train",
help="Train a new anomaly detection model",
Expand All @@ -99,6 +102,7 @@ def _add_train_parser(subparsers) -> None:

def _add_export_parser(subparsers) -> None:
from anomavision.export import create_parser as _cp

subparsers.add_parser(
"export",
help="Export trained model to different formats",
Expand All @@ -109,6 +113,7 @@ def _add_export_parser(subparsers) -> None:

def _add_detect_parser(subparsers) -> None:
from anomavision.detect import create_parser as _cp

subparsers.add_parser(
"detect",
help="Run inference on images",
Expand All @@ -119,6 +124,7 @@ def _add_detect_parser(subparsers) -> None:

def _add_eval_parser(subparsers) -> None:
from anomavision.eval import create_parser as _cp

subparsers.add_parser(
"eval",
help="Evaluate model performance",
Expand All @@ -132,30 +138,36 @@ def _add_eval_parser(subparsers) -> None:
# No sys.argv manipulation. No double-parsing.
# ============================================================


def _dispatch_train(args: argparse.Namespace) -> None:
from anomavision import train

train.main(args)


def _dispatch_export(args: argparse.Namespace) -> None:
from anomavision import export

export.main(args)


def _dispatch_detect(args: argparse.Namespace) -> None:
from anomavision import detect

detect.main(args)


def _dispatch_eval(args: argparse.Namespace) -> None:
from anomavision import eval as eval_module # 'eval' shadows the Python builtin

eval_module.main(args)


# ============================================================
# Entry point
# ============================================================


def main() -> None:
parser = create_parser()
args = parser.parse_args()
Expand Down
8 changes: 4 additions & 4 deletions anomavision/datasets/MQTTSource.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from anomavision.datasets.StreamSource import StreamSource

from typing import Optional
import threading
import time
from queue import Queue, Empty, Full
from queue import Empty, Full, Queue
from typing import Optional

import cv2
import numpy as np

from anomavision.datasets.StreamSource import StreamSource

try:
import paho.mqtt.client as mqtt
except ImportError as e:
Expand Down
7 changes: 2 additions & 5 deletions anomavision/datasets/StreamDataset.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
from typing import Optional, Tuple, Union, List
from typing import List, Optional, Tuple, Union

import numpy as np
import torch
from PIL import Image
from torch.utils.data import IterableDataset

from anomavision.datasets.StreamSource import StreamSource
from anomavision.utils import (
create_image_transform,
create_mask_transform,
)
from anomavision.utils import create_image_transform, create_mask_transform


class StreamDataset(IterableDataset):
Expand Down
3 changes: 1 addition & 2 deletions anomavision/datasets/StreamSource.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from abc import ABC, abstractmethod


# ========== BASE STRATEGY INTERFACE ==========


class StreamSource(ABC):
@abstractmethod
def connect(self) -> None:
Expand All @@ -27,4 +27,3 @@ def disconnect(self) -> None:
def is_connected(self) -> bool:
"""Check if source is connected."""
pass

29 changes: 4 additions & 25 deletions anomavision/datasets/StreamSourceFactory.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Dict, Any

from anomavision.datasets.StreamDataset import StreamDataset
from anomavision.datasets.StreamSource import StreamSource
import contextlib
from typing import Any, Dict

from torch.utils.data import IterableDataset

from anomavision.datasets.MQTTSource import MQTTSource
from anomavision.datasets.StreamDataset import StreamDataset
from anomavision.datasets.StreamSource import StreamSource
from anomavision.datasets.TCPsource import TCPSource
from anomavision.datasets.VideoSource import VideoSource
from anomavision.datasets.WebcamSource import WebcamSource
Expand Down Expand Up @@ -65,24 +65,3 @@ def create(config: Dict[str, Any]) -> StreamSource:
)

raise ValueError(f"Unknown StreamSource type: {source_type}")


# Optional: convenience ctor on StreamDataset
class StreamDataset(IterableDataset):
# your existing __init__ here...

@classmethod
def from_config(
cls,
source_config: Dict[str, Any],
**dataset_kwargs: Any,
) -> "StreamDataset":
"""
Build StreamDataset from a source config dict + dataset kwargs.

Example:
source_cfg = {"type": "webcam", "camera_id": 0}
ds = StreamDataset.from_config(source_cfg, max_frames=100)
"""
source = StreamSourceFactory.create(source_config)
return cls(source=source, **dataset_kwargs)
19 changes: 13 additions & 6 deletions anomavision/datasets/TCPsource.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from anomavision.datasets.StreamSource import StreamSource

from typing import Optional
import socket
import threading
import time
from queue import Queue, Empty, Full
from queue import Empty, Full, Queue
from typing import Optional

import cv2
import numpy as np

from anomavision.datasets.StreamSource import StreamSource


class TCPSource(StreamSource):
"""
Expand Down Expand Up @@ -132,7 +132,10 @@ def _receiver_loop(self) -> None:
if payload_len <= 0:
continue

if self.max_message_size is not None and payload_len > self.max_message_size:
if (
self.max_message_size is not None
and payload_len > self.max_message_size
):
# Drain and skip (best-effort)
_ = self._recv_exact(payload_len)
continue
Expand Down Expand Up @@ -238,4 +241,8 @@ def disconnect(self) -> None:

def is_connected(self) -> bool:
with self._lock:
return self._connected and not self._stop_event.is_set() and self.socket is not None
return (
self._connected
and not self._stop_event.is_set()
and self.socket is not None
)
Loading
Loading