From ff34cdead63db8b6c162cd909720141a78e20e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 14:29:40 +0100 Subject: [PATCH 01/25] feat: gaussian noiose unit tests --- test/augmentation/__init__.py | 0 test/augmentation/test_gaussain_noise.py | 77 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 test/augmentation/__init__.py create mode 100644 test/augmentation/test_gaussain_noise.py diff --git a/test/augmentation/__init__.py b/test/augmentation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/augmentation/test_gaussain_noise.py b/test/augmentation/test_gaussain_noise.py new file mode 100644 index 0000000..82e6f93 --- /dev/null +++ b/test/augmentation/test_gaussain_noise.py @@ -0,0 +1,77 @@ +# python +import math +import numpy as np +import torch +import pytest +from pytest import approx +from PIL import Image +from torchvision.transforms.v2.functional import to_tensor +from src.augmentation.gaussian_noise_pil import GaussianNoisePIL + +@pytest.fixture +def example_image(size=(64, 48)): + r = torch.linspace(0, 1, steps=size[0]).repeat(size[1], 1).T + g = torch.linspace(0, 1, steps=size[1]).repeat(size[0], 1) + b = torch.zeros(size[0], size[1]) + img = torch.stack([r, g, b], dim=0) + return Image.fromarray((img.permute(1, 2, 0).numpy() * 255).astype("uint8"), mode="RGB") + +def test_shape_and_mode_preserved(example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.1) + out = aug(example_image) + assert out.size == example_image.size + assert out.mode == example_image.mode + +def test_original_not_mutated(example_image): + before = example_image.tobytes() + aug = GaussianNoisePIL() + _ = aug(example_image) + assert example_image.tobytes() == before + +def test_sigma_zero_is_identity(example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.0) + out = aug(example_image) + assert to_tensor(out).equal(to_tensor(example_image)) + +def test_statistical_properties_approximate(example_image): + mean, sigma = 0.05, 0.1 + aug = GaussianNoisePIL(mean=mean, sigma=sigma) + torch.manual_seed(123) + out = aug(example_image) + diff = to_tensor(out) - to_tensor(example_image) + diff_mean = diff.mean().item() + diff_std = diff.std().item() + assert abs(diff_mean - mean) < 0.03 # tolerance + assert abs(diff_std - sigma) < 0.03 + +def test_reproducibility_with_seed(example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.1) + torch.manual_seed(42) + out1 = aug(example_image) + torch.manual_seed(42) + out2 = aug(example_image) + assert to_tensor(out1).equal(to_tensor(out2)) + +def test_variation_across_calls(example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.2) + torch.manual_seed(1) + out1 = aug(example_image) + torch.manual_seed(2) + out2 = aug(example_image) + diff = (to_tensor(out1) - to_tensor(out2)).abs().mean().item() + assert diff > 0.005 + +def test_output_range(example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.3) + out = aug(example_image) + tensor = to_tensor(out) + assert 0.0 <= tensor.min().item() <= 1.0 + assert 0.0 <= tensor.max().item() <= 1.0 + +def test_large_sigma_clamped(example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=5.0) + out = aug(example_image) + t = to_tensor(out) + assert t.min().item() >= 0.0 + assert t.max().item() <= 1.0 + From adf94d652d3a25a095d1b5f3dc4a63c051c53637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 14:42:42 +0100 Subject: [PATCH 02/25] feat: gaussian noise aug proper tests --- src/augmentation/gaussian_noise_pil.py | 9 +- test/augmentation/test_gaussain_noise.py | 119 ++++++++++++----------- 2 files changed, 71 insertions(+), 57 deletions(-) diff --git a/src/augmentation/gaussian_noise_pil.py b/src/augmentation/gaussian_noise_pil.py index 3ec5ee0..a7303ab 100644 --- a/src/augmentation/gaussian_noise_pil.py +++ b/src/augmentation/gaussian_noise_pil.py @@ -1,5 +1,6 @@ +import torch from PIL import Image -from torchvision.transforms.v2.functional import gaussian_noise, to_pil_image, to_tensor +from torchvision.transforms.v2.functional import gaussian_noise, to_pil_image, to_dtype, to_image class GaussianNoisePIL: @@ -11,6 +12,10 @@ def __init__(self, mean=0.0, sigma=0.1): def __call__(self, img: Image.Image) -> Image.Image: """Apply Gaussian noise to the input PIL image.""" - img_tensor = to_tensor(img) + img_tensor = self.pil_to_tensor(img) noisy_tensor = gaussian_noise(img_tensor, mean=self.mean, sigma=self.sigma) return to_pil_image(noisy_tensor) + + @staticmethod + def pil_to_tensor(img: Image.Image): + return to_dtype(to_image(img), dtype=torch.float32, scale=True) \ No newline at end of file diff --git a/test/augmentation/test_gaussain_noise.py b/test/augmentation/test_gaussain_noise.py index 82e6f93..b6d7977 100644 --- a/test/augmentation/test_gaussain_noise.py +++ b/test/augmentation/test_gaussain_noise.py @@ -1,11 +1,8 @@ -# python -import math -import numpy as np import torch import pytest from pytest import approx from PIL import Image -from torchvision.transforms.v2.functional import to_tensor +from torchvision.transforms.v2.functional import to_image, to_dtype from src.augmentation.gaussian_noise_pil import GaussianNoisePIL @pytest.fixture @@ -16,62 +13,74 @@ def example_image(size=(64, 48)): img = torch.stack([r, g, b], dim=0) return Image.fromarray((img.permute(1, 2, 0).numpy() * 255).astype("uint8"), mode="RGB") -def test_shape_and_mode_preserved(example_image): - aug = GaussianNoisePIL(mean=0.0, sigma=0.1) - out = aug(example_image) - assert out.size == example_image.size - assert out.mode == example_image.mode +class TestGaussianNoisePIL: -def test_original_not_mutated(example_image): - before = example_image.tobytes() - aug = GaussianNoisePIL() - _ = aug(example_image) - assert example_image.tobytes() == before + @staticmethod + def pil_to_tensor(img): + return to_dtype(to_image(img), dtype=torch.float32, scale=True) -def test_sigma_zero_is_identity(example_image): - aug = GaussianNoisePIL(mean=0.0, sigma=0.0) - out = aug(example_image) - assert to_tensor(out).equal(to_tensor(example_image)) + def img_diff(self, img1: Image.Image, img2: Image.Image): + return self.pil_to_tensor(img1) - self.pil_to_tensor(img2) -def test_statistical_properties_approximate(example_image): - mean, sigma = 0.05, 0.1 - aug = GaussianNoisePIL(mean=mean, sigma=sigma) - torch.manual_seed(123) - out = aug(example_image) - diff = to_tensor(out) - to_tensor(example_image) - diff_mean = diff.mean().item() - diff_std = diff.std().item() - assert abs(diff_mean - mean) < 0.03 # tolerance - assert abs(diff_std - sigma) < 0.03 + def images_equal(self, img1: Image.Image, img2: Image.Image): + return self.pil_to_tensor(img1).equal(self.pil_to_tensor(img2)) -def test_reproducibility_with_seed(example_image): - aug = GaussianNoisePIL(mean=0.0, sigma=0.1) - torch.manual_seed(42) - out1 = aug(example_image) - torch.manual_seed(42) - out2 = aug(example_image) - assert to_tensor(out1).equal(to_tensor(out2)) + def test_shape_and_mode_preserved(self, example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.1) + out = aug(example_image) + assert out.size == example_image.size + assert out.mode == example_image.mode -def test_variation_across_calls(example_image): - aug = GaussianNoisePIL(mean=0.0, sigma=0.2) - torch.manual_seed(1) - out1 = aug(example_image) - torch.manual_seed(2) - out2 = aug(example_image) - diff = (to_tensor(out1) - to_tensor(out2)).abs().mean().item() - assert diff > 0.005 + def test_original_not_mutated(self, example_image): + before = example_image.tobytes() + aug = GaussianNoisePIL() + _ = aug(example_image) + assert example_image.tobytes() == before -def test_output_range(example_image): - aug = GaussianNoisePIL(mean=0.0, sigma=0.3) - out = aug(example_image) - tensor = to_tensor(out) - assert 0.0 <= tensor.min().item() <= 1.0 - assert 0.0 <= tensor.max().item() <= 1.0 + def test_sigma_zero_is_identity(self, example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.0) + out = aug(example_image) + assert self.images_equal(example_image, out) -def test_large_sigma_clamped(example_image): - aug = GaussianNoisePIL(mean=0.0, sigma=5.0) - out = aug(example_image) - t = to_tensor(out) - assert t.min().item() >= 0.0 - assert t.max().item() <= 1.0 + def test_statistical_properties_approximate(self, example_image): + mean, sigma = 0.05, 0.1 + aug = GaussianNoisePIL(mean=mean, sigma=sigma) + torch.manual_seed(123) + out = aug(example_image) + diff = self.img_diff(out, example_image) + diff_mean = diff.mean().item() + diff_std = diff.std().item() + assert approx(diff_mean, abs=0.03) == mean + assert approx(diff_std, abs=0.03) == sigma + + def test_reproducibility_with_seed(self, example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.1) + torch.manual_seed(42) + out1 = aug(example_image) + torch.manual_seed(42) + out2 = aug(example_image) + assert self.images_equal(out1, out2) + + def test_variation_across_calls(self, example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.2) + torch.manual_seed(1) + out1 = aug(example_image) + torch.manual_seed(2) + out2 = aug(example_image) + diff = self.img_diff(out1, out2).abs().mean().item() + assert diff > 0.005 + + def test_output_range(self, example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=0.3) + out = aug(example_image) + tensor = self.pil_to_tensor(out) + assert 0.0 <= tensor.min().item() <= 1.0 + assert 0.0 <= tensor.max().item() <= 1.0 + + def test_output_range_large_sigma(self, example_image): + aug = GaussianNoisePIL(mean=0.0, sigma=5.0) + out = aug(example_image) + t = self.pil_to_tensor(out) + assert t.min().item() >= 0.0 + assert t.max().item() <= 1.0 From 1a0dd00c9286209b2d293b16223c79284d2f7dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 15:49:49 +0100 Subject: [PATCH 03/25] feat: script for downloading shape predictor weights --- Justfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 29e25b3..a9c5447 100644 --- a/Justfile +++ b/Justfile @@ -6,6 +6,12 @@ default: install: uv sync +# Download shape predictor model weights +download_shape_predictor: + mkdir -p models/dlib + wget -O models/dlib/shape_predictor_68_face_landmarks.dat.bz2 https://raw.githubusercontent.com/davisking/dlib-models/master/shape_predictor_68_face_landmarks.dat.bz2 + bzip2 -d models/dlib/shape_predictor_68_face_landmarks.dat.bz2 + # === Scripts from src.scripts package === # Run benchmark @@ -36,7 +42,7 @@ test: # Run tests with coverage cov: - uv run pytest --cov=src + uv run pytest --cov=src --cov-report=term-missing # === Static Analysis === From ecc7c584b0057f854d82ac767fea6ddcc2a8a1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 15:50:26 +0100 Subject: [PATCH 04/25] feat: AddGlassesAugmentation test, refactor gaussian noise pil test --- pyproject.toml | 3 ++ test/assets/face.jpg | Bin 0 -> 22977 bytes test/augmentation/test_gaussain_noise.py | 34 ++++++----------------- test/augmentation/test_glasses.py | 21 ++++++++++++++ test/conftest.py | 30 ++++++++++++++++++++ 5 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 test/assets/face.jpg create mode 100644 test/augmentation/test_glasses.py create mode 100644 test/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 3d94f87..b918b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ profile = "black" [tool.pytest.ini_options] pythonpath = ["src"] +markers = [ + "integration: integration tests that may require external resources (e.g. model weights)", +] [dependency-groups] dev = [ diff --git a/test/assets/face.jpg b/test/assets/face.jpg new file mode 100644 index 0000000000000000000000000000000000000000..140a2264bca32b68c5385b1d3e71ce9bb1b12887 GIT binary patch literal 22977 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 literal 0 HcmV?d00001 diff --git a/test/augmentation/test_gaussain_noise.py b/test/augmentation/test_gaussain_noise.py index b6d7977..0db794f 100644 --- a/test/augmentation/test_gaussain_noise.py +++ b/test/augmentation/test_gaussain_noise.py @@ -1,30 +1,11 @@ import torch -import pytest from pytest import approx -from PIL import Image -from torchvision.transforms.v2.functional import to_image, to_dtype from src.augmentation.gaussian_noise_pil import GaussianNoisePIL +from test.conftest import img_diff, images_equal, pil_to_tensor -@pytest.fixture -def example_image(size=(64, 48)): - r = torch.linspace(0, 1, steps=size[0]).repeat(size[1], 1).T - g = torch.linspace(0, 1, steps=size[1]).repeat(size[0], 1) - b = torch.zeros(size[0], size[1]) - img = torch.stack([r, g, b], dim=0) - return Image.fromarray((img.permute(1, 2, 0).numpy() * 255).astype("uint8"), mode="RGB") class TestGaussianNoisePIL: - @staticmethod - def pil_to_tensor(img): - return to_dtype(to_image(img), dtype=torch.float32, scale=True) - - def img_diff(self, img1: Image.Image, img2: Image.Image): - return self.pil_to_tensor(img1) - self.pil_to_tensor(img2) - - def images_equal(self, img1: Image.Image, img2: Image.Image): - return self.pil_to_tensor(img1).equal(self.pil_to_tensor(img2)) - def test_shape_and_mode_preserved(self, example_image): aug = GaussianNoisePIL(mean=0.0, sigma=0.1) out = aug(example_image) @@ -40,14 +21,14 @@ def test_original_not_mutated(self, example_image): def test_sigma_zero_is_identity(self, example_image): aug = GaussianNoisePIL(mean=0.0, sigma=0.0) out = aug(example_image) - assert self.images_equal(example_image, out) + assert images_equal(example_image, out) def test_statistical_properties_approximate(self, example_image): mean, sigma = 0.05, 0.1 aug = GaussianNoisePIL(mean=mean, sigma=sigma) torch.manual_seed(123) out = aug(example_image) - diff = self.img_diff(out, example_image) + diff = img_diff(out, example_image) diff_mean = diff.mean().item() diff_std = diff.std().item() assert approx(diff_mean, abs=0.03) == mean @@ -59,7 +40,7 @@ def test_reproducibility_with_seed(self, example_image): out1 = aug(example_image) torch.manual_seed(42) out2 = aug(example_image) - assert self.images_equal(out1, out2) + assert images_equal(out1, out2) def test_variation_across_calls(self, example_image): aug = GaussianNoisePIL(mean=0.0, sigma=0.2) @@ -67,20 +48,21 @@ def test_variation_across_calls(self, example_image): out1 = aug(example_image) torch.manual_seed(2) out2 = aug(example_image) - diff = self.img_diff(out1, out2).abs().mean().item() + diff = img_diff(out1, out2).abs().mean().item() assert diff > 0.005 def test_output_range(self, example_image): aug = GaussianNoisePIL(mean=0.0, sigma=0.3) out = aug(example_image) - tensor = self.pil_to_tensor(out) + tensor = pil_to_tensor(out) assert 0.0 <= tensor.min().item() <= 1.0 assert 0.0 <= tensor.max().item() <= 1.0 def test_output_range_large_sigma(self, example_image): aug = GaussianNoisePIL(mean=0.0, sigma=5.0) out = aug(example_image) - t = self.pil_to_tensor(out) + t = pil_to_tensor(out) assert t.min().item() >= 0.0 assert t.max().item() <= 1.0 + diff --git a/test/augmentation/test_glasses.py b/test/augmentation/test_glasses.py new file mode 100644 index 0000000..dcbc822 --- /dev/null +++ b/test/augmentation/test_glasses.py @@ -0,0 +1,21 @@ +import pytest + +from src.augmentation.glasses import AddGlassesAugmentation +from test.conftest import images_equal + + +class TestAddGlassesAugmentation: + + @pytest.mark.integration + def test_add_glasses_to_face(self, face_image): + try: + add_glasses = AddGlassesAugmentation.with_default_models() + except RuntimeError as e: + pytest.skip(f"Model weights not available: {e}") + return + + img_with_glasses = add_glasses(face_image) + + assert img_with_glasses.size == face_image.size + assert img_with_glasses.mode == face_image.mode + assert not images_equal(img_with_glasses, face_image) diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..9dd1fb0 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,30 @@ +import pytest +import torch +from PIL import Image +from torchvision.transforms.v2.functional import to_dtype, to_image + + +@pytest.fixture +def example_image(size=(64, 48)): + r = torch.linspace(0, 1, steps=size[0]).repeat(size[1], 1).T + g = torch.linspace(0, 1, steps=size[1]).repeat(size[0], 1) + b = torch.zeros(size[0], size[1]) + img = torch.stack([r, g, b], dim=0) + return Image.fromarray((img.permute(1, 2, 0).numpy() * 255).astype("uint8"), mode="RGB") + + +@pytest.fixture +def face_image(): + return Image.open("test/assets/face.jpg") + + +def img_diff(img1: Image.Image, img2: Image.Image): + return pil_to_tensor(img1) - pil_to_tensor(img2) + + +def images_equal(img1: Image.Image, img2: Image.Image): + return pil_to_tensor(img1).equal(pil_to_tensor(img2)) + + +def pil_to_tensor(img): + return to_dtype(to_image(img), dtype=torch.float32, scale=True) From dfeb67d1760d28b475b314f7d0aec2ef8cbb72d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 15:59:09 +0100 Subject: [PATCH 05/25] feat: AddMaskAugmentation test --- test/augmentation/test_glasses.py | 2 +- test/augmentation/test_mask.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 test/augmentation/test_mask.py diff --git a/test/augmentation/test_glasses.py b/test/augmentation/test_glasses.py index dcbc822..b8e2c61 100644 --- a/test/augmentation/test_glasses.py +++ b/test/augmentation/test_glasses.py @@ -1,7 +1,7 @@ import pytest from src.augmentation.glasses import AddGlassesAugmentation -from test.conftest import images_equal +from test.conftest import images_equal, img_diff class TestAddGlassesAugmentation: diff --git a/test/augmentation/test_mask.py b/test/augmentation/test_mask.py new file mode 100644 index 0000000..94c999e --- /dev/null +++ b/test/augmentation/test_mask.py @@ -0,0 +1,22 @@ +import pytest + +from src.augmentation import AddMaskAugmentation +from test.conftest import images_equal + + +class TestAddMaskAugmentation: + + @pytest.mark.integration + def test_add_mask_to_face(self, face_image): + try: + add_mask = AddMaskAugmentation.with_default_models() + except RuntimeError as e: + pytest.skip(f"Model weights not available: {e}") + return + + original_copy = face_image.copy() + img_with_mask = add_mask(face_image) + + assert img_with_mask.size == original_copy.size + assert img_with_mask.mode == original_copy.mode + assert not images_equal(img_with_mask, original_copy), "May mutate the original" From 0c00259f67cbd78ff50dc7a34d8cec91254db204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 16:09:26 +0100 Subject: [PATCH 06/25] feat: random rectangle aug test --- test/augmentation/test_random_rectangle.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/augmentation/test_random_rectangle.py diff --git a/test/augmentation/test_random_rectangle.py b/test/augmentation/test_random_rectangle.py new file mode 100644 index 0000000..5c1e5a7 --- /dev/null +++ b/test/augmentation/test_random_rectangle.py @@ -0,0 +1,19 @@ +import pytest + +from src.augmentation import AddRandomRectangleRandomColor, AddRandomBlackRectangle, AddRandomRectangleAverageColor +from test.conftest import images_equal + + +class TestAddRandomRectangle: + + @pytest.mark.parametrize("aug_cls", [ + AddRandomRectangleRandomColor, + AddRandomBlackRectangle, + AddRandomRectangleAverageColor, + ]) + def test_add_random_rectangle(self, aug_cls, example_image): + aug = aug_cls(min_size=5, max_size=15) + out = aug(example_image.copy()) + assert out.size == example_image.size, "Image size changed" + assert out.mode == example_image.mode, "Image mode changed" + assert not images_equal(out, example_image), "Image should be modified" From 6c78a325f53f64eaea6e1ac0404ecbfb465eec75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 16:58:07 +0100 Subject: [PATCH 07/25] feat: add just script for connecting to vpn --- .gitignore | 3 ++- Justfile | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3272ec0..eee00a3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ __pycache__ .env wandb .wandb -results \ No newline at end of file +results +wireguard \ No newline at end of file diff --git a/Justfile b/Justfile index a9c5447..05eef92 100644 --- a/Justfile +++ b/Justfile @@ -6,6 +6,14 @@ default: install: uv sync +# Connect to VPN +vpn_connect: + sudo wg-quick up wg0 + +# Disconnect from VPN +vpn_disconnect: + sudo wg-quick down wg0 + # Download shape predictor model weights download_shape_predictor: mkdir -p models/dlib From b3c0924df2b3b57400e4068582d05ba2ad994caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 17:15:01 +0100 Subject: [PATCH 08/25] feat: add tests for FacePairsBenchmnark --- src/benchmark/face_pairs.py | 3 - test/benchmark/__init__.py | 0 test/benchmark/test_face_pairs.py | 95 +++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 test/benchmark/__init__.py create mode 100644 test/benchmark/test_face_pairs.py diff --git a/src/benchmark/face_pairs.py b/src/benchmark/face_pairs.py index 736973d..8843a5e 100644 --- a/src/benchmark/face_pairs.py +++ b/src/benchmark/face_pairs.py @@ -45,8 +45,6 @@ class FacePairsBenchmark: Computes accuracy: (TP + TN) / TOTAL """ - N_ELEMENTS = 6000 # Expected number of elements in LFW and ROF datasets - def __init__( self, model: nn.Module, @@ -112,7 +110,6 @@ def _get_data_loader(self): def _compute_embeddings(self) -> tuple[Tensor, Tensor, Tensor]: n_examples = len(self.dataset) - assert n_examples == self.N_ELEMENTS pair_classes = [] embeddings_1 = [] embeddings_2 = [] diff --git a/test/benchmark/__init__.py b/test/benchmark/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/benchmark/test_face_pairs.py b/test/benchmark/test_face_pairs.py new file mode 100644 index 0000000..c078522 --- /dev/null +++ b/test/benchmark/test_face_pairs.py @@ -0,0 +1,95 @@ +import pytest +from PIL import Image + +from src.dataset.lfw import LFWDataset +from src.dataset.rof import ROFDataset +from src.benchmark import FacePairsBenchmarkConfig, FacePairsBenchmark +from src.dataset.face_pairs import FacePairsDataset +from src.training.models import get_facenet_pytorch_inception_resnet_v1 + + +class TinyDataset(FacePairsDataset): + def __init__(self): + self.transform = FacePairsBenchmark._get_default_transform() + + # 60 pairs: 30 match (same color), 30 mismatch (different colors) + self.pairs = [] + for i in range(30): + color = (i * 8, 128, 200) + img = Image.new("RGB", (160, 160), color) + self.pairs.append((img, img.copy(), 1)) # match + + for i in range(30): + img1 = Image.new("RGB", (160, 160), (i * 8, 100, 150)) + img2 = Image.new("RGB", (160, 160), (255 - i * 8, 200, 50)) + self.pairs.append((img1, img2, -1)) # mismatch + + def __len__(self): + return len(self.pairs) + + def __getitem__(self, idx): + img1, img2, label = self.pairs[idx] + return self.transform(img1), self.transform(img2), label + + +@pytest.fixture +def face_pairs_config(): + return FacePairsBenchmarkConfig( + n_folds=10, + batch_size=10, + embedding_size=512, + device="cpu", + ) + +@pytest.fixture +def model(): + return get_facenet_pytorch_inception_resnet_v1() + +class TestFacePairsBenchmark: + """Tests for FacePairsBenchmark.""" + + @pytest.mark.integration + def test_run_benchmark_on_tiny_dataset(self, model, face_pairs_config): + """Run full benchmark on a tiny real dataset.""" + dataset = TinyDataset() + benchmark = FacePairsBenchmark(model, dataset, face_pairs_config) + result = benchmark.evaluate() + + assert 0.0 <= result.mean_accuracy <= 1.0 + assert result.std_accuracy >= 0.0 + + @pytest.mark.integration + def test_lfw_benchmark_initialization(self, model, face_pairs_config): + """Test LFW benchmark initialization.""" + + try: + benchmark = FacePairsBenchmark.lfw(model, face_pairs_config) + except FileNotFoundError: + pytest.skip("LFW dataset files missing") + return + + assert isinstance(benchmark.dataset, LFWDataset), f"Dataset should be LFWDataset, got {type(benchmark.dataset)}" + + @pytest.mark.integration + def test_rof_sunglasses_benchmark_initialization(self, model, face_pairs_config): + """Test LFW benchmark initialization.""" + + try: + benchmark = FacePairsBenchmark.rof_sunglasses(model, face_pairs_config) + except FileNotFoundError: + pytest.skip("ROF-s dataset files missing") + return + + assert isinstance(benchmark.dataset, ROFDataset) + + @pytest.mark.integration + def test_rof_masked_initialization(self, model, face_pairs_config): + """Test LFW benchmark initialization.""" + + try: + benchmark = FacePairsBenchmark.rof_masked(model, face_pairs_config) + except FileNotFoundError: + pytest.skip("ROF-m dataset files missing") + return + + assert isinstance(benchmark.dataset, ROFDataset) \ No newline at end of file From 9539b1eeca89cb86613656bdcb78533ec5ad4715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 17:22:52 +0100 Subject: [PATCH 09/25] fix: disable multiprocessing in test to avoid warnings --- test/benchmark/test_face_pairs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/benchmark/test_face_pairs.py b/test/benchmark/test_face_pairs.py index c078522..7b74a4b 100644 --- a/test/benchmark/test_face_pairs.py +++ b/test/benchmark/test_face_pairs.py @@ -39,6 +39,7 @@ def face_pairs_config(): batch_size=10, embedding_size=512, device="cpu", + num_workers=0, ) @pytest.fixture From a1e7c6d5b4a4dc640e0293de96000b5246058384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 17:39:47 +0100 Subject: [PATCH 10/25] feat: tests for rococo evaluation adapter --- pyproject.toml | 3 +++ test/conftest.py | 5 +++++ test/evaluation/__init__.py | 0 test/evaluation/test_rococo.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 test/evaluation/__init__.py create mode 100644 test/evaluation/test_rococo.py diff --git a/pyproject.toml b/pyproject.toml index b918b07..5abf779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ pythonpath = ["src"] markers = [ "integration: integration tests that may require external resources (e.g. model weights)", ] +filterwarnings = [ + "ignore::pydantic._internal._generate_schema.UnsupportedFieldAttributeWarning", # Ignore warning from libraries +] [dependency-groups] dev = [ diff --git a/test/conftest.py b/test/conftest.py index 9dd1fb0..80fd044 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import pytest import torch +import cv2 from PIL import Image from torchvision.transforms.v2.functional import to_dtype, to_image @@ -17,6 +18,10 @@ def example_image(size=(64, 48)): def face_image(): return Image.open("test/assets/face.jpg") +@pytest.fixture +def face_image_cv2(): + return cv2.imread("test/assets/face.jpg", cv2.IMREAD_COLOR) + def img_diff(img1: Image.Image, img2: Image.Image): return pil_to_tensor(img1) - pil_to_tensor(img2) diff --git a/test/evaluation/__init__.py b/test/evaluation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/evaluation/test_rococo.py b/test/evaluation/test_rococo.py new file mode 100644 index 0000000..f70f885 --- /dev/null +++ b/test/evaluation/test_rococo.py @@ -0,0 +1,34 @@ +import numpy as np +import pytest +import torch +from rococo_evaluation_lib import NoFaceDetectedException + +from src.evaluation.rococo import FaceRecognitionSystemFacenetPytorchAdapter +from training.models import get_facenet_pytorch_inception_resnet_v1, get_facenet_pytorch_mtcnn_detector + + +@pytest.fixture +def adapter(): + facenet = get_facenet_pytorch_inception_resnet_v1() + mtcnn = get_facenet_pytorch_mtcnn_detector() + device = torch.device("cpu") + return FaceRecognitionSystemFacenetPytorchAdapter( + facenet=facenet, + detector=mtcnn, + device=device, + ) + + +class TestFaceRecognitionSystemFacenetPytorchAdapter: + + @pytest.mark.integration + def test_compute_feature_vector_with_facenet_pytorch_models(self, adapter, face_image_cv2): + feature_vector = adapter.compute_feature_vector(face_image_cv2) + assert feature_vector.shape == (512,), "Feature vector should have length 512" + assert adapter.feature_vector_length() == 512, "Feature vector length should be 512" + + @pytest.mark.integration + def test_compute_feature_vector_no_face_detected(self, adapter, example_image): + cv2_img = np.asarray(example_image) + with pytest.raises(NoFaceDetectedException): + adapter.compute_feature_vector(cv2_img) From de48df4272a18cb503f62b45d610dc5ee7fe2dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 19:32:45 +0100 Subject: [PATCH 11/25] feat: add just script for downloading opencv detector weights --- Justfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Justfile b/Justfile index 05eef92..4eb54bd 100644 --- a/Justfile +++ b/Justfile @@ -20,6 +20,11 @@ download_shape_predictor: wget -O models/dlib/shape_predictor_68_face_landmarks.dat.bz2 https://raw.githubusercontent.com/davisking/dlib-models/master/shape_predictor_68_face_landmarks.dat.bz2 bzip2 -d models/dlib/shape_predictor_68_face_landmarks.dat.bz2 +# Download opencv face detector model weights +download_opencv_face_detector: + mkdir -p models/opencv + wget -O models/opencv/haarcascade_frontalface_default.xml https://raw.githubusercontent.com/opencv/opencv/4.x/data/haarcascades/haarcascade_frontalface_default.xml + # === Scripts from src.scripts package === # Run benchmark From a39bed1f2a1e6f5d5955c5b1e029040030c10192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 19:32:58 +0100 Subject: [PATCH 12/25] feat: move functions from script to library --- src/scripts/process_video.py | 33 +------------------------------ src/video_processing/processor.py | 33 ++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/scripts/process_video.py b/src/scripts/process_video.py index 4caf6a8..c310c9d 100644 --- a/src/scripts/process_video.py +++ b/src/scripts/process_video.py @@ -71,38 +71,7 @@ from time import perf_counter from src.video_processing.configuration import Configuration -from src.video_processing.processor import VideoProcessor - - -def naive_strategy(config: Configuration): - """Naive sampling strategy that samples frames from each sequence.""" - processor = VideoProcessor(config) - processor.find_face_sequences() - processor.print_sequence_analysis() - - # Sample frames from sequences - sequence_mapping = processor.sample_frames_from_sequences_naive( - config.samples_per_sequence - ) - processor.save_sampled_camera_regions_from_sequences(sequence_mapping) - - -def offset_strategy(config: Configuration): - """Offset sampling strategy that samples frames based on sequence descriptors.""" - processor = VideoProcessor(config) - processor.find_face_sequences() - processor.print_sequence_analysis() - - # Offset sequence sampling - assert config.offset_before_marker is not None - assert config.offset_after_marker is not None - seq_descriptors = processor.get_offset_sequence_bounds( - config.offset_before_marker, config.offset_after_marker - ) - samples = processor.sample_frames_from_sequence_offset_based( - seq_descriptors, config.samples_per_sequence - ) - processor.save_sampled_camera_regions_from_sequences(samples) +from src.video_processing.processor import naive_strategy, offset_strategy def main(): diff --git a/src/video_processing/processor.py b/src/video_processing/processor.py index b1b8c24..6ba9df9 100644 --- a/src/video_processing/processor.py +++ b/src/video_processing/processor.py @@ -3,7 +3,6 @@ import cv2 import numpy as np -from src.video_processing.configuration import Configuration from src.video_processing.filters import ( FaceDetectionFilter, FilterType, @@ -13,6 +12,7 @@ from src.video_processing.rect import Rect from src.video_processing.sequence_marker import SequenceDescriptor, SequenceMarker from src.video_processing.video_capture import VideoProperties, video_capture +from src.video_processing.configuration import Configuration class VideoProcessor: @@ -421,3 +421,34 @@ def print_sequence_analysis(self): print( f"Icon sequences: {len(icon_sequences)} total, avg length: {sum(icon_sequences)/len(icon_sequences):.1f} frames" ) + + +def naive_strategy(config: Configuration): + """Naive sampling strategy that samples frames from each sequence.""" + processor = VideoProcessor(config) + processor.find_face_sequences() + processor.print_sequence_analysis() + + # Sample frames from sequences + sequence_mapping = processor.sample_frames_from_sequences_naive( + config.samples_per_sequence + ) + processor.save_sampled_camera_regions_from_sequences(sequence_mapping) + + +def offset_strategy(config: Configuration): + """Offset sampling strategy that samples frames based on sequence descriptors.""" + processor = VideoProcessor(config) + processor.find_face_sequences() + processor.print_sequence_analysis() + + # Offset sequence sampling + assert config.offset_before_marker is not None + assert config.offset_after_marker is not None + seq_descriptors = processor.get_offset_sequence_bounds( + config.offset_before_marker, config.offset_after_marker + ) + samples = processor.sample_frames_from_sequence_offset_based( + seq_descriptors, config.samples_per_sequence + ) + processor.save_sampled_camera_regions_from_sequences(samples) From 40639009308cb83a151d57b859602947dd4dfc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 19:33:07 +0100 Subject: [PATCH 13/25] feat: add video processor test --- test/conftest.py | 23 ++++++++++++++- test/video_processing/__init__.py | 0 test/video_processing/test_processor.py | 39 +++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/video_processing/__init__.py create mode 100644 test/video_processing/test_processor.py diff --git a/test/conftest.py b/test/conftest.py index 80fd044..dffdf2f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,6 +1,8 @@ +from pathlib import Path + +import cv2 import pytest import torch -import cv2 from PIL import Image from torchvision.transforms.v2.functional import to_dtype, to_image @@ -18,11 +20,30 @@ def example_image(size=(64, 48)): def face_image(): return Image.open("test/assets/face.jpg") + @pytest.fixture def face_image_cv2(): return cv2.imread("test/assets/face.jpg", cv2.IMREAD_COLOR) +@pytest.fixture +def video_fragment_path(): + path = Path("data/test_video_2_people.mp4") + if not path.exists(): + pytest.skip("Test video file is missing") + + return path + + +@pytest.fixture +def opencv_face_detector_weights_path(): + path = Path("models/opencv/haarcascade_frontalface_default.xml") + if not path.exists(): + pytest.skip("OpenCV face detector weights file is missing") + + return path + + def img_diff(img1: Image.Image, img2: Image.Image): return pil_to_tensor(img1) - pil_to_tensor(img2) diff --git a/test/video_processing/__init__.py b/test/video_processing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/video_processing/test_processor.py b/test/video_processing/test_processor.py new file mode 100644 index 0000000..7ef86c5 --- /dev/null +++ b/test/video_processing/test_processor.py @@ -0,0 +1,39 @@ +from src.video_processing.configuration import Configuration +from src.video_processing.processor import offset_strategy +from src.video_processing.rect import Rect + + +class TestVideoProcessor: + def test_process_video(self, video_fragment_path, opencv_face_detector_weights_path, tmp_path): + """Process a short real video""" + + expected_people = 2 + faces_output_dir = tmp_path / "faces" + camera_output_dir = tmp_path / "camera" + config = Configuration( + mode="offset", + video_path=str(video_fragment_path), + faces_output_dir=str(faces_output_dir), + face_rectangle=Rect(x=1415, y=930, width=500, height=350), + camera_rectangle=Rect(x=100, y=0, width=1280, height=1300), + camera_output_dir=str(camera_output_dir), + pixel_diff_threshold=100.0, + variance_filter_value=3085.0, + mean_filter_value=214.0, + variance_filter_tolerance=5.0, + mean_filter_tolerance=2.0, + face_detection_model_path=str(opencv_face_detector_weights_path), + face_detection_min_size=(30, 30), + samples_per_sequence=3, + offset_before_marker=1, + offset_after_marker=1, + ) + + offset_strategy(config) + + face_files = list(faces_output_dir.glob("*.jpg")) + frame_files = list(camera_output_dir.glob("*.jpg")) + + assert len(face_files) == expected_people, f"Expected {expected_people} people in the video" + assert len( + frame_files) == expected_people * config.samples_per_sequence, f"Expected {config.samples_per_sequence} frames per person" From 625bbfe5c9ae33f67964bf87db3df0b580babc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:00:10 +0100 Subject: [PATCH 14/25] feat: add test for fine tuning trainer --- src/dataset/dummy.py | 29 ++++++++++++++++ src/training/data_loaders.py | 45 +++++++++++++++++++++---- src/training/fine_tuning.py | 3 ++ src/training/fine_tuning_config.py | 2 +- test/benchmark/test_face_pairs.py | 35 ++++--------------- test/training/__init__.py | 0 test/training/test_fine_tuning.py | 20 +++++++++++ test/video_processing/test_processor.py | 4 +++ 8 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 src/dataset/dummy.py create mode 100644 test/training/__init__.py create mode 100644 test/training/test_fine_tuning.py diff --git a/src/dataset/dummy.py b/src/dataset/dummy.py new file mode 100644 index 0000000..979e921 --- /dev/null +++ b/src/dataset/dummy.py @@ -0,0 +1,29 @@ +from PIL import Image + +from src.dataset.face_pairs import FacePairsDataset + + +class DummyFacePairsDataset(FacePairsDataset): + """Dummy in-memory dataset for testing""" + + def __init__(self, transform): + self.transform = transform + + # 60 pairs: 30 match (same color), 30 mismatch (different colors) + self.pairs = [] + for i in range(30): + color = (i * 8, 128, 200) + img = Image.new("RGB", (160, 160), color) + self.pairs.append((img, img.copy(), 1)) # match + + for i in range(30): + img1 = Image.new("RGB", (160, 160), (i * 8, 100, 150)) + img2 = Image.new("RGB", (160, 160), (255 - i * 8, 200, 50)) + self.pairs.append((img1, img2, -1)) # mismatch + + def __len__(self): + return len(self.pairs) + + def __getitem__(self, idx): + img1, img2, label = self.pairs[idx] + return self.transform(img1), self.transform(img2), label diff --git a/src/training/data_loaders.py b/src/training/data_loaders.py index 28cbce7..b3351f5 100644 --- a/src/training/data_loaders.py +++ b/src/training/data_loaders.py @@ -1,6 +1,7 @@ from torch.utils.data import DataLoader from src.dataset.lfw import LFWDataset +from src.dataset.dummy import DummyFacePairsDataset from src.dataset.rococo_training import RococoTrainingDataset @@ -51,14 +52,13 @@ def get_lfw_loaders( def get_rococo_loaders( - transform_face, - transform_frame, - rococo_root="data/rococo2v3-dev", - batch_size=32, - num_workers=4, - pin_memory=True, + transform_face, + transform_frame, + rococo_root="data/rococo2v3-dev", + batch_size=32, + num_workers=4, + pin_memory=True, ) -> dict[str, DataLoader]: - train_set = RococoTrainingDataset.from_match_and_mismatch_pairs( root_dir=rococo_root, match_pairs_file="data/rococo2v3-dev/train_match_pairs.csv", @@ -99,3 +99,34 @@ def get_rococo_loaders( "train": train_loader, "val": val_loader, } + + +def get_dummy_loaders( + transform, + batch_size=32, + num_workers=0, + pin_memory=False, +) -> dict[str, DataLoader]: + train_set = DummyFacePairsDataset(transform) + val_set = DummyFacePairsDataset(transform) + + train_loader = DataLoader( + train_set, + batch_size=batch_size, + shuffle=False, + num_workers=num_workers, + pin_memory=pin_memory, + ) + + val_loader = DataLoader( + val_set, + batch_size=batch_size, + shuffle=False, + num_workers=num_workers, + pin_memory=pin_memory, + ) + + return { + "train": train_loader, + "val": val_loader, + } diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index df4650e..dc0506f 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -15,6 +15,7 @@ from src.training.data_loaders import get_lfw_loaders, get_rococo_loaders from src.training.fine_tuning_config import FineTuningConfig from src.training.models import get_facenet_pytorch_inception_resnet_v1 +from src.training.data_loaders import get_dummy_loaders def save_model_to_file(model: nn.Module, file_path: str): @@ -56,6 +57,8 @@ def get_data_loaders(config: FineTuningConfig) -> dict[str, DataLoader]: transform_frame=transform, batch_size=config.batch_size, ) + elif config.dataset == "dummy": + return get_dummy_loaders(transform, batch_size=config.batch_size) else: raise ValueError(f"Unknown dataset: {config.dataset}") diff --git a/src/training/fine_tuning_config.py b/src/training/fine_tuning_config.py index 1815351..a012847 100644 --- a/src/training/fine_tuning_config.py +++ b/src/training/fine_tuning_config.py @@ -17,4 +17,4 @@ class FineTuningConfig: device: str = field(default_factory=default_device_str) augmentation: Optional[str] = None only_head: bool = True - dataset: Literal["lfw", "rococo"] = "lfw" + dataset: Literal["lfw", "rococo", "dummy"] = "lfw" diff --git a/test/benchmark/test_face_pairs.py b/test/benchmark/test_face_pairs.py index 7b74a4b..052ba3d 100644 --- a/test/benchmark/test_face_pairs.py +++ b/test/benchmark/test_face_pairs.py @@ -1,37 +1,12 @@ import pytest -from PIL import Image +from src.benchmark import FacePairsBenchmarkConfig, FacePairsBenchmark from src.dataset.lfw import LFWDataset +from src.dataset.dummy import DummyFacePairsDataset from src.dataset.rof import ROFDataset -from src.benchmark import FacePairsBenchmarkConfig, FacePairsBenchmark -from src.dataset.face_pairs import FacePairsDataset from src.training.models import get_facenet_pytorch_inception_resnet_v1 -class TinyDataset(FacePairsDataset): - def __init__(self): - self.transform = FacePairsBenchmark._get_default_transform() - - # 60 pairs: 30 match (same color), 30 mismatch (different colors) - self.pairs = [] - for i in range(30): - color = (i * 8, 128, 200) - img = Image.new("RGB", (160, 160), color) - self.pairs.append((img, img.copy(), 1)) # match - - for i in range(30): - img1 = Image.new("RGB", (160, 160), (i * 8, 100, 150)) - img2 = Image.new("RGB", (160, 160), (255 - i * 8, 200, 50)) - self.pairs.append((img1, img2, -1)) # mismatch - - def __len__(self): - return len(self.pairs) - - def __getitem__(self, idx): - img1, img2, label = self.pairs[idx] - return self.transform(img1), self.transform(img2), label - - @pytest.fixture def face_pairs_config(): return FacePairsBenchmarkConfig( @@ -42,17 +17,19 @@ def face_pairs_config(): num_workers=0, ) + @pytest.fixture def model(): return get_facenet_pytorch_inception_resnet_v1() + class TestFacePairsBenchmark: """Tests for FacePairsBenchmark.""" @pytest.mark.integration def test_run_benchmark_on_tiny_dataset(self, model, face_pairs_config): """Run full benchmark on a tiny real dataset.""" - dataset = TinyDataset() + dataset = DummyFacePairsDataset(transform=FacePairsBenchmark._get_default_transform()) benchmark = FacePairsBenchmark(model, dataset, face_pairs_config) result = benchmark.evaluate() @@ -93,4 +70,4 @@ def test_rof_masked_initialization(self, model, face_pairs_config): pytest.skip("ROF-m dataset files missing") return - assert isinstance(benchmark.dataset, ROFDataset) \ No newline at end of file + assert isinstance(benchmark.dataset, ROFDataset) diff --git a/test/training/__init__.py b/test/training/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/training/test_fine_tuning.py b/test/training/test_fine_tuning.py new file mode 100644 index 0000000..db4a561 --- /dev/null +++ b/test/training/test_fine_tuning.py @@ -0,0 +1,20 @@ +import pytest + +from src.training.fine_tuning_config import FineTuningConfig +from src.training.fine_tuning import FineTuningTrainer + + +class TestFineTuningTrainer: + + @pytest.mark.integration + def test_training_on_dummy_dataset(self, tmp_path): + config = FineTuningConfig( + epochs=1, + dataset="dummy", + device="cpu", + model_save_path=str(tmp_path / "model.pth") + ) + trainer = FineTuningTrainer(config, use_wandb=False) + results = trainer.run() + print(results) + diff --git a/test/video_processing/test_processor.py b/test/video_processing/test_processor.py index 7ef86c5..b7898aa 100644 --- a/test/video_processing/test_processor.py +++ b/test/video_processing/test_processor.py @@ -1,9 +1,13 @@ +import pytest + from src.video_processing.configuration import Configuration from src.video_processing.processor import offset_strategy from src.video_processing.rect import Rect class TestVideoProcessor: + + @pytest.mark.integration def test_process_video(self, video_fragment_path, opencv_face_detector_weights_path, tmp_path): """Process a short real video""" From 491cd1d80ccb6299b9aff562d54f32ccc72d3737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:00:39 +0100 Subject: [PATCH 15/25] chore: formatting --- src/augmentation/gaussian_noise_pil.py | 9 +++++++-- src/evaluation/rococo.py | 3 +-- src/plots/wandb_client.py | 2 +- src/scripts/benchmark.py | 1 + src/scripts/experiment_sweep.py | 2 +- src/scripts/rococo_evaluation.py | 2 +- src/training/data_loaders.py | 22 +++++++++++----------- src/training/fine_tuning.py | 9 ++++++--- src/video_processing/processor.py | 2 +- test/augmentation/test_gaussain_noise.py | 6 +++--- test/augmentation/test_glasses.py | 3 ++- test/augmentation/test_mask.py | 3 ++- test/augmentation/test_random_rectangle.py | 22 +++++++++++++++------- test/benchmark/test_face_pairs.py | 12 ++++++++---- test/conftest.py | 4 +++- test/evaluation/test_rococo.py | 13 ++++++++++--- test/training/test_fine_tuning.py | 5 ++--- test/video_processing/test_processor.py | 13 +++++++++---- 18 files changed, 84 insertions(+), 49 deletions(-) diff --git a/src/augmentation/gaussian_noise_pil.py b/src/augmentation/gaussian_noise_pil.py index a7303ab..1854076 100644 --- a/src/augmentation/gaussian_noise_pil.py +++ b/src/augmentation/gaussian_noise_pil.py @@ -1,6 +1,11 @@ import torch from PIL import Image -from torchvision.transforms.v2.functional import gaussian_noise, to_pil_image, to_dtype, to_image +from torchvision.transforms.v2.functional import ( + gaussian_noise, + to_dtype, + to_image, + to_pil_image, +) class GaussianNoisePIL: @@ -18,4 +23,4 @@ def __call__(self, img: Image.Image) -> Image.Image: @staticmethod def pil_to_tensor(img: Image.Image): - return to_dtype(to_image(img), dtype=torch.float32, scale=True) \ No newline at end of file + return to_dtype(to_image(img), dtype=torch.float32, scale=True) diff --git a/src/evaluation/rococo.py b/src/evaluation/rococo.py index e95d24e..22cf858 100644 --- a/src/evaluation/rococo.py +++ b/src/evaluation/rococo.py @@ -3,6 +3,7 @@ import cv2 import torch import torch.nn as nn +import wandb from numpy import ndarray from PIL import Image from rococo_evaluation_lib import ( @@ -11,8 +12,6 @@ ValidationResults, ) -import wandb - class FaceRecognitionSystemFacenetPytorchAdapter(FaceRecognitionSystem): """Adapter for compatibility with the rococo evaluation library. diff --git a/src/plots/wandb_client.py b/src/plots/wandb_client.py index cc21d22..765d824 100644 --- a/src/plots/wandb_client.py +++ b/src/plots/wandb_client.py @@ -1,9 +1,9 @@ import json from pathlib import Path +import wandb from pandas import DataFrame -import wandb from src.plots.sweep_data import SweepData diff --git a/src/scripts/benchmark.py b/src/scripts/benchmark.py index 121e344..70d7610 100644 --- a/src/scripts/benchmark.py +++ b/src/scripts/benchmark.py @@ -1,6 +1,7 @@ import argparse import wandb + from src.benchmark import FacePairsBenchmark, FacePairsBenchmarkConfig from src.training.models import get_facenet_pytorch_inception_resnet_v1 diff --git a/src/scripts/experiment_sweep.py b/src/scripts/experiment_sweep.py index 65deeec..aeb7f82 100644 --- a/src/scripts/experiment_sweep.py +++ b/src/scripts/experiment_sweep.py @@ -6,9 +6,9 @@ from datetime import datetime import torch +import wandb from rococo_evaluation_lib import RococoDataset, RococoValidation -import wandb from src.benchmark import FacePairsBenchmark, FacePairsBenchmarkConfig from src.evaluation.rococo import ( FaceRecognitionSystemFacenetPytorchAdapter, diff --git a/src/scripts/rococo_evaluation.py b/src/scripts/rococo_evaluation.py index ae9a34e..6db9bc6 100644 --- a/src/scripts/rococo_evaluation.py +++ b/src/scripts/rococo_evaluation.py @@ -3,12 +3,12 @@ from dataclasses import asdict import torch +import wandb from rococo_evaluation_lib import ( RococoDataset, RococoValidation, ) -import wandb from src.evaluation.rococo import ( FaceRecognitionSystemFacenetPytorchAdapter, log_results_to_wandb, diff --git a/src/training/data_loaders.py b/src/training/data_loaders.py index b3351f5..5097300 100644 --- a/src/training/data_loaders.py +++ b/src/training/data_loaders.py @@ -1,7 +1,7 @@ from torch.utils.data import DataLoader -from src.dataset.lfw import LFWDataset from src.dataset.dummy import DummyFacePairsDataset +from src.dataset.lfw import LFWDataset from src.dataset.rococo_training import RococoTrainingDataset @@ -52,12 +52,12 @@ def get_lfw_loaders( def get_rococo_loaders( - transform_face, - transform_frame, - rococo_root="data/rococo2v3-dev", - batch_size=32, - num_workers=4, - pin_memory=True, + transform_face, + transform_frame, + rococo_root="data/rococo2v3-dev", + batch_size=32, + num_workers=4, + pin_memory=True, ) -> dict[str, DataLoader]: train_set = RococoTrainingDataset.from_match_and_mismatch_pairs( root_dir=rococo_root, @@ -102,10 +102,10 @@ def get_rococo_loaders( def get_dummy_loaders( - transform, - batch_size=32, - num_workers=0, - pin_memory=False, + transform, + batch_size=32, + num_workers=0, + pin_memory=False, ) -> dict[str, DataLoader]: train_set = DummyFacePairsDataset(transform) val_set = DummyFacePairsDataset(transform) diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index dc0506f..2f695ff 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -4,18 +4,21 @@ import torch import torch.nn.functional as F +import wandb from torch import Tensor, nn from torch.optim import Optimizer from torch.utils.data import DataLoader from torchvision import transforms -import wandb from src.augmentation.get_augmentation_transform import get_augmentation_transform from src.loss.get_loss_fn import get_loss_fn -from src.training.data_loaders import get_lfw_loaders, get_rococo_loaders +from src.training.data_loaders import ( + get_dummy_loaders, + get_lfw_loaders, + get_rococo_loaders, +) from src.training.fine_tuning_config import FineTuningConfig from src.training.models import get_facenet_pytorch_inception_resnet_v1 -from src.training.data_loaders import get_dummy_loaders def save_model_to_file(model: nn.Module, file_path: str): diff --git a/src/video_processing/processor.py b/src/video_processing/processor.py index 6ba9df9..a715e96 100644 --- a/src/video_processing/processor.py +++ b/src/video_processing/processor.py @@ -3,6 +3,7 @@ import cv2 import numpy as np +from src.video_processing.configuration import Configuration from src.video_processing.filters import ( FaceDetectionFilter, FilterType, @@ -12,7 +13,6 @@ from src.video_processing.rect import Rect from src.video_processing.sequence_marker import SequenceDescriptor, SequenceMarker from src.video_processing.video_capture import VideoProperties, video_capture -from src.video_processing.configuration import Configuration class VideoProcessor: diff --git a/test/augmentation/test_gaussain_noise.py b/test/augmentation/test_gaussain_noise.py index 0db794f..9b16064 100644 --- a/test/augmentation/test_gaussain_noise.py +++ b/test/augmentation/test_gaussain_noise.py @@ -1,7 +1,9 @@ +from test.conftest import images_equal, img_diff, pil_to_tensor + import torch from pytest import approx + from src.augmentation.gaussian_noise_pil import GaussianNoisePIL -from test.conftest import img_diff, images_equal, pil_to_tensor class TestGaussianNoisePIL: @@ -64,5 +66,3 @@ def test_output_range_large_sigma(self, example_image): t = pil_to_tensor(out) assert t.min().item() >= 0.0 assert t.max().item() <= 1.0 - - diff --git a/test/augmentation/test_glasses.py b/test/augmentation/test_glasses.py index b8e2c61..766bf1f 100644 --- a/test/augmentation/test_glasses.py +++ b/test/augmentation/test_glasses.py @@ -1,7 +1,8 @@ +from test.conftest import images_equal, img_diff + import pytest from src.augmentation.glasses import AddGlassesAugmentation -from test.conftest import images_equal, img_diff class TestAddGlassesAugmentation: diff --git a/test/augmentation/test_mask.py b/test/augmentation/test_mask.py index 94c999e..2af9ca3 100644 --- a/test/augmentation/test_mask.py +++ b/test/augmentation/test_mask.py @@ -1,7 +1,8 @@ +from test.conftest import images_equal + import pytest from src.augmentation import AddMaskAugmentation -from test.conftest import images_equal class TestAddMaskAugmentation: diff --git a/test/augmentation/test_random_rectangle.py b/test/augmentation/test_random_rectangle.py index 5c1e5a7..35f58d2 100644 --- a/test/augmentation/test_random_rectangle.py +++ b/test/augmentation/test_random_rectangle.py @@ -1,16 +1,24 @@ +from test.conftest import images_equal + import pytest -from src.augmentation import AddRandomRectangleRandomColor, AddRandomBlackRectangle, AddRandomRectangleAverageColor -from test.conftest import images_equal +from src.augmentation import ( + AddRandomBlackRectangle, + AddRandomRectangleAverageColor, + AddRandomRectangleRandomColor, +) class TestAddRandomRectangle: - @pytest.mark.parametrize("aug_cls", [ - AddRandomRectangleRandomColor, - AddRandomBlackRectangle, - AddRandomRectangleAverageColor, - ]) + @pytest.mark.parametrize( + "aug_cls", + [ + AddRandomRectangleRandomColor, + AddRandomBlackRectangle, + AddRandomRectangleAverageColor, + ], + ) def test_add_random_rectangle(self, aug_cls, example_image): aug = aug_cls(min_size=5, max_size=15) out = aug(example_image.copy()) diff --git a/test/benchmark/test_face_pairs.py b/test/benchmark/test_face_pairs.py index 052ba3d..a6ffdcd 100644 --- a/test/benchmark/test_face_pairs.py +++ b/test/benchmark/test_face_pairs.py @@ -1,8 +1,8 @@ import pytest -from src.benchmark import FacePairsBenchmarkConfig, FacePairsBenchmark -from src.dataset.lfw import LFWDataset +from src.benchmark import FacePairsBenchmark, FacePairsBenchmarkConfig from src.dataset.dummy import DummyFacePairsDataset +from src.dataset.lfw import LFWDataset from src.dataset.rof import ROFDataset from src.training.models import get_facenet_pytorch_inception_resnet_v1 @@ -29,7 +29,9 @@ class TestFacePairsBenchmark: @pytest.mark.integration def test_run_benchmark_on_tiny_dataset(self, model, face_pairs_config): """Run full benchmark on a tiny real dataset.""" - dataset = DummyFacePairsDataset(transform=FacePairsBenchmark._get_default_transform()) + dataset = DummyFacePairsDataset( + transform=FacePairsBenchmark._get_default_transform() + ) benchmark = FacePairsBenchmark(model, dataset, face_pairs_config) result = benchmark.evaluate() @@ -46,7 +48,9 @@ def test_lfw_benchmark_initialization(self, model, face_pairs_config): pytest.skip("LFW dataset files missing") return - assert isinstance(benchmark.dataset, LFWDataset), f"Dataset should be LFWDataset, got {type(benchmark.dataset)}" + assert isinstance( + benchmark.dataset, LFWDataset + ), f"Dataset should be LFWDataset, got {type(benchmark.dataset)}" @pytest.mark.integration def test_rof_sunglasses_benchmark_initialization(self, model, face_pairs_config): diff --git a/test/conftest.py b/test/conftest.py index dffdf2f..2bf1dc6 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -13,7 +13,9 @@ def example_image(size=(64, 48)): g = torch.linspace(0, 1, steps=size[1]).repeat(size[0], 1) b = torch.zeros(size[0], size[1]) img = torch.stack([r, g, b], dim=0) - return Image.fromarray((img.permute(1, 2, 0).numpy() * 255).astype("uint8"), mode="RGB") + return Image.fromarray( + (img.permute(1, 2, 0).numpy() * 255).astype("uint8"), mode="RGB" + ) @pytest.fixture diff --git a/test/evaluation/test_rococo.py b/test/evaluation/test_rococo.py index f70f885..be29cba 100644 --- a/test/evaluation/test_rococo.py +++ b/test/evaluation/test_rococo.py @@ -4,7 +4,10 @@ from rococo_evaluation_lib import NoFaceDetectedException from src.evaluation.rococo import FaceRecognitionSystemFacenetPytorchAdapter -from training.models import get_facenet_pytorch_inception_resnet_v1, get_facenet_pytorch_mtcnn_detector +from training.models import ( + get_facenet_pytorch_inception_resnet_v1, + get_facenet_pytorch_mtcnn_detector, +) @pytest.fixture @@ -22,10 +25,14 @@ def adapter(): class TestFaceRecognitionSystemFacenetPytorchAdapter: @pytest.mark.integration - def test_compute_feature_vector_with_facenet_pytorch_models(self, adapter, face_image_cv2): + def test_compute_feature_vector_with_facenet_pytorch_models( + self, adapter, face_image_cv2 + ): feature_vector = adapter.compute_feature_vector(face_image_cv2) assert feature_vector.shape == (512,), "Feature vector should have length 512" - assert adapter.feature_vector_length() == 512, "Feature vector length should be 512" + assert ( + adapter.feature_vector_length() == 512 + ), "Feature vector length should be 512" @pytest.mark.integration def test_compute_feature_vector_no_face_detected(self, adapter, example_image): diff --git a/test/training/test_fine_tuning.py b/test/training/test_fine_tuning.py index db4a561..1bf1009 100644 --- a/test/training/test_fine_tuning.py +++ b/test/training/test_fine_tuning.py @@ -1,7 +1,7 @@ import pytest -from src.training.fine_tuning_config import FineTuningConfig from src.training.fine_tuning import FineTuningTrainer +from src.training.fine_tuning_config import FineTuningConfig class TestFineTuningTrainer: @@ -12,9 +12,8 @@ def test_training_on_dummy_dataset(self, tmp_path): epochs=1, dataset="dummy", device="cpu", - model_save_path=str(tmp_path / "model.pth") + model_save_path=str(tmp_path / "model.pth"), ) trainer = FineTuningTrainer(config, use_wandb=False) results = trainer.run() print(results) - diff --git a/test/video_processing/test_processor.py b/test/video_processing/test_processor.py index b7898aa..9badf54 100644 --- a/test/video_processing/test_processor.py +++ b/test/video_processing/test_processor.py @@ -8,7 +8,9 @@ class TestVideoProcessor: @pytest.mark.integration - def test_process_video(self, video_fragment_path, opencv_face_detector_weights_path, tmp_path): + def test_process_video( + self, video_fragment_path, opencv_face_detector_weights_path, tmp_path + ): """Process a short real video""" expected_people = 2 @@ -38,6 +40,9 @@ def test_process_video(self, video_fragment_path, opencv_face_detector_weights_p face_files = list(faces_output_dir.glob("*.jpg")) frame_files = list(camera_output_dir.glob("*.jpg")) - assert len(face_files) == expected_people, f"Expected {expected_people} people in the video" - assert len( - frame_files) == expected_people * config.samples_per_sequence, f"Expected {config.samples_per_sequence} frames per person" + assert ( + len(face_files) == expected_people + ), f"Expected {expected_people} people in the video" + assert ( + len(frame_files) == expected_people * config.samples_per_sequence + ), f"Expected {config.samples_per_sequence} frames per person" From e13573a6c4a10d98919b91c9ba1c57ad188bbdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:08:51 +0100 Subject: [PATCH 16/25] chore: merge benchmark and evaluation packages --- src/benchmark/__init__.py | 11 ----------- src/evaluation/__init__.py | 17 +++++++++++++++++ src/{benchmark => evaluation}/face_pairs.py | 0 src/scripts/benchmark.py | 2 +- src/scripts/experiment_sweep.py | 5 +++-- test/benchmark/__init__.py | 0 .../test_face_pairs.py | 2 +- 7 files changed, 22 insertions(+), 15 deletions(-) delete mode 100644 src/benchmark/__init__.py rename src/{benchmark => evaluation}/face_pairs.py (100%) delete mode 100644 test/benchmark/__init__.py rename test/{benchmark => evaluation}/test_face_pairs.py (97%) diff --git a/src/benchmark/__init__.py b/src/benchmark/__init__.py deleted file mode 100644 index 6b491cb..0000000 --- a/src/benchmark/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from src.benchmark.face_pairs import ( - FacePairsBenchmark, - FacePairsBenchmarkConfig, - FacePairsBenchmarkResult, -) - -__all__ = [ - "FacePairsBenchmarkConfig", - "FacePairsBenchmarkResult", - "FacePairsBenchmark", -] diff --git a/src/evaluation/__init__.py b/src/evaluation/__init__.py index e69de29..78e7ea3 100644 --- a/src/evaluation/__init__.py +++ b/src/evaluation/__init__.py @@ -0,0 +1,17 @@ +from src.evaluation.face_pairs import ( + FacePairsBenchmark, + FacePairsBenchmarkConfig, + FacePairsBenchmarkResult, +) +from src.evaluation.rococo import ( + FaceRecognitionSystemFacenetPytorchAdapter, + log_results_to_wandb, +) + +__all__ = [ + "FacePairsBenchmarkConfig", + "FacePairsBenchmarkResult", + "FacePairsBenchmark", + "FaceRecognitionSystemFacenetPytorchAdapter", + "log_results_to_wandb", +] diff --git a/src/benchmark/face_pairs.py b/src/evaluation/face_pairs.py similarity index 100% rename from src/benchmark/face_pairs.py rename to src/evaluation/face_pairs.py diff --git a/src/scripts/benchmark.py b/src/scripts/benchmark.py index 70d7610..17beddc 100644 --- a/src/scripts/benchmark.py +++ b/src/scripts/benchmark.py @@ -2,7 +2,7 @@ import wandb -from src.benchmark import FacePairsBenchmark, FacePairsBenchmarkConfig +from src.evaluation import FacePairsBenchmark, FacePairsBenchmarkConfig from src.training.models import get_facenet_pytorch_inception_resnet_v1 diff --git a/src/scripts/experiment_sweep.py b/src/scripts/experiment_sweep.py index aeb7f82..600f0e6 100644 --- a/src/scripts/experiment_sweep.py +++ b/src/scripts/experiment_sweep.py @@ -9,8 +9,9 @@ import wandb from rococo_evaluation_lib import RococoDataset, RococoValidation -from src.benchmark import FacePairsBenchmark, FacePairsBenchmarkConfig -from src.evaluation.rococo import ( +from src.evaluation import ( + FacePairsBenchmark, + FacePairsBenchmarkConfig, FaceRecognitionSystemFacenetPytorchAdapter, log_results_to_wandb, ) diff --git a/test/benchmark/__init__.py b/test/benchmark/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/benchmark/test_face_pairs.py b/test/evaluation/test_face_pairs.py similarity index 97% rename from test/benchmark/test_face_pairs.py rename to test/evaluation/test_face_pairs.py index a6ffdcd..89716b4 100644 --- a/test/benchmark/test_face_pairs.py +++ b/test/evaluation/test_face_pairs.py @@ -1,9 +1,9 @@ import pytest -from src.benchmark import FacePairsBenchmark, FacePairsBenchmarkConfig from src.dataset.dummy import DummyFacePairsDataset from src.dataset.lfw import LFWDataset from src.dataset.rof import ROFDataset +from src.evaluation import FacePairsBenchmark, FacePairsBenchmarkConfig from src.training.models import get_facenet_pytorch_inception_resnet_v1 From 29a13119a83a26a196326a9ba428b9e2e0056315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:23:26 +0100 Subject: [PATCH 17/25] chore: dataset package init and cleanup imports --- src/dataset/__init__.py | 21 +++++++++++++++++++++ src/{training => dataset}/data_loaders.py | 6 +++--- src/dataset/dummy.py | 2 +- src/dataset/lfw.py | 2 +- src/dataset/rococo_training.py | 2 +- src/dataset/rof.py | 2 +- src/training/fine_tuning.py | 4 ++-- test/evaluation/test_face_pairs.py | 4 +--- test/evaluation/test_rococo.py | 2 +- 9 files changed, 32 insertions(+), 13 deletions(-) rename src/{training => dataset}/data_loaders.py (95%) diff --git a/src/dataset/__init__.py b/src/dataset/__init__.py index e69de29..ddd5d68 100644 --- a/src/dataset/__init__.py +++ b/src/dataset/__init__.py @@ -0,0 +1,21 @@ +from .data_loaders import ( + get_dummy_loaders, + get_lfw_loaders, + get_rococo_loaders, +) +from .dummy import DummyFacePairsDataset +from .face_pairs import FacePairsDataset +from .lfw import LFWDataset +from .rococo_training import RococoTrainingDataset +from .rof import ROFDataset + +__all__ = [ + "get_lfw_loaders", + "get_rococo_loaders", + "get_dummy_loaders", + "DummyFacePairsDataset", + "FacePairsDataset", + "LFWDataset", + "RococoTrainingDataset", + "ROFDataset", +] diff --git a/src/training/data_loaders.py b/src/dataset/data_loaders.py similarity index 95% rename from src/training/data_loaders.py rename to src/dataset/data_loaders.py index 5097300..d5ab0d4 100644 --- a/src/training/data_loaders.py +++ b/src/dataset/data_loaders.py @@ -1,8 +1,8 @@ from torch.utils.data import DataLoader -from src.dataset.dummy import DummyFacePairsDataset -from src.dataset.lfw import LFWDataset -from src.dataset.rococo_training import RococoTrainingDataset +from .dummy import DummyFacePairsDataset +from .lfw import LFWDataset +from .rococo_training import RococoTrainingDataset def get_lfw_loaders( diff --git a/src/dataset/dummy.py b/src/dataset/dummy.py index 979e921..14356ef 100644 --- a/src/dataset/dummy.py +++ b/src/dataset/dummy.py @@ -1,6 +1,6 @@ from PIL import Image -from src.dataset.face_pairs import FacePairsDataset +from .face_pairs import FacePairsDataset class DummyFacePairsDataset(FacePairsDataset): diff --git a/src/dataset/lfw.py b/src/dataset/lfw.py index 150adc7..856866d 100644 --- a/src/dataset/lfw.py +++ b/src/dataset/lfw.py @@ -2,7 +2,7 @@ from PIL import Image -from src.dataset.face_pairs import FacePairsDataset +from .face_pairs import FacePairsDataset class LFWDataset(FacePairsDataset): diff --git a/src/dataset/rococo_training.py b/src/dataset/rococo_training.py index d2789b5..ba5dac7 100644 --- a/src/dataset/rococo_training.py +++ b/src/dataset/rococo_training.py @@ -2,7 +2,7 @@ from PIL import Image -from src.dataset.face_pairs import FacePairsDataset +from .face_pairs import FacePairsDataset class RococoTrainingDataset(FacePairsDataset): diff --git a/src/dataset/rof.py b/src/dataset/rof.py index 6c4f827..caf1183 100644 --- a/src/dataset/rof.py +++ b/src/dataset/rof.py @@ -6,7 +6,7 @@ from PIL import Image -from src.dataset.face_pairs import FacePairsDataset +from .face_pairs import FacePairsDataset class RofVariant(Enum): diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index 2f695ff..44f1d1a 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -11,12 +11,12 @@ from torchvision import transforms from src.augmentation.get_augmentation_transform import get_augmentation_transform -from src.loss.get_loss_fn import get_loss_fn -from src.training.data_loaders import ( +from src.dataset import ( get_dummy_loaders, get_lfw_loaders, get_rococo_loaders, ) +from src.loss.get_loss_fn import get_loss_fn from src.training.fine_tuning_config import FineTuningConfig from src.training.models import get_facenet_pytorch_inception_resnet_v1 diff --git a/test/evaluation/test_face_pairs.py b/test/evaluation/test_face_pairs.py index 89716b4..a83de1a 100644 --- a/test/evaluation/test_face_pairs.py +++ b/test/evaluation/test_face_pairs.py @@ -1,8 +1,6 @@ import pytest -from src.dataset.dummy import DummyFacePairsDataset -from src.dataset.lfw import LFWDataset -from src.dataset.rof import ROFDataset +from src.dataset import DummyFacePairsDataset, LFWDataset, ROFDataset from src.evaluation import FacePairsBenchmark, FacePairsBenchmarkConfig from src.training.models import get_facenet_pytorch_inception_resnet_v1 diff --git a/test/evaluation/test_rococo.py b/test/evaluation/test_rococo.py index be29cba..30ba19e 100644 --- a/test/evaluation/test_rococo.py +++ b/test/evaluation/test_rococo.py @@ -4,7 +4,7 @@ from rococo_evaluation_lib import NoFaceDetectedException from src.evaluation.rococo import FaceRecognitionSystemFacenetPytorchAdapter -from training.models import ( +from src.training.models import ( get_facenet_pytorch_inception_resnet_v1, get_facenet_pytorch_mtcnn_detector, ) From c1d1a7a280c76060b74140b21f4f2b7907f50713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:34:44 +0100 Subject: [PATCH 18/25] chore: cleanup imports --- src/augmentation/__init__.py | 10 +++++----- src/augmentation/get_augmentation_transform.py | 8 ++++---- src/evaluation/__init__.py | 4 ++-- src/evaluation/face_pairs.py | 4 +--- src/loss/__init__.py | 7 +++++++ src/loss/get_loss_fn.py | 3 ++- src/plots/__init__.py | 5 +++++ src/plots/plot_groups.py | 4 ++-- src/plots/wandb_client.py | 2 +- src/utils/__init__.py | 5 +++++ src/video_processing/__init__.py | 9 +++++++++ src/video_processing/configuration.py | 2 +- src/video_processing/processor.py | 10 +++++----- 13 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/augmentation/__init__.py b/src/augmentation/__init__.py index 5f47911..626a825 100644 --- a/src/augmentation/__init__.py +++ b/src/augmentation/__init__.py @@ -1,8 +1,8 @@ -from src.augmentation.gaussian_noise_pil import GaussianNoisePIL -from src.augmentation.get_augmentation_transform import get_augmentation_transform -from src.augmentation.glasses import AddGlassesAugmentation -from src.augmentation.mask import AddMaskAugmentation -from src.augmentation.random_rectangle import ( +from .gaussian_noise_pil import GaussianNoisePIL +from .get_augmentation_transform import get_augmentation_transform +from .glasses import AddGlassesAugmentation +from .mask import AddMaskAugmentation +from .random_rectangle import ( AddRandomBlackRectangle, AddRandomRectangleAverageColor, AddRandomRectangleRandomColor, diff --git a/src/augmentation/get_augmentation_transform.py b/src/augmentation/get_augmentation_transform.py index 153ef8b..c727f36 100644 --- a/src/augmentation/get_augmentation_transform.py +++ b/src/augmentation/get_augmentation_transform.py @@ -2,10 +2,10 @@ from torchvision.transforms.v2 import ColorJitter, GaussianBlur, Lambda, RandomChoice -from src.augmentation.gaussian_noise_pil import GaussianNoisePIL -from src.augmentation.glasses import AddGlassesAugmentation -from src.augmentation.mask import AddMaskAugmentation -from src.augmentation.random_rectangle import ( +from .gaussian_noise_pil import GaussianNoisePIL +from .glasses import AddGlassesAugmentation +from .mask import AddMaskAugmentation +from .random_rectangle import ( AddRandomBlackRectangle, AddRandomRectangleAverageColor, AddRandomRectangleRandomColor, diff --git a/src/evaluation/__init__.py b/src/evaluation/__init__.py index 78e7ea3..7b8b651 100644 --- a/src/evaluation/__init__.py +++ b/src/evaluation/__init__.py @@ -1,9 +1,9 @@ -from src.evaluation.face_pairs import ( +from .face_pairs import ( FacePairsBenchmark, FacePairsBenchmarkConfig, FacePairsBenchmarkResult, ) -from src.evaluation.rococo import ( +from .rococo import ( FaceRecognitionSystemFacenetPytorchAdapter, log_results_to_wandb, ) diff --git a/src/evaluation/face_pairs.py b/src/evaluation/face_pairs.py index 8843a5e..d83d15c 100644 --- a/src/evaluation/face_pairs.py +++ b/src/evaluation/face_pairs.py @@ -7,9 +7,7 @@ from torch.utils.data import DataLoader from torchvision import transforms -from src.dataset.face_pairs import FacePairsDataset -from src.dataset.lfw import LFWDataset -from src.dataset.rof import ROFDataset +from src.dataset import FacePairsDataset, LFWDataset, ROFDataset from src.training.models import get_facenet_pytorch_inception_resnet_v1 from src.utils.config import default_device_str diff --git a/src/loss/__init__.py b/src/loss/__init__.py index e69de29..5336533 100644 --- a/src/loss/__init__.py +++ b/src/loss/__init__.py @@ -0,0 +1,7 @@ +from .contrastive_loss import ContrastiveLoss +from .get_loss_fn import get_loss_fn + +__all__ = [ + "get_loss_fn", + "ContrastiveLoss", +] diff --git a/src/loss/get_loss_fn.py b/src/loss/get_loss_fn.py index d9ea9bd..97e7b15 100644 --- a/src/loss/get_loss_fn.py +++ b/src/loss/get_loss_fn.py @@ -1,8 +1,9 @@ from torch import nn -from src.loss.contrastive_loss import ContrastiveLoss from src.training.fine_tuning_config import FineTuningConfig +from .contrastive_loss import ContrastiveLoss + def get_loss_fn(config: FineTuningConfig) -> nn.Module: if config.loss_type == "ContrastiveLoss": diff --git a/src/plots/__init__.py b/src/plots/__init__.py index c3b760d..25ca985 100644 --- a/src/plots/__init__.py +++ b/src/plots/__init__.py @@ -2,3 +2,8 @@ Visualizing experiment data downloaded from Weights & Biases API. """ + +from .plot_groups import PlotGroups +from .wandb_client import WandbClient + +__all__ = ["PlotGroups", "WandbClient"] diff --git a/src/plots/plot_groups.py b/src/plots/plot_groups.py index 4d790c6..518ba1f 100644 --- a/src/plots/plot_groups.py +++ b/src/plots/plot_groups.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Any -from src.plots.individual_plots import PlotMaker -from src.plots.sweep_data import SweepData +from .individual_plots import PlotMaker +from .sweep_data import SweepData class PlotGroups: diff --git a/src/plots/wandb_client.py b/src/plots/wandb_client.py index 765d824..e0b5679 100644 --- a/src/plots/wandb_client.py +++ b/src/plots/wandb_client.py @@ -4,7 +4,7 @@ import wandb from pandas import DataFrame -from src.plots.sweep_data import SweepData +from .sweep_data import SweepData class WandbClient: diff --git a/src/utils/__init__.py b/src/utils/__init__.py index e69de29..06f138a 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -0,0 +1,5 @@ +from .config import default_device_str + +__all__ = [ + "default_device_str", +] diff --git a/src/video_processing/__init__.py b/src/video_processing/__init__.py index e69de29..1cab542 100644 --- a/src/video_processing/__init__.py +++ b/src/video_processing/__init__.py @@ -0,0 +1,9 @@ +from .configuration import Configuration +from .processor import VideoProcessor, naive_strategy, offset_strategy + +__all__ = [ + "Configuration", + "VideoProcessor", + "naive_strategy", + "offset_strategy", +] diff --git a/src/video_processing/configuration.py b/src/video_processing/configuration.py index 53e80b8..562c896 100644 --- a/src/video_processing/configuration.py +++ b/src/video_processing/configuration.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Self -from src.video_processing.rect import Rect +from .rect import Rect @dataclass diff --git a/src/video_processing/processor.py b/src/video_processing/processor.py index a715e96..35dc2ea 100644 --- a/src/video_processing/processor.py +++ b/src/video_processing/processor.py @@ -3,16 +3,16 @@ import cv2 import numpy as np -from src.video_processing.configuration import Configuration -from src.video_processing.filters import ( +from .configuration import Configuration +from .filters import ( FaceDetectionFilter, FilterType, SimilarityFilter, StatisticsFilter, ) -from src.video_processing.rect import Rect -from src.video_processing.sequence_marker import SequenceDescriptor, SequenceMarker -from src.video_processing.video_capture import VideoProperties, video_capture +from .rect import Rect +from .sequence_marker import SequenceDescriptor, SequenceMarker +from .video_capture import VideoProperties, video_capture class VideoProcessor: From ca8028787bd03350af3b5b77539615f15e94b205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:46:26 +0100 Subject: [PATCH 19/25] chore: rename utils to common, move models to common --- src/augmentation/glasses.py | 2 +- src/augmentation/mask.py | 2 +- src/common/__init__.py | 15 +++++++++++++++ src/{utils => common}/config.py | 0 src/{training => common}/models.py | 0 src/evaluation/face_pairs.py | 3 +-- src/scripts/benchmark.py | 2 +- src/scripts/detector_evaluation.py | 3 +-- src/scripts/experiment_sweep.py | 10 +++++----- src/scripts/rococo_evaluation.py | 12 ++++++------ src/training/fine_tuning.py | 6 +++--- src/training/fine_tuning_config.py | 2 +- src/utils/__init__.py | 5 ----- test/evaluation/test_face_pairs.py | 2 +- test/evaluation/test_rococo.py | 4 ++-- 15 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 src/common/__init__.py rename src/{utils => common}/config.py (100%) rename src/{training => common}/models.py (100%) delete mode 100644 src/utils/__init__.py diff --git a/src/augmentation/glasses.py b/src/augmentation/glasses.py index e1f8ed7..5c72621 100644 --- a/src/augmentation/glasses.py +++ b/src/augmentation/glasses.py @@ -4,7 +4,7 @@ import numpy as np from PIL import Image, ImageDraw, ImageFilter -from src.training.models import get_dlib_frontal_face_detector, get_dlib_shape_predictor +from src.common import get_dlib_frontal_face_detector, get_dlib_shape_predictor class AddGlassesAugmentation: diff --git a/src/augmentation/mask.py b/src/augmentation/mask.py index 3fec8fc..4194a97 100644 --- a/src/augmentation/mask.py +++ b/src/augmentation/mask.py @@ -2,7 +2,7 @@ import numpy as np from PIL import Image, ImageDraw -from src.training.models import get_dlib_frontal_face_detector, get_dlib_shape_predictor +from src.common import get_dlib_frontal_face_detector, get_dlib_shape_predictor class AddMaskAugmentation: diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..06ee77e --- /dev/null +++ b/src/common/__init__.py @@ -0,0 +1,15 @@ +from .config import default_device_str +from .models import ( + get_dlib_frontal_face_detector, + get_dlib_shape_predictor, + get_facenet_pytorch_inception_resnet_v1, + get_facenet_pytorch_mtcnn_detector, +) + +__all__ = [ + "default_device_str", + "get_facenet_pytorch_inception_resnet_v1", + "get_facenet_pytorch_mtcnn_detector", + "get_dlib_frontal_face_detector", + "get_dlib_shape_predictor", +] diff --git a/src/utils/config.py b/src/common/config.py similarity index 100% rename from src/utils/config.py rename to src/common/config.py diff --git a/src/training/models.py b/src/common/models.py similarity index 100% rename from src/training/models.py rename to src/common/models.py diff --git a/src/evaluation/face_pairs.py b/src/evaluation/face_pairs.py index d83d15c..e4decc1 100644 --- a/src/evaluation/face_pairs.py +++ b/src/evaluation/face_pairs.py @@ -7,9 +7,8 @@ from torch.utils.data import DataLoader from torchvision import transforms +from src.common import default_device_str, get_facenet_pytorch_inception_resnet_v1 from src.dataset import FacePairsDataset, LFWDataset, ROFDataset -from src.training.models import get_facenet_pytorch_inception_resnet_v1 -from src.utils.config import default_device_str @dataclass(frozen=True) diff --git a/src/scripts/benchmark.py b/src/scripts/benchmark.py index 17beddc..32ff8c6 100644 --- a/src/scripts/benchmark.py +++ b/src/scripts/benchmark.py @@ -2,8 +2,8 @@ import wandb +from src.common import get_facenet_pytorch_inception_resnet_v1 from src.evaluation import FacePairsBenchmark, FacePairsBenchmarkConfig -from src.training.models import get_facenet_pytorch_inception_resnet_v1 def main(): diff --git a/src/scripts/detector_evaluation.py b/src/scripts/detector_evaluation.py index a7662e9..fe61c6d 100644 --- a/src/scripts/detector_evaluation.py +++ b/src/scripts/detector_evaluation.py @@ -5,8 +5,7 @@ from PIL import Image from rococo_evaluation_lib import RococoDataset -from src.training.models import get_facenet_pytorch_mtcnn_detector -from src.utils.config import default_device_str +from src.common import default_device_str, get_facenet_pytorch_mtcnn_detector @dataclass diff --git a/src/scripts/experiment_sweep.py b/src/scripts/experiment_sweep.py index 600f0e6..24dea49 100644 --- a/src/scripts/experiment_sweep.py +++ b/src/scripts/experiment_sweep.py @@ -9,6 +9,11 @@ import wandb from rococo_evaluation_lib import RococoDataset, RococoValidation +from src.common import ( + default_device_str, + get_facenet_pytorch_inception_resnet_v1, + get_facenet_pytorch_mtcnn_detector, +) from src.evaluation import ( FacePairsBenchmark, FacePairsBenchmarkConfig, @@ -17,11 +22,6 @@ ) from src.training.fine_tuning import FineTuningTrainer from src.training.fine_tuning_config import FineTuningConfig -from src.training.models import ( - get_facenet_pytorch_inception_resnet_v1, - get_facenet_pytorch_mtcnn_detector, -) -from src.utils.config import default_device_str def main(): diff --git a/src/scripts/rococo_evaluation.py b/src/scripts/rococo_evaluation.py index 6db9bc6..484828c 100644 --- a/src/scripts/rococo_evaluation.py +++ b/src/scripts/rococo_evaluation.py @@ -9,15 +9,15 @@ RococoValidation, ) -from src.evaluation.rococo import ( - FaceRecognitionSystemFacenetPytorchAdapter, - log_results_to_wandb, -) -from src.training.models import ( +from src.common import ( + default_device_str, get_facenet_pytorch_inception_resnet_v1, get_facenet_pytorch_mtcnn_detector, ) -from src.utils.config import default_device_str +from src.evaluation import ( + FaceRecognitionSystemFacenetPytorchAdapter, + log_results_to_wandb, +) def main(): diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index 44f1d1a..4b1b4ca 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -10,15 +10,15 @@ from torch.utils.data import DataLoader from torchvision import transforms -from src.augmentation.get_augmentation_transform import get_augmentation_transform +from src.augmentation import get_augmentation_transform +from src.common import get_facenet_pytorch_inception_resnet_v1 from src.dataset import ( get_dummy_loaders, get_lfw_loaders, get_rococo_loaders, ) -from src.loss.get_loss_fn import get_loss_fn +from src.loss import get_loss_fn from src.training.fine_tuning_config import FineTuningConfig -from src.training.models import get_facenet_pytorch_inception_resnet_v1 def save_model_to_file(model: nn.Module, file_path: str): diff --git a/src/training/fine_tuning_config.py b/src/training/fine_tuning_config.py index a012847..baef077 100644 --- a/src/training/fine_tuning_config.py +++ b/src/training/fine_tuning_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from typing import Literal, Optional -from src.utils.config import default_device_str +from src.common import default_device_str @dataclass diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index 06f138a..0000000 --- a/src/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .config import default_device_str - -__all__ = [ - "default_device_str", -] diff --git a/test/evaluation/test_face_pairs.py b/test/evaluation/test_face_pairs.py index a83de1a..597bef7 100644 --- a/test/evaluation/test_face_pairs.py +++ b/test/evaluation/test_face_pairs.py @@ -1,8 +1,8 @@ import pytest +from src.common import get_facenet_pytorch_inception_resnet_v1 from src.dataset import DummyFacePairsDataset, LFWDataset, ROFDataset from src.evaluation import FacePairsBenchmark, FacePairsBenchmarkConfig -from src.training.models import get_facenet_pytorch_inception_resnet_v1 @pytest.fixture diff --git a/test/evaluation/test_rococo.py b/test/evaluation/test_rococo.py index 30ba19e..b305013 100644 --- a/test/evaluation/test_rococo.py +++ b/test/evaluation/test_rococo.py @@ -3,11 +3,11 @@ import torch from rococo_evaluation_lib import NoFaceDetectedException -from src.evaluation.rococo import FaceRecognitionSystemFacenetPytorchAdapter -from src.training.models import ( +from src.common import ( get_facenet_pytorch_inception_resnet_v1, get_facenet_pytorch_mtcnn_detector, ) +from src.evaluation import FaceRecognitionSystemFacenetPytorchAdapter @pytest.fixture From cffb1dc2338bc4f7870f76f86c313fb15e4e5446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:50:41 +0100 Subject: [PATCH 20/25] chore: make augmenation a subpackage of training --- notebooks/04-fine_tuning.ipynb | 4 ++-- notebooks/07-face_landmark_based_augmentation.ipynb | 2 +- notebooks/08-glasses-augmentation.ipynb | 4 ++-- notebooks/09-all-augmentations-demo.ipynb | 2 +- src/{ => training}/augmentation/__init__.py | 0 src/{ => training}/augmentation/gaussian_noise_pil.py | 0 src/{ => training}/augmentation/get_augmentation_transform.py | 0 src/{ => training}/augmentation/glasses.py | 2 +- src/{ => training}/augmentation/mask.py | 0 src/{ => training}/augmentation/random_rectangle.py | 0 src/training/fine_tuning.py | 2 +- test/dataset/test_rococo.py | 2 +- test/{ => training}/augmentation/__init__.py | 0 test/{ => training}/augmentation/test_gaussain_noise.py | 2 +- test/{ => training}/augmentation/test_glasses.py | 4 ++-- test/{ => training}/augmentation/test_mask.py | 2 +- test/{ => training}/augmentation/test_random_rectangle.py | 2 +- 17 files changed, 14 insertions(+), 14 deletions(-) rename src/{ => training}/augmentation/__init__.py (100%) rename src/{ => training}/augmentation/gaussian_noise_pil.py (100%) rename src/{ => training}/augmentation/get_augmentation_transform.py (100%) rename src/{ => training}/augmentation/glasses.py (98%) rename src/{ => training}/augmentation/mask.py (100%) rename src/{ => training}/augmentation/random_rectangle.py (100%) rename test/{ => training}/augmentation/__init__.py (100%) rename test/{ => training}/augmentation/test_gaussain_noise.py (96%) rename test/{ => training}/augmentation/test_glasses.py (83%) rename test/{ => training}/augmentation/test_mask.py (91%) rename test/{ => training}/augmentation/test_random_rectangle.py (94%) diff --git a/notebooks/04-fine_tuning.ipynb b/notebooks/04-fine_tuning.ipynb index f3b4355..50cd650 100644 --- a/notebooks/04-fine_tuning.ipynb +++ b/notebooks/04-fine_tuning.ipynb @@ -44,8 +44,8 @@ "from torchvision import transforms\n", "from torchvision.utils import make_grid\n", "\n", - "from src.augmentation.mask import GenerateMask\n", - "from src.augmentation.random_rectangle import (\n", + "from src.training.augmentation import GenerateMask\n", + "from src.training.augmentation.random_rectangle import (\n", " AddRandomRectangleAverageColor,\n", " AddRandomRectangleRandomColor,\n", " AddRandomBlackRectangle,\n", diff --git a/notebooks/07-face_landmark_based_augmentation.ipynb b/notebooks/07-face_landmark_based_augmentation.ipynb index 02fd3e4..36c7042 100644 --- a/notebooks/07-face_landmark_based_augmentation.ipynb +++ b/notebooks/07-face_landmark_based_augmentation.ipynb @@ -43,7 +43,7 @@ "\n", "os.chdir(\"..\")\n", "\n", - "from src.augmentation import AddMaskAugmentation\n", + "from src.training.augmentation import AddMaskAugmentation\n", "\n", "os.getcwd()" ] diff --git a/notebooks/08-glasses-augmentation.ipynb b/notebooks/08-glasses-augmentation.ipynb index 109ab7f..4b189f0 100644 --- a/notebooks/08-glasses-augmentation.ipynb +++ b/notebooks/08-glasses-augmentation.ipynb @@ -26,8 +26,8 @@ "\n", "os.chdir(\"..\")\n", "\n", - "from src.training.data_loaders import get_lfw_loaders\n", - "from src.augmentation import AddGlassesAugmentation\n", + "from src.dataset.data_loaders import get_lfw_loaders\n", + "from src.training.augmentation import AddGlassesAugmentation\n", "\n", "os.getcwd()" ] diff --git a/notebooks/09-all-augmentations-demo.ipynb b/notebooks/09-all-augmentations-demo.ipynb index 1b7c80f..bd69a5a 100644 --- a/notebooks/09-all-augmentations-demo.ipynb +++ b/notebooks/09-all-augmentations-demo.ipynb @@ -31,7 +31,7 @@ "\n", "os.chdir(\"..\")\n", "\n", - "from src.augmentation import get_augmentation_transform\n", + "from src.training.augmentation import get_augmentation_transform\n", "\n", "os.getcwd()" ] diff --git a/src/augmentation/__init__.py b/src/training/augmentation/__init__.py similarity index 100% rename from src/augmentation/__init__.py rename to src/training/augmentation/__init__.py diff --git a/src/augmentation/gaussian_noise_pil.py b/src/training/augmentation/gaussian_noise_pil.py similarity index 100% rename from src/augmentation/gaussian_noise_pil.py rename to src/training/augmentation/gaussian_noise_pil.py diff --git a/src/augmentation/get_augmentation_transform.py b/src/training/augmentation/get_augmentation_transform.py similarity index 100% rename from src/augmentation/get_augmentation_transform.py rename to src/training/augmentation/get_augmentation_transform.py diff --git a/src/augmentation/glasses.py b/src/training/augmentation/glasses.py similarity index 98% rename from src/augmentation/glasses.py rename to src/training/augmentation/glasses.py index 5c72621..36b5264 100644 --- a/src/augmentation/glasses.py +++ b/src/training/augmentation/glasses.py @@ -16,7 +16,7 @@ class AddGlassesAugmentation: The glasses are composed of 3 line segments and 2 ellipses, inside the ellipses a Gaussian blur is applied. Example usage: - >>> from src.augmentation import AddGlassesAugmentation + >>> from src.training.augmentation import AddGlassesAugmentation >>> from PIL import Image >>> img = Image.open("image.jpg") >>> add_glasses = AddGlassesAugmentation.with_default_models() diff --git a/src/augmentation/mask.py b/src/training/augmentation/mask.py similarity index 100% rename from src/augmentation/mask.py rename to src/training/augmentation/mask.py diff --git a/src/augmentation/random_rectangle.py b/src/training/augmentation/random_rectangle.py similarity index 100% rename from src/augmentation/random_rectangle.py rename to src/training/augmentation/random_rectangle.py diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index 4b1b4ca..1e5cf77 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -10,7 +10,6 @@ from torch.utils.data import DataLoader from torchvision import transforms -from src.augmentation import get_augmentation_transform from src.common import get_facenet_pytorch_inception_resnet_v1 from src.dataset import ( get_dummy_loaders, @@ -18,6 +17,7 @@ get_rococo_loaders, ) from src.loss import get_loss_fn +from src.training.augmentation import get_augmentation_transform from src.training.fine_tuning_config import FineTuningConfig diff --git a/test/dataset/test_rococo.py b/test/dataset/test_rococo.py index 30088d9..82a48d3 100644 --- a/test/dataset/test_rococo.py +++ b/test/dataset/test_rococo.py @@ -1,4 +1,4 @@ -from dataset.rococo import Frame +from src.dataset.rococo import Frame class TestFrame: diff --git a/test/augmentation/__init__.py b/test/training/augmentation/__init__.py similarity index 100% rename from test/augmentation/__init__.py rename to test/training/augmentation/__init__.py diff --git a/test/augmentation/test_gaussain_noise.py b/test/training/augmentation/test_gaussain_noise.py similarity index 96% rename from test/augmentation/test_gaussain_noise.py rename to test/training/augmentation/test_gaussain_noise.py index 9b16064..d47d13f 100644 --- a/test/augmentation/test_gaussain_noise.py +++ b/test/training/augmentation/test_gaussain_noise.py @@ -3,7 +3,7 @@ import torch from pytest import approx -from src.augmentation.gaussian_noise_pil import GaussianNoisePIL +from src.training.augmentation.gaussian_noise_pil import GaussianNoisePIL class TestGaussianNoisePIL: diff --git a/test/augmentation/test_glasses.py b/test/training/augmentation/test_glasses.py similarity index 83% rename from test/augmentation/test_glasses.py rename to test/training/augmentation/test_glasses.py index 766bf1f..823dd59 100644 --- a/test/augmentation/test_glasses.py +++ b/test/training/augmentation/test_glasses.py @@ -1,8 +1,8 @@ -from test.conftest import images_equal, img_diff +from test.conftest import images_equal import pytest -from src.augmentation.glasses import AddGlassesAugmentation +from src.training.augmentation.glasses import AddGlassesAugmentation class TestAddGlassesAugmentation: diff --git a/test/augmentation/test_mask.py b/test/training/augmentation/test_mask.py similarity index 91% rename from test/augmentation/test_mask.py rename to test/training/augmentation/test_mask.py index 2af9ca3..dcc3b3c 100644 --- a/test/augmentation/test_mask.py +++ b/test/training/augmentation/test_mask.py @@ -2,7 +2,7 @@ import pytest -from src.augmentation import AddMaskAugmentation +from src.training.augmentation import AddMaskAugmentation class TestAddMaskAugmentation: diff --git a/test/augmentation/test_random_rectangle.py b/test/training/augmentation/test_random_rectangle.py similarity index 94% rename from test/augmentation/test_random_rectangle.py rename to test/training/augmentation/test_random_rectangle.py index 35f58d2..c8ab429 100644 --- a/test/augmentation/test_random_rectangle.py +++ b/test/training/augmentation/test_random_rectangle.py @@ -2,7 +2,7 @@ import pytest -from src.augmentation import ( +from src.training.augmentation import ( AddRandomBlackRectangle, AddRandomRectangleAverageColor, AddRandomRectangleRandomColor, From b2e73d21e89dd7f597cbc21248ec68cdce55bd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:55:26 +0100 Subject: [PATCH 21/25] chore: make loss a subpackage of training --- notebooks/04-fine_tuning.ipynb | 2 +- src/training/fine_tuning.py | 2 +- src/{ => training}/loss/__init__.py | 0 src/{ => training}/loss/contrastive_loss.py | 0 src/{ => training}/loss/get_loss_fn.py | 0 test/{ => training}/loss/__init__.py | 0 test/{ => training}/loss/test_contrastive_loss.py | 2 +- 7 files changed, 3 insertions(+), 3 deletions(-) rename src/{ => training}/loss/__init__.py (100%) rename src/{ => training}/loss/contrastive_loss.py (100%) rename src/{ => training}/loss/get_loss_fn.py (100%) rename test/{ => training}/loss/__init__.py (100%) rename test/{ => training}/loss/test_contrastive_loss.py (96%) diff --git a/notebooks/04-fine_tuning.ipynb b/notebooks/04-fine_tuning.ipynb index 50cd650..168c95e 100644 --- a/notebooks/04-fine_tuning.ipynb +++ b/notebooks/04-fine_tuning.ipynb @@ -52,7 +52,7 @@ ")\n", "from src.dataset.lfw import LFWDataset\n", "from src.dataset.rof import ROFDataset\n", - "from src.loss.contrastive_loss import ContrastiveLoss" + "from src.training.loss import ContrastiveLoss" ], "id": "8d8ee930d7320f79", "outputs": [], diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index 1e5cf77..cd26724 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -16,9 +16,9 @@ get_lfw_loaders, get_rococo_loaders, ) -from src.loss import get_loss_fn from src.training.augmentation import get_augmentation_transform from src.training.fine_tuning_config import FineTuningConfig +from src.training.loss import get_loss_fn def save_model_to_file(model: nn.Module, file_path: str): diff --git a/src/loss/__init__.py b/src/training/loss/__init__.py similarity index 100% rename from src/loss/__init__.py rename to src/training/loss/__init__.py diff --git a/src/loss/contrastive_loss.py b/src/training/loss/contrastive_loss.py similarity index 100% rename from src/loss/contrastive_loss.py rename to src/training/loss/contrastive_loss.py diff --git a/src/loss/get_loss_fn.py b/src/training/loss/get_loss_fn.py similarity index 100% rename from src/loss/get_loss_fn.py rename to src/training/loss/get_loss_fn.py diff --git a/test/loss/__init__.py b/test/training/loss/__init__.py similarity index 100% rename from test/loss/__init__.py rename to test/training/loss/__init__.py diff --git a/test/loss/test_contrastive_loss.py b/test/training/loss/test_contrastive_loss.py similarity index 96% rename from test/loss/test_contrastive_loss.py rename to test/training/loss/test_contrastive_loss.py index bf4d2a6..a9f105f 100644 --- a/test/loss/test_contrastive_loss.py +++ b/test/training/loss/test_contrastive_loss.py @@ -1,7 +1,7 @@ import torch from pytest import approx -from src.loss.contrastive_loss import ContrastiveLoss +from src.training.loss import ContrastiveLoss def test_equal_vectors_same_person(): From 08eaeec1813813cbba41c045c8f0863977643474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:59:13 +0100 Subject: [PATCH 22/25] chore: fine tuning package init and relative imports --- src/training/__init__.py | 4 ++++ src/training/fine_tuning.py | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/training/__init__.py b/src/training/__init__.py index e69de29..4600f6e 100644 --- a/src/training/__init__.py +++ b/src/training/__init__.py @@ -0,0 +1,4 @@ +from .fine_tuning import FineTuningTrainer +from .fine_tuning_config import FineTuningConfig + +__all__ = ["FineTuningTrainer", "FineTuningConfig"] diff --git a/src/training/fine_tuning.py b/src/training/fine_tuning.py index cd26724..ea0c4ef 100644 --- a/src/training/fine_tuning.py +++ b/src/training/fine_tuning.py @@ -16,9 +16,10 @@ get_lfw_loaders, get_rococo_loaders, ) -from src.training.augmentation import get_augmentation_transform -from src.training.fine_tuning_config import FineTuningConfig -from src.training.loss import get_loss_fn + +from .augmentation import get_augmentation_transform +from .fine_tuning_config import FineTuningConfig +from .loss import get_loss_fn def save_model_to_file(model: nn.Module, file_path: str): From f3ac0d0d5f66ea87c23502d5a0da1e163bc4aa34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 20:59:30 +0100 Subject: [PATCH 23/25] chore: script package imports --- src/scripts/experiment_sweep.py | 3 +-- src/scripts/plots_for_publication.py | 3 +-- src/scripts/process_video.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/scripts/experiment_sweep.py b/src/scripts/experiment_sweep.py index 24dea49..aaf5d9f 100644 --- a/src/scripts/experiment_sweep.py +++ b/src/scripts/experiment_sweep.py @@ -20,8 +20,7 @@ FaceRecognitionSystemFacenetPytorchAdapter, log_results_to_wandb, ) -from src.training.fine_tuning import FineTuningTrainer -from src.training.fine_tuning_config import FineTuningConfig +from src.training import FineTuningConfig, FineTuningTrainer def main(): diff --git a/src/scripts/plots_for_publication.py b/src/scripts/plots_for_publication.py index 74018a7..163d3d0 100644 --- a/src/scripts/plots_for_publication.py +++ b/src/scripts/plots_for_publication.py @@ -1,12 +1,11 @@ from dataclasses import dataclass from pathlib import Path +from src.plots import PlotGroups, WandbClient from src.plots.parameter_importance import ( experiment_05_parameter_importance, experiment_06_parameter_importance, ) -from src.plots.plot_groups import PlotGroups -from src.plots.wandb_client import WandbClient @dataclass diff --git a/src/scripts/process_video.py b/src/scripts/process_video.py index c310c9d..5223ca1 100644 --- a/src/scripts/process_video.py +++ b/src/scripts/process_video.py @@ -70,8 +70,7 @@ from datetime import datetime from time import perf_counter -from src.video_processing.configuration import Configuration -from src.video_processing.processor import naive_strategy, offset_strategy +from src.video_processing import Configuration, naive_strategy, offset_strategy def main(): From b033c3411d1cac2d320a69ba7cc5fe1c0937d5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 21:21:41 +0100 Subject: [PATCH 24/25] feat: update mkdocs config --- docs/mkdocs/api/augmentation.md | 3 --- docs/mkdocs/api/common.md | 3 +++ docs/mkdocs/api/evaluation.md | 3 +++ docs/mkdocs/api/loss.md | 3 --- docs/mkdocs/api/plots.md | 3 +++ docs/mkdocs/api/scripts.md | 3 +++ docs/mkdocs/api/training.md | 5 +++++ docs/mkdocs/api/validation.md | 3 --- docs/mkdocs/api/video_processing.md | 3 +++ docs/mkdocs/index.md | 4 ++-- mkdocs.yml | 13 ++++++++----- 11 files changed, 30 insertions(+), 16 deletions(-) delete mode 100644 docs/mkdocs/api/augmentation.md create mode 100644 docs/mkdocs/api/common.md create mode 100644 docs/mkdocs/api/evaluation.md delete mode 100644 docs/mkdocs/api/loss.md create mode 100644 docs/mkdocs/api/plots.md create mode 100644 docs/mkdocs/api/scripts.md create mode 100644 docs/mkdocs/api/training.md delete mode 100644 docs/mkdocs/api/validation.md create mode 100644 docs/mkdocs/api/video_processing.md diff --git a/docs/mkdocs/api/augmentation.md b/docs/mkdocs/api/augmentation.md deleted file mode 100644 index 76a79fe..0000000 --- a/docs/mkdocs/api/augmentation.md +++ /dev/null @@ -1,3 +0,0 @@ -# Augmentation - -::: src.augmentation \ No newline at end of file diff --git a/docs/mkdocs/api/common.md b/docs/mkdocs/api/common.md new file mode 100644 index 0000000..c45b997 --- /dev/null +++ b/docs/mkdocs/api/common.md @@ -0,0 +1,3 @@ +# Common + +::: src.common \ No newline at end of file diff --git a/docs/mkdocs/api/evaluation.md b/docs/mkdocs/api/evaluation.md new file mode 100644 index 0000000..94e1aee --- /dev/null +++ b/docs/mkdocs/api/evaluation.md @@ -0,0 +1,3 @@ +# Evaluation + +::: src.evaluation \ No newline at end of file diff --git a/docs/mkdocs/api/loss.md b/docs/mkdocs/api/loss.md deleted file mode 100644 index 61107bb..0000000 --- a/docs/mkdocs/api/loss.md +++ /dev/null @@ -1,3 +0,0 @@ -# Loss - -::: src.loss \ No newline at end of file diff --git a/docs/mkdocs/api/plots.md b/docs/mkdocs/api/plots.md new file mode 100644 index 0000000..c89ee07 --- /dev/null +++ b/docs/mkdocs/api/plots.md @@ -0,0 +1,3 @@ +# Plots + +::: src.plots \ No newline at end of file diff --git a/docs/mkdocs/api/scripts.md b/docs/mkdocs/api/scripts.md new file mode 100644 index 0000000..f6d3490 --- /dev/null +++ b/docs/mkdocs/api/scripts.md @@ -0,0 +1,3 @@ +# Scripts + +::: src.scripts \ No newline at end of file diff --git a/docs/mkdocs/api/training.md b/docs/mkdocs/api/training.md new file mode 100644 index 0000000..f51d772 --- /dev/null +++ b/docs/mkdocs/api/training.md @@ -0,0 +1,5 @@ +# Training + +::: src.training + options: + show_submodules: true diff --git a/docs/mkdocs/api/validation.md b/docs/mkdocs/api/validation.md deleted file mode 100644 index ab1a6cb..0000000 --- a/docs/mkdocs/api/validation.md +++ /dev/null @@ -1,3 +0,0 @@ -# Validation - -::: src.validation \ No newline at end of file diff --git a/docs/mkdocs/api/video_processing.md b/docs/mkdocs/api/video_processing.md new file mode 100644 index 0000000..179fdf4 --- /dev/null +++ b/docs/mkdocs/api/video_processing.md @@ -0,0 +1,3 @@ +# Video Processing + +::: src.video_processing \ No newline at end of file diff --git a/docs/mkdocs/index.md b/docs/mkdocs/index.md index 6fd3c2a..b6c9962 100644 --- a/docs/mkdocs/index.md +++ b/docs/mkdocs/index.md @@ -1,3 +1,3 @@ -# Dokumentacja projektu +# Project documentation -Wygenerowana przy pomocy MkDocs \ No newline at end of file +Generated with MkDocs. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 08f117c..e5aff22 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: "Dokumentacja projektu" +site_name: "Project documentation" docs_dir: "docs/mkdocs" theme: name: "material" @@ -21,9 +21,12 @@ plugins: show_function_docstring: true nav: - Home: index.md - - API: - - Augmentation: api/augmentation.md + - API Reference: + - Common: api/common.md - Dataset: api/dataset.md - - Loss: api/loss.md - - Validation: api/validation.md + - Evaluation: api/evaluation.md + - Plots: api/plots.md + - Scripts: api/scripts.md + - Training: api/training.md + - Video Processing: api/video_processing.md - Wymagania: doorstop/index.html \ No newline at end of file From 9e7365416192e7c43bbf71cbbadc092be8dfaf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Garbowski?= Date: Sat, 8 Nov 2025 21:24:39 +0100 Subject: [PATCH 25/25] feat: remove doorstop from project --- .github/workflows/mkdocs.yml | 9 +--- Justfile | 4 -- docs/doorstop/WF/.doorstop.yml | 5 -- docs/doorstop/WF/WF001.yml | 10 ---- docs/doorstop/WT/.doorstop.yml | 6 --- docs/doorstop/WT/WT001.yml | 12 ----- mkdocs.yml | 3 +- pyproject.toml | 1 - uv.lock | 97 ---------------------------------- 9 files changed, 3 insertions(+), 144 deletions(-) delete mode 100644 docs/doorstop/WF/.doorstop.yml delete mode 100644 docs/doorstop/WF/WF001.yml delete mode 100644 docs/doorstop/WT/.doorstop.yml delete mode 100644 docs/doorstop/WT/WT001.yml diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 4882e03..41d9856 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -1,4 +1,4 @@ -name: Deploy MkDocs and Doorstop to GitHub Pages +name: Deploy MkDocs to GitHub Pages on: push: @@ -7,7 +7,6 @@ on: paths: - 'src/**' - 'docs/mkdocs/**' - - 'docs/doorstop/**' workflow_dispatch: jobs: @@ -25,11 +24,7 @@ jobs: - name: Install dependencies run: | - pip install mkdocs mkdocs-material mkdocstrings[python] doorstop - - - name: Generate Doorstop HTML - run: | - doorstop publish all docs/mkdocs/doorstop + pip install mkdocs mkdocs-material mkdocstrings[python] - name: Build and deploy MkDocs run: | diff --git a/Justfile b/Justfile index 4eb54bd..0aaa1e8 100644 --- a/Justfile +++ b/Justfile @@ -112,10 +112,6 @@ metrics_all: metrics_raw_src metrics_raw_tests metrics_raw_notebooks metrics_cyc # === Documentation === -# Build doorstop documentation -doorstop_build: - uv run doorstop publish all site/doorstop - # Serve documentation locally docs: uv run mkdocs serve diff --git a/docs/doorstop/WF/.doorstop.yml b/docs/doorstop/WF/.doorstop.yml deleted file mode 100644 index 0b81268..0000000 --- a/docs/doorstop/WF/.doorstop.yml +++ /dev/null @@ -1,5 +0,0 @@ -settings: - digits: 3 - itemformat: yaml - prefix: WF - sep: '' diff --git a/docs/doorstop/WF/WF001.yml b/docs/doorstop/WF/WF001.yml deleted file mode 100644 index 465005a..0000000 --- a/docs/doorstop/WF/WF001.yml +++ /dev/null @@ -1,10 +0,0 @@ -active: true -derived: false -header: '' -level: 1.0 -links: [] -normative: true -ref: '' -reviewed: KkjTuFFAY9xLIk27cikSjKfv939TdA42tyEL7Xm0ez8= -text: | - Model jest walidowany na zbiorze Rococo diff --git a/docs/doorstop/WT/.doorstop.yml b/docs/doorstop/WT/.doorstop.yml deleted file mode 100644 index 3a87d47..0000000 --- a/docs/doorstop/WT/.doorstop.yml +++ /dev/null @@ -1,6 +0,0 @@ -settings: - digits: 3 - itemformat: yaml - parent: WF - prefix: WT - sep: '' diff --git a/docs/doorstop/WT/WT001.yml b/docs/doorstop/WT/WT001.yml deleted file mode 100644 index 816ad01..0000000 --- a/docs/doorstop/WT/WT001.yml +++ /dev/null @@ -1,12 +0,0 @@ -active: true -derived: false -header: '' -level: 1.0 -links: -- WF001: KkjTuFFAY9xLIk27cikSjKfv939TdA42tyEL7Xm0ez8= -normative: true -ref: '' -reviewed: K6mIjvT6I4-7imX8YIB3nguXp06pW4hVjjLaYSnTYy8= -test: untested -text: | - Jakiƛ test diff --git a/mkdocs.yml b/mkdocs.yml index e5aff22..5b7f500 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,5 +28,4 @@ nav: - Plots: api/plots.md - Scripts: api/scripts.md - Training: api/training.md - - Video Processing: api/video_processing.md - - Wymagania: doorstop/index.html \ No newline at end of file + - Video Processing: api/video_processing.md \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5abf779..8b676fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ dev = [ "mkdocs>=1.6.1", "mkdocstrings[python]>=0.29.1", "mkdocs-material>=9.6.13", - "doorstop>=3.0", "pytest-cov>=6.1.1", ] diff --git a/uv.lock b/uv.lock index 9709d1f..b04d63e 100644 --- a/uv.lock +++ b/uv.lock @@ -191,15 +191,6 @@ css = [ { name = "tinycss2" }, ] -[[package]] -name = "bottle" -version = "0.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" }, -] - [[package]] name = "certifi" version = "2025.10.5" @@ -377,27 +368,6 @@ version = "20.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/28/f4/f8949b18ec1df2ef05fc2ea1d1dd82ff2d050b8704b7d0d088017315c221/dlib-20.0.0.tar.gz", hash = "sha256:9ab6a6fe113cc36a20c3f611c57fa6a07f18d1169bd04efb85c32e21b23b7d2b", size = 3310015, upload-time = "2025-05-28T01:22:16.141Z" } -[[package]] -name = "doorstop" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bottle" }, - { name = "markdown" }, - { name = "openpyxl" }, - { name = "plantuml-markdown" }, - { name = "python-frontmatter" }, - { name = "python-markdown-math" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "six" }, - { name = "verchew" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/52/758c5c2955b64c32fb029a537ff307eca8940ee22ec165ae5d671c55590a/doorstop-3.0.2.tar.gz", hash = "sha256:b444fd4bafe67a7f265bee714e1c0ade844b9490f79947946f91efd343f74772", size = 1376290, upload-time = "2025-06-16T19:34:25.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/65/337211babe5bd4d2660abf0e594f4e7fe1af2a8848154509442ed5310746/doorstop-3.0.2-py3-none-any.whl", hash = "sha256:366054bef70c5d2f27f5d4b0eea056eb3244cc6fdbcb434e0b1ba83f324f4ca4", size = 1409948, upload-time = "2025-06-16T19:34:22.104Z" }, -] - [[package]] name = "dotenv" version = "0.9.9" @@ -409,15 +379,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, ] -[[package]] -name = "et-xmlfile" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, -] - [[package]] name = "executing" version = "2.2.1" @@ -1524,18 +1485,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, ] -[[package]] -name = "openpyxl" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "et-xmlfile" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1633,20 +1582,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, ] -[[package]] -name = "plantuml-markdown" -version = "3.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "requests" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/c5/1749bae9c838a78d9f6bb0c628fd47251572d5ad1502e33b51fb72582838/plantuml_markdown-3.11.1.tar.gz", hash = "sha256:0ac39c5d13d3ea8b84ec88f57369a85d058ff15c0c225591dd649b327586142c", size = 32120, upload-time = "2025-02-07T17:30:57.131Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/06/fed3b67b5488854eb7934bfbbb580d0651d8af243ffb39937cb00838bc3d/plantuml_markdown-3.11.1-py3-none-any.whl", hash = "sha256:52c8507be5b4e5b57e3c4c1cfbe8894b6204f4b6bac2279896efb03d3b79bacf", size = 20116, upload-time = "2025-02-07T17:30:54.88Z" }, -] - [[package]] name = "platformdirs" version = "4.5.0" @@ -1883,18 +1818,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] -[[package]] -name = "python-frontmatter" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/de/910fa208120314a12f9a88ea63e03707261692af782c99283f1a2c8a5e6f/python-frontmatter-1.1.0.tar.gz", hash = "sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d", size = 16256, upload-time = "2024-01-16T18:50:04.052Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834, upload-time = "2024-01-16T18:50:00.911Z" }, -] - [[package]] name = "python-json-logger" version = "4.0.0" @@ -1904,15 +1827,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] -[[package]] -name = "python-markdown-math" -version = "0.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/e1/24021b30466ca2985352ec1f15b3cb38e0b62ba742c46820c33ddf7114a8/python-markdown-math-0.6.tar.gz", hash = "sha256:c68d8cb9695cb7b435484403dc18941d1bad0ff148e4166d9417046a0d5d3022", size = 6406, upload-time = "2018-06-15T12:52:47.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/81/2c86570437821d77b90a6d939d54e11b507b71785850840a5e56d8febeca/python_markdown_math-0.6-py2.py3-none-any.whl", hash = "sha256:d443e264cf063623a5f02b0c9730867e5172b31d49967da424ed3457c25b2848", size = 3819, upload-time = "2018-06-15T12:53:21.726Z" }, -] - [[package]] name = "pytokens" version = "0.2.0" @@ -2281,7 +2195,6 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "black", extra = ["jupyter"] }, - { name = "doorstop" }, { name = "isort" }, { name = "mkdocs" }, { name = "mkdocs-material" }, @@ -2315,7 +2228,6 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "black", extras = ["jupyter"], specifier = ">=25.1.0" }, - { name = "doorstop", specifier = ">=3.0" }, { name = "isort", specifier = ">=6.0.1" }, { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-material", specifier = ">=9.6.13" }, @@ -2491,15 +2403,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] -[[package]] -name = "verchew" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/69/543e3a2fb6a8fb4dcc0d18bc34ebd54d789dc844c8f83af0926ba0b8bcf6/verchew-3.4.2.tar.gz", hash = "sha256:98f3f8e4894528afe316879ac848d1a6c8b47168a3b3de2e44fc2f5b83b29149", size = 9528, upload-time = "2023-09-15T17:02:05.577Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/8a/697eb1daee017b9313336c4899c897542d8c011cbbfbb1a6e87a8370fd89/verchew-3.4.2-py3-none-any.whl", hash = "sha256:d000ec54ccc87240feae05cf9e37b29016ca73406b6f39af69114a32773dfb7e", size = 10629, upload-time = "2023-09-15T17:02:02.166Z" }, -] - [[package]] name = "wandb" version = "0.22.2"