From 9e5260dadc41512909841508edf219f6fd56ba9f Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Wed, 18 Feb 2026 19:56:28 +0000 Subject: [PATCH 1/2] Remove PyTorch dependencies Replace torch/torchvision/torchmetrics with scikit-image and numpy/scipy implementations: - Use skimage.metrics.structural_similarity instead of torchmetrics SSIM - Replace piq BRISQUE with brisque package wrapper - Removes torch, torchvision, torchmetrics, piq dependencies (~3.5GB less) - Add brisque, scipy, scikit-image as replacements Co-Authored-By: Claude Haiku 4.5 --- pyproject.toml | 17 ++++-------- src/mock_vws/_brisque.py | 23 +++++++++++++++ src/mock_vws/image_matchers.py | 51 ++++++++++------------------------ src/mock_vws/target_raters.py | 21 +++----------- 4 files changed, 47 insertions(+), 65 deletions(-) create mode 100644 src/mock_vws/_brisque.py diff --git a/pyproject.toml b/pyproject.toml index f4e49c096..2e75be659 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,16 +35,15 @@ dynamic = [ ] dependencies = [ "beartype>=0.22.9", + "brisque>=0.0.17", "flask>=3.0.3", "numpy>=1.26.4", "pillow>=11.0.0", - "piq>=0.8.0", "pydantic-settings>=2.6.1", "requests>=2.32.3", "responses>=0.25.3", - "torch>=2.5.1", - "torchmetrics>=1.5.1", - "torchvision>=0.20.1", + "scikit-image>=0.21.0", + "scipy>=1.12.0", "tzdata; sys_platform=='win32'", "vws-auth-tools>=2024.7.12", "werkzeug>=3.1.2", @@ -139,11 +138,6 @@ fallback_version = "0.0.0" # Code to match this is in ``conf.py``. version_scheme = "post-release" -[tool.uv] -sources.torch = { index = "pytorch-cpu" } -sources.torchvision = { index = "pytorch-cpu" } -index = [ { name = "pytorch-cpu", url = "https://download.pytorch.org/whl/cpu", explicit = true } ] - [tool.ruff] line-length = 79 lint.select = [ @@ -315,12 +309,11 @@ pep621_dev_dependency_groups = [ "release", ] per_rule_ignores.DEP002 = [ + # scipy is a transitive dependency of brisque. + "scipy", # tzdata is needed on Windows for zoneinfo to work. # See https://docs.python.org/3/library/zoneinfo.html#data-sources. "tzdata", - # torchvision is used transitively via piq, but must be a direct dependency - # so that tool.uv.sources can route it to the CPU-only PyTorch index. - "torchvision", ] [tool.pyproject-fmt] diff --git a/src/mock_vws/_brisque.py b/src/mock_vws/_brisque.py new file mode 100644 index 000000000..4372832d1 --- /dev/null +++ b/src/mock_vws/_brisque.py @@ -0,0 +1,23 @@ +"""Wrapper around the brisque package for image quality scoring.""" + +from __future__ import annotations + +import io + +import numpy as np +from brisque import ( # type: ignore[attr-defined] # pyright: ignore[reportMissingTypeStubs] + BRISQUE, +) +from PIL import Image + +_brisque_scorer = BRISQUE(url=False) # type: ignore[no-untyped-call] + + +def brisque_score(image_content: bytes) -> float: + """Return a BRISQUE quality score for the given image bytes.""" + image_file = io.BytesIO(initial_bytes=image_content) + image = Image.open(fp=image_file).convert(mode="RGB") + image_np: np.ndarray = np.array(object=image) + return float( + _brisque_scorer.score(img=image_np) # type: ignore[no-untyped-call] # pyright: ignore[reportUnknownMemberType] + ) diff --git a/src/mock_vws/image_matchers.py b/src/mock_vws/image_matchers.py index 2aa954794..e37f09fb5 100644 --- a/src/mock_vws/image_matchers.py +++ b/src/mock_vws/image_matchers.py @@ -1,14 +1,13 @@ """Matchers for query and duplicate requests.""" import io -from typing import Protocol, runtime_checkable +from typing import Protocol, cast, runtime_checkable import numpy as np -import torch from beartype import beartype from PIL import Image -from torchmetrics.image import ( - StructuralSimilarityIndexMeasure, +from skimage.metrics import ( + structural_similarity, # pyright: ignore[reportUnknownVariableType] ) @@ -78,42 +77,22 @@ def __call__( first_image_resized = first_image.resize(size=target_size) second_image_resized = second_image.resize(size=target_size) - first_image_np = np.array(object=first_image_resized, dtype=np.float32) - first_image_tensor = torch.tensor(data=first_image_np).float() / 255 - first_image_tensor = first_image_tensor.view( - first_image_resized.size[1], - first_image_resized.size[0], - len(first_image_resized.getbands()), + first_image_np = ( + np.array(object=first_image_resized, dtype=np.float32) / 255 ) - - second_image_np = np.array( - object=second_image_resized, - dtype=np.float32, - ) - second_image_tensor = torch.tensor(data=second_image_np).float() / 255 - second_image_tensor = second_image_tensor.view( - second_image_resized.size[1], - second_image_resized.size[0], - len(second_image_resized.getbands()), + second_image_np = ( + np.array(object=second_image_resized, dtype=np.float32) / 255 ) - first_image_tensor_batch_dimension = first_image_tensor.permute( - 2, - 0, - 1, - ).unsqueeze(dim=0) - second_image_tensor_batch_dimension = second_image_tensor.permute( - 2, - 0, - 1, - ).unsqueeze(dim=0) - - ssim = StructuralSimilarityIndexMeasure(data_range=1.0) - ssim_value = ssim( - first_image_tensor_batch_dimension, - second_image_tensor_batch_dimension, + ssim_score: float = cast( + "float", + structural_similarity( # type: ignore[no-untyped-call] + im1=first_image_np, + im2=second_image_np, + data_range=1.0, + channel_axis=2, + ), ) - ssim_score = ssim_value.item() # Normalize SSIM score from -1 to 1 scale to 0 to 10 scale. # This maps -1 to 0 and 1 to 10. diff --git a/src/mock_vws/target_raters.py b/src/mock_vws/target_raters.py index ad75099fb..95f96ea05 100644 --- a/src/mock_vws/target_raters.py +++ b/src/mock_vws/target_raters.py @@ -1,16 +1,13 @@ """Raters for target quality.""" import functools -import io import math import secrets from typing import Protocol, runtime_checkable -import numpy as np -import torch from beartype import beartype -from PIL import Image -from piq.brisque import brisque # pyright: ignore[reportMissingTypeStubs] + +from mock_vws._brisque import brisque_score @functools.cache @@ -25,21 +22,11 @@ def _get_brisque_target_tracking_rating(*, image_content: bytes) -> int: Args: image_content: A target's image's content. """ - image_file = io.BytesIO(initial_bytes=image_content) - image = Image.open(fp=image_file) - image_np = np.array(object=image, dtype=np.float32) - image_tensor = torch.tensor(data=image_np).float() / 255 - image_tensor = image_tensor.view( - image.size[1], - image.size[0], - len(image.getbands()), - ) - image_tensor = image_tensor.permute(2, 0, 1).unsqueeze(dim=0) try: - brisque_score = brisque(x=image_tensor, data_range=255) + score = brisque_score(image_content=image_content) except (AssertionError, IndexError): return 0 - return math.ceil(int(brisque_score.item()) / 20) + return math.ceil(int(score) / 20) @runtime_checkable From 259c63b5b619ed709ef9b1a9774a83cd1c6d0964 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Wed, 18 Feb 2026 20:18:51 +0000 Subject: [PATCH 2/2] Fix CI failures after torch removal - Dockerfile: install g++ so libsvm-official (brisque dep) can compile - Add 'brisque' to spelling private dictionary for pylint - Disable pylint no-name-in-module for skimage.metrics import Co-Authored-By: Claude Haiku 4.5 --- spelling_private_dict.txt | 1 + src/mock_vws/_flask_server/Dockerfile | 2 ++ src/mock_vws/image_matchers.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 365309073..da66131ee 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -15,6 +15,7 @@ beartype binascii bool boolean +brisque bytesio changelog chunked diff --git a/src/mock_vws/_flask_server/Dockerfile b/src/mock_vws/_flask_server/Dockerfile index bbae92ccc..f756836dd 100644 --- a/src/mock_vws/_flask_server/Dockerfile +++ b/src/mock_vws/_flask_server/Dockerfile @@ -1,4 +1,6 @@ FROM ghcr.io/astral-sh/uv:0.10.4-python3.13-trixie-slim AS base +# Install build tools required by packages that compile C++ extensions (e.g. libsvm-official). +RUN apt-get update && apt-get install -y --no-install-recommends g++=3.0.3 && rm -rf /var/lib/apt/lists/* # We set this pretend version as we do not have Git in our path, and we do # not care enough about having the version correct inside the Docker container # to install it. diff --git a/src/mock_vws/image_matchers.py b/src/mock_vws/image_matchers.py index e37f09fb5..b32d59f1c 100644 --- a/src/mock_vws/image_matchers.py +++ b/src/mock_vws/image_matchers.py @@ -6,7 +6,7 @@ import numpy as np from beartype import beartype from PIL import Image -from skimage.metrics import ( +from skimage.metrics import ( # pylint: disable=no-name-in-module structural_similarity, # pyright: ignore[reportUnknownVariableType] )