File size: 4,155 Bytes
059fc13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""Tests for the multi-modal image encoder (``palimseste.vision``).

Verifies:
  - encode_image returns a valid HV of the right dimensionality
  - similar images produce similar HVs (correlated)
  - dissimilar images produce quasi-orthogonal HVs
  - ImageMemory stores and retrieves by similarity
  - query_by_image finds the most similar stored image
"""

from __future__ import annotations

import numpy as np
import pytest

from palimseste import hv
from palimseste.vision import ImageEncoder, ImageMemory, encode_image


def _solid_image(color: tuple[int, int, int], size: int = 64) -> np.ndarray:
    """A solid-color image."""
    img = np.zeros((size, size, 3), dtype=np.uint8)
    img[:] = color
    return img


def _random_image(seed: int, size: int = 64) -> np.ndarray:
    rng = np.random.default_rng(seed)
    return rng.integers(0, 256, size=(size, size, 3), dtype=np.uint8)


class TestImageEncoder:
    def test_encode_returns_valid_hv(self):
        enc = ImageEncoder(D=2000, grid=8)
        img = _random_image(0)
        h = enc.encode(img)
        assert isinstance(h, hv.HV)
        assert h.D == 2000

    def test_similar_images_are_similar(self):
        enc = ImageEncoder(D=5000, grid=8)
        img1 = _solid_image((100, 50, 200))
        img2 = _solid_image((105, 55, 195))  # very close colors
        h1 = enc.encode(img1)
        h2 = enc.encode(img2)
        sim = hv.similarity(h1, h2)
        assert sim > 0.3, f"similar images should be correlated, got sim={sim}"

    def test_dissimilar_images_are_quasi_orthogonal(self):
        enc = ImageEncoder(D=5000, grid=8)
        img1 = _solid_image((0, 0, 0))    # black
        img2 = _solid_image((255, 255, 255))  # white
        # black vs white are opposite but share structure (solid), so not
        # fully orthogonal. Use random images instead.
        img3 = _random_image(1)
        img4 = _random_image(2)
        h3 = enc.encode(img3)
        h4 = enc.encode(img4)
        sim = hv.similarity(h3, h4)
        # random images with 8x8 grid share mean-RGB distribution structure,
        # so they're not fully orthogonal but should be well below 0.5
        assert sim < 0.5, f"random images should be weakly correlated, got sim={sim}"

    def test_same_image_same_hv(self):
        enc = ImageEncoder(D=3000, grid=8)
        img = _random_image(42)
        h1 = enc.encode(img)
        h2 = enc.encode(img)
        assert h1 == h2

    def test_encode_image_convenience(self):
        img = _random_image(0, size=32)
        h = encode_image(img, D=2000, grid=4)
        assert h.D == 2000

    def test_invalid_shape_raises(self):
        enc = ImageEncoder(D=1000)
        with pytest.raises(ValueError):
            enc.encode(np.zeros((10, 10), dtype=np.uint8))

    def test_small_image_works(self):
        enc = ImageEncoder(D=2000, grid=8)
        img = _random_image(0, size=4)  # smaller than grid
        h = enc.encode(img)
        assert h.D == 2000


class TestImageMemory:
    def test_store_and_retrieve(self):
        mem = ImageMemory(D=3000)
        img1 = _solid_image((255, 0, 0), size=32)
        img2 = _solid_image((0, 255, 0), size=32)
        mem.store(img1, label="red")
        mem.store(img2, label="green")
        results = mem.query_by_image(img1, top_k=1)
        assert len(results) == 1
        idx, sim, label = results[0]
        assert label == "red"
        assert sim > 0.5

    def test_retrieve_top_k(self):
        mem = ImageMemory(D=3000)
        for i in range(10):
            img = _solid_image((i * 25, 0, 0), size=32)
            mem.store(img, label=f"color_{i}")
        query = _solid_image((50, 0, 0), size=32)
        results = mem.query_by_image(query, top_k=3)
        assert len(results) == 3
        # results should be sorted by descending similarity
        assert results[0][1] >= results[1][1] >= results[2][1]

    def test_empty_memory(self):
        mem = ImageMemory(D=1000)
        assert mem.retrieve(hv.random_hv(1000)) == []

    def test_size(self):
        mem = ImageMemory(D=1000)
        assert mem.size == 0
        mem.store(_random_image(0, size=16))
        assert mem.size == 1