| from __future__ import annotations | |
| import pytest | |
| def torch_or_skip(): | |
| return pytest.importorskip("torch") | |
| def test_tensor_to_pil_images_quantizes_rgb_range(): | |
| torch = torch_or_skip() | |
| from uw_nriqa.tensor import tensor_to_pil_images | |
| images = torch.tensor([[[[0.0, 0.5], [1.0, 2.0]], [[-1.0, 0.25], [0.75, 1.0]], [[0.1, 0.2], [0.3, 0.4]]]]) | |
| pil = tensor_to_pil_images(images) | |
| assert len(pil) == 1 | |
| assert pil[0].mode == "RGB" | |
| assert pil[0].size == (2, 2) | |
| assert pil[0].getpixel((0, 0)) == (0, 0, 26) | |
| assert pil[0].getpixel((1, 0)) == (128, 64, 51) | |
| assert pil[0].getpixel((0, 1)) == (255, 191, 77) | |
| assert pil[0].getpixel((1, 1)) == (255, 255, 102) | |
| def test_tensor_to_pil_images_rejects_non_bchw_rgb(): | |
| torch = torch_or_skip() | |
| from uw_nriqa.tensor import tensor_to_pil_images | |
| with pytest.raises(ValueError, match="BCHW RGB"): | |
| tensor_to_pil_images(torch.zeros(1, 1, 8, 8)) | |
| def test_tensor_direct_rejects_unsupported_methods(): | |
| torch = torch_or_skip() | |
| from uw_nriqa.tensor import score_tensor | |
| with pytest.raises(ValueError, match="unsupported"): | |
| score_tensor(torch.zeros(1, 3, 8, 8), methods=["tuda"], preload=False) | |
| with pytest.raises(ValueError, match="rsuia"): | |
| score_tensor(torch.zeros(1, 3, 8, 8), methods=["rsuia"], preload=False) | |
| def test_tensor_direct_traditional_metrics_do_not_require_pil(monkeypatch): | |
| torch = torch_or_skip() | |
| from uw_nriqa.tensor import score_tensor | |
| def fail_pil(_images): | |
| raise AssertionError("PIL conversion should not be used for tensor metrics") | |
| monkeypatch.setattr("uw_nriqa.tensor.tensor_to_pil_images", fail_pil) | |
| images = torch.rand(4, 3, 64, 64) | |
| scores = score_tensor(images, methods=["uciqe", "uiqm"], preload=False) | |
| assert scores.shape == (4, 2) | |
| assert torch.isfinite(scores).all() | |
| def test_tensor_uciqe_tracks_legacy_metric(): | |
| torch = torch_or_skip() | |
| from uw_nriqa.metrics import calculate_uciqe | |
| from uw_nriqa.tensor import score_tensor, tensor_to_pil_images | |
| images = torch.rand(3, 3, 64, 64) | |
| tensor_scores = score_tensor(images, methods=["uciqe"], preload=False)[:, 0] | |
| legacy_scores = torch.tensor( | |
| [calculate_uciqe(pil) for pil in tensor_to_pil_images(images)], | |
| dtype=torch.float32, | |
| ) | |
| assert torch.allclose(tensor_scores, legacy_scores, atol=0.02) | |
| def test_tensor_uiqm_tracks_legacy_metric(): | |
| torch = torch_or_skip() | |
| import numpy as np | |
| from uw_nriqa.metrics import calculate_uiqm | |
| from uw_nriqa.tensor import score_tensor, tensor_to_pil_images | |
| rng = np.random.default_rng(20260602) | |
| arrays = rng.integers(0, 256, size=(4, 72, 96, 3), dtype=np.uint8) | |
| images = torch.from_numpy(arrays.copy()).permute(0, 3, 1, 2).to(dtype=torch.float32) / 255.0 | |
| tensor_scores = score_tensor(images, methods=["uiqm"], preload=False)[:, 0] | |
| legacy_scores = torch.tensor( | |
| [calculate_uiqm(pil) for pil in tensor_to_pil_images(images)], | |
| dtype=torch.float32, | |
| ) | |
| assert torch.allclose(tensor_scores, legacy_scores, atol=1e-4, rtol=1e-4) | |
| def test_tensor_niqe_tracks_legacy_metric(): | |
| pytest.importorskip("pyiqa") | |
| torch = torch_or_skip() | |
| import numpy as np | |
| from uw_nriqa.metrics import calculate_niqe | |
| from uw_nriqa.tensor import score_tensor, tensor_to_pil_images | |
| rng = np.random.default_rng(20260602) | |
| arrays = rng.integers(0, 256, size=(3, 256, 256, 3), dtype=np.uint8) | |
| images = torch.from_numpy(arrays.copy()).permute(0, 3, 1, 2).to(dtype=torch.float32) / 255.0 | |
| tensor_scores = score_tensor(images, methods=["niqe"], preload=False)[:, 0] | |
| legacy_scores = torch.tensor( | |
| [calculate_niqe(pil) for pil in tensor_to_pil_images(images)], | |
| dtype=torch.float32, | |
| ) | |
| assert torch.allclose(tensor_scores, legacy_scores, atol=1e-4, rtol=1e-4) | |
| def test_tensor_local_nuiq_tracks_image_batch_path(): | |
| torch = torch_or_skip() | |
| import numpy as np | |
| from PIL import Image | |
| from uw_nriqa.nuiq import calculate_nuiq_local | |
| from uw_nriqa.tensor import score_tensor | |
| rng = np.random.default_rng(123) | |
| arrays = rng.integers(0, 256, size=(4, 36, 44, 3), dtype=np.uint8) | |
| images = [Image.fromarray(arr) for arr in arrays] | |
| tensor = torch.from_numpy(arrays).permute(0, 3, 1, 2).to(dtype=torch.float32) / 255.0 | |
| image_scores = torch.tensor(calculate_nuiq_local(images), dtype=torch.float32) | |
| tensor_scores = score_tensor(tensor, methods=["nuiq_local"], preload=False)[:, 0] | |
| assert torch.all(torch.isfinite(tensor_scores)) | |
| assert torch.allclose(tensor_scores, image_scores, atol=1e-3) | |
| def test_predict_tensor_uses_tensor_scorer(monkeypatch): | |
| torch = torch_or_skip() | |
| from uw_nriqa import NRIQAEvaluator | |
| def fake_score_tensor_with_evaluator(evaluator, images): | |
| assert evaluator.methods == ["uranker", "uciqe"] | |
| assert tuple(images.shape) == (2, 3, 8, 8) | |
| return torch.tensor([[1.0, 0.5], [2.0, 0.7]], dtype=torch.float32) | |
| monkeypatch.setattr("uw_nriqa.tensor.score_tensor_with_evaluator", fake_score_tensor_with_evaluator) | |
| evaluator = NRIQAEvaluator(["uranker", "uciqe"], preload=False) | |
| scores = evaluator.predict_tensor(torch.zeros(2, 3, 8, 8), raise_errors=True) | |
| assert scores == {"uranker": [1.0, 2.0], "uciqe": [0.5, pytest.approx(0.7)]} | |
| def test_uranker_histogram_matches_legacy_histc(): | |
| torch = torch_or_skip() | |
| from uw_nriqa.vendor.UnderwaterRanker import utils | |
| images = torch.tensor( | |
| [ | |
| [ | |
| [[0.0, 0.1, 0.5], [1.0, -0.1, 1.2]], | |
| [[0.25, 0.25, 0.25], [0.75, 0.75, 0.75]], | |
| [[0.0, 1.0, 0.5], [0.25, 0.75, 0.33]], | |
| ], | |
| [ | |
| [[0.2, 0.4, 0.6], [0.8, 1.0, 0.0]], | |
| [[0.9, 0.7, 0.5], [0.3, 0.1, 0.0]], | |
| [[1.1, -0.2, 0.4], [0.4, 0.4, 0.4]], | |
| ], | |
| ], | |
| dtype=torch.float32, | |
| ) | |
| expected = [] | |
| for image in images: | |
| channels = [torch.histc(channel, 64, min=0.0, max=1.0) for channel in image] | |
| expected.append(torch.cat(channels)) | |
| expected = torch.stack(expected, dim=0).unsqueeze(1) | |
| actual = utils.build_historgram(images) | |
| assert actual.shape == (2, 1, 192) | |
| assert torch.equal(actual, expected) | |