Skip to content

Commit bcc9a60

Browse files
MechaCritterclaude
andauthored
Bugfix/fix mypy errors (#13)
* Fix mypy arg-type errors Add read_image_rgb helper that raises FileNotFoundError when cv2.imread returns None, and count queries during iteration in top_k_accuracy instead of calling len() on an Iterable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy assignment errors Add missing '| None' to parameters defaulting to None, declare attribute placeholders with their real types, and correct the similarity_func type: it returns a similarity matrix (np.ndarray), not a float. ImageEncoderBase now requires a feature extractor and defaults similarity_func to cosine_similarity instead of None. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy attr-defined errors Import FeatureExtractorBase and SimilarityMetric from their defining module, annotate the _pca/_clustering_model placeholders, and replace property fget/fset class access with a protected _set_clustering_model method that subclass setters call directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy import-untyped errors Add per-module ignore_missing_imports overrides for third-party libraries that ship without type stubs or a py.typed marker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy misc error Use non-inplace division in RootSIFT since in-place true division is not defined for the integer dtypes cv2 may return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy no-any-return errors Wrap untyped sklearn results in np.asarray, type the Lambda extractor's callable precisely, validate the submodule returned by getattr, and convert scipy .mat labels to int explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy no-untyped-call errors Annotate setup_logging, _set_clustering_model, _register_hook, download_oxford_flowers_data and the SIFT/RootSIFT constructors, and replace torch's untyped private Module._get_name() with type(...).__name__. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy no-untyped-def errors Annotate all remaining functions: typed decorator pattern for _check_output_shape and _tupleize_first_arg, an Encoder protocol for eval functions, property getter/setter annotations, and **kwargs types. similarity_score now honestly returns the np.ndarray similarity matrix it always produced at runtime. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy operator and return-value errors Type list_conv_layers as returning Conv2d layers so out_channels is an int instead of the Tensor | Module union from Module.__getattr__. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy safe-super errors Remove no-op super().__call__ invocations of the abstract base method from the feature extractors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mypy type-arg errors Parameterize the bare generic types: list[Any] for the heatmap matrix and Dataset[tuple[np.ndarray, int, str]] for OxfordFlowerDataset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix remaining mypy assignment errors Annotate the hook buffer as Tensor | None, widen the VLAD descriptor vector annotation, and use a separate variable for the spatial coordinates array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * bumpy Python version up to 3.12 * fixed div by zero error in top_k_accuracy * fixed wrong api in docstring (compute_vector -> encode) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1fa5449 commit bcc9a60

13 files changed

Lines changed: 200 additions & 130 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ This project uses [uv](https://github.com/astral-sh/uv) instead of `pip` for man
3535

3636
### Prerequisites
3737

38-
- Python >= 3.10
38+
- Python >= 3.12
3939
- [uv](https://github.com/astral-sh/uv)
4040

4141
### Steps

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
![License](https://img.shields.io/badge/license-MIT-brightgreen)
88
![Version](https://img.shields.io/badge/version-0.1.3-blue)
99
![Status](https://img.shields.io/badge/status-pre--release-orange)
10-
![Python](https://img.shields.io/badge/Python-3.10%2B-brightgreen)
10+
![Python](https://img.shields.io/badge/Python-3.12%2B-brightgreen)
1111
![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen)
1212

1313
# Welcome to `pyvisim`!

pyproject.toml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ license = {text = "MIT"}
1111
authors = [
1212
{name = "Nhat Huy Vu", email = "vunhathuy234@gmail.com"}
1313
]
14-
requires-python = ">=3.10"
14+
requires-python = ">=3.12"
1515
classifiers = [
1616
"Programming Language :: Python :: 3",
1717
"Operating System :: OS Independent",
@@ -62,7 +62,7 @@ fmt = [
6262
]
6363

6464
[tool.mypy]
65-
python_version = "3.14"
65+
python_version = "3.12"
6666
strict = true
6767
warn_return_any = true
6868
warn_unused_configs = true
@@ -77,8 +77,21 @@ no_implicit_optional = true
7777
check_untyped_defs = true
7878
exclude = ["^build/", "^dist/", "^\\.venv/", "^venv/"]
7979

80+
# Third-party libraries that ship without type stubs or a py.typed marker.
81+
[[tool.mypy.overrides]]
82+
module = [
83+
"joblib.*",
84+
"scipy.*",
85+
"seaborn.*",
86+
"sklearn.*",
87+
"torchvision.*",
88+
"tqdm.*",
89+
"yaml.*",
90+
]
91+
ignore_missing_imports = true
92+
8093
[tool.ruff]
81-
target-version = "py314"
94+
target-version = "py312"
8295
line-length = 88
8396

8497
[tool.ruff.lint]

pyvisim/_base_classes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ class SimilarityMetric(abc.ABC):
1717
@abc.abstractmethod
1818
def similarity_score(
1919
self, image1: Iterable[np.ndarray], image2: Iterable[np.ndarray]
20-
):
20+
) -> np.ndarray:
2121
"""
2222
Compute a similarity score between two images.
2323
2424
:param image1: First image
2525
:param image2: Second image
26-
:return: A similarity score
26+
:return: A similarity score matrix
2727
"""
2828
pass
2929

@@ -39,7 +39,7 @@ class FeatureExtractorBase(abc.ABC):
3939
_logger = logging.getLogger("Feature_Extractor")
4040

4141
@abc.abstractmethod
42-
def __call__(self, image: np.ndarray):
42+
def __call__(self, image: np.ndarray, /) -> np.ndarray:
4343
"""
4444
Extracts features from an image.
4545

pyvisim/_config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818

1919
# - Logging - #
20-
def setup_logging(default_path=LOGGER_FILE, default_level=logging.WARNING):
20+
def setup_logging(
21+
default_path: str | pathlib.Path = LOGGER_FILE,
22+
default_level: int = logging.WARNING,
23+
) -> None:
2124
"""Setup logging configuration"""
2225
try:
2326
with open(default_path) as f:

pyvisim/_utils.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from typing import Literal
1+
from typing import Any, Literal
22

3+
import cv2
34
import matplotlib.pyplot as plt
45
import numpy as np
56
import seaborn as sns
@@ -13,6 +14,20 @@
1314
from sklearn.metrics.pairwise import cosine_similarity as cs
1415

1516

17+
def read_image_rgb(path: str) -> np.ndarray:
18+
"""
19+
Read an image from disk and convert it to RGB.
20+
21+
:param path: Path to the image file.
22+
:return: Image as a NumPy array (H, W, C) in RGB order.
23+
:raises FileNotFoundError: If the image cannot be read from the given path.
24+
"""
25+
image = cv2.imread(path)
26+
if image is None:
27+
raise FileNotFoundError(f"Could not read image at '{path}'.")
28+
return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
29+
30+
1631
def cosine_similarity(x: np.ndarray, y: np.ndarray) -> np.ndarray:
1732
"""
1833
Compute the cosine similarity between two matrices.
@@ -33,7 +48,7 @@ def cosine_similarity(x: np.ndarray, y: np.ndarray) -> np.ndarray:
3348
f"Cosine similarity requires at least 2 features. Got {x.shape[-1]} features for x and {y.shape[-1]} features for y."
3449
)
3550

36-
return cs(x, y)
51+
return np.asarray(cs(x, y))
3752

3853

3954
def plot_image(image: np.ndarray | torch.Tensor, title: str = "Image") -> None:
@@ -59,7 +74,7 @@ def cluster_and_return_labels(
5974
data: np.ndarray,
6075
method: Literal["kmeans", "dbscan", "spectral"] = "kmeans",
6176
n_clusters: int | None = None,
62-
**kwargs,
77+
**kwargs: Any,
6378
) -> np.ndarray:
6479
"""
6580
Clusters 'data' using the specified method.
@@ -74,12 +89,12 @@ def cluster_and_return_labels(
7489
if n_clusters is None:
7590
raise ValueError("n_clusters must be specified for KMeans.")
7691
model = KMeans(n_clusters=n_clusters, random_state=42, **kwargs)
77-
return model.fit_predict(data)
92+
return np.asarray(model.fit_predict(data))
7893

7994
if method == "dbscan":
8095
# DBSCAN doesn't need n_clusters (but can accept eps, min_samples)
8196
model = DBSCAN(**kwargs)
82-
return model.fit_predict(data)
97+
return np.asarray(model.fit_predict(data))
8398

8499
if method == "spectral":
85100
if n_clusters is None:
@@ -90,7 +105,7 @@ def cluster_and_return_labels(
90105
random_state=42,
91106
**kwargs,
92107
)
93-
return model.fit_predict(data)
108+
return np.asarray(model.fit_predict(data))
94109

95110
raise ValueError(f"Unknown method: {method}")
96111

@@ -100,7 +115,7 @@ def cluster_images_and_generate_statistics(
100115
true_labels: np.ndarray,
101116
n_clusters: int,
102117
method: Literal["kmeans", "dbscan", "spectral"] = "kmeans",
103-
**kwargs,
118+
**kwargs: Any,
104119
) -> dict[str, float]:
105120
"""
106121
Clusters the given features and computes RI, ARI and NMI.
@@ -127,7 +142,7 @@ def cluster_images_and_generate_statistics(
127142

128143

129144
def plot_and_save_heatmap(
130-
matrix: list | np.ndarray | torch.Tensor,
145+
matrix: list[Any] | np.ndarray | torch.Tensor,
131146
figsize: tuple[int, int] | None = None,
132147
x_tick_labels: list[str] | None = None,
133148
y_tick_labels: list[str] | None = None,

pyvisim/datasets/datasets.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import os
33
from multiprocessing import Process
44

5-
import cv2
65
import numpy as np
76
import requests
87
import scipy
@@ -12,6 +11,7 @@
1211
from tqdm import tqdm
1312

1413
from pyvisim._config import setup_logging
14+
from pyvisim._utils import read_image_rgb
1515

1616
setup_logging()
1717

@@ -35,7 +35,7 @@
3535
NUM_VAL_IMG = 1020
3636

3737

38-
def _download_and_process_file(url: str, dest: str, extract_dir: str):
38+
def _download_and_process_file(url: str, dest: str, extract_dir: str) -> None:
3939
"""
4040
Downloads a file and processes it (e.g., extraction if it's a zip or tar.gz file).
4141
"""
@@ -53,7 +53,7 @@ def _download_and_process_file(url: str, dest: str, extract_dir: str):
5353
os.remove(dest)
5454

5555

56-
def _download_file_with_progress(url: str, dest: str):
56+
def _download_file_with_progress(url: str, dest: str) -> None:
5757
"""
5858
Download a file with a progress bar.
5959
"""
@@ -78,7 +78,7 @@ def _download_file_with_progress(url: str, dest: str):
7878
logger.info(f"Downloaded file to {dest}")
7979

8080

81-
def _extract_zip(zip_file: str, extract_to: str):
81+
def _extract_zip(zip_file: str, extract_to: str) -> None:
8282
"""
8383
Extract a zip archive.
8484
"""
@@ -97,7 +97,7 @@ def _extract_zip(zip_file: str, extract_to: str):
9797
progress_bar.update(1)
9898

9999

100-
def _extract_tar(tar_file: str, extract_to: str):
100+
def _extract_tar(tar_file: str, extract_to: str) -> None:
101101
"""
102102
Extract a tar.gz archive.
103103
"""
@@ -184,7 +184,7 @@ def _check_data_integrity() -> bool:
184184
return True
185185

186186

187-
def download_oxford_flowers_data():
187+
def download_oxford_flowers_data() -> None:
188188
"""
189189
Downloads the 102 flowers dataset and organizes it into the desired structure,
190190
under `destination/oxford_flower_dataset/`.
@@ -207,7 +207,7 @@ def download_oxford_flowers_data():
207207
logger.info("Oxford Flowers dataset downloaded and processed successfully.")
208208

209209

210-
class OxfordFlowerDataset(Dataset):
210+
class OxfordFlowerDataset(Dataset[tuple[np.ndarray, int, str]]):
211211
"""
212212
Oxford Flower Dataset. It can be found at: https://www.robots.ox.ac.uk/~vgg/data/flowers/102/index.html.
213213
@@ -249,7 +249,7 @@ def _load_labels(self, labels_file: str) -> list[int]:
249249
:return: List of labels.
250250
"""
251251
mat_data = scipy.io.loadmat(labels_file)
252-
return mat_data["labels"].squeeze().tolist()
252+
return [int(label) for label in mat_data["labels"].squeeze()]
253253

254254
def _load_image_paths(self) -> list[str]:
255255
"""
@@ -319,7 +319,7 @@ def __getitem__(self, idx: int) -> tuple[np.ndarray, int, str]:
319319
img_path = self.image_paths[idx]
320320
label = self.labels[idx] if self.labels else -1
321321

322-
image = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)
322+
image = read_image_rgb(img_path)
323323

324324
if self.transform:
325325
image = self.transform(image)

0 commit comments

Comments
 (0)