File size: 7,464 Bytes
ef19228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
"""Build the paired evaluation dataset from the raw item photos.



Input:  Dataset/items/<item>/<front|back|side>.jpeg          photo set 1 of every item

        Dataset/items/<item>/set2/<front|back|side>.jpeg     optional second, independent photo set

                                                             (an angle missing in set2 falls back to set 1)

Output: Dataset/{same,different}/{delivery_images,return_images}/{front,back,side}/<pair_id>.jpeg

        Dataset/pairs.csv



Genuine ("same") pairs come in two kinds:

- simulated re-capture: set-1 photos, with the return side given a different rotation,

  zoom, framing, exposure, white balance, blur and JPEG quality. Easy: it is the same photo.

- real re-capture: delivery photos from one set, return photos from the other set, taken

  separately (different place, angle, lighting). No digital changes. This is the honest test.

Fraud ("different") pairs: every ordered pair of different items, set-1 photos, with the same

simulated capture noise on the return side, so the classes differ only by the item.



Pair IDs and seeds of earlier pairs never change when items are added, so results stay comparable.

"""
import csv
import io
import random
import shutil
import sys
from itertools import permutations
from pathlib import Path

import numpy as np
from PIL import Image, ImageEnhance, ImageFilter, ImageOps

ROOT = Path(__file__).resolve().parent.parent
DATASET = ROOT / "Dataset"
ANGLES = ("front", "back", "side")
RECAPTURES_PER_ITEM = 3
MAX_SIDE = 1024

ITEMS = {
    "speaker_A": {"category": "speaker", "model": "portronics_sounddrum_1", "note": "clean blue unit with bottom label"},
    "speaker_B": {"category": "speaker", "model": "portronics_sounddrum_1", "note": "worn black unit, rusted grille, label missing"},
    "shaker_A": {"category": "shaker", "model": "shaker_green_lid", "note": "green flip lid with carry loop"},
    "shaker_B": {"category": "shaker", "model": "shaker_black_lid", "note": "black lid, storage compartment"},
    "bottle_A": {"category": "bottle", "model": "steel_bottle_1l", "note": "plain steel bottle"},
    "bottle_B": {"category": "bottle", "model": "steel_bottle_1l", "note": "same model with strap and small dents"},
    # added later: appended so the IDs and seeds of the pairs above stay the same
    "headphones_blue": {"category": "headphones", "model": "sony_overear_anc", "note": "blue unit, two photo sets"},
    "headphones_black": {"category": "headphones", "model": "sony_overear_anc", "note": "black, worn unit of the same design"},
}
ORIGINAL_ITEMS = list(ITEMS)[:6]


def photo_path(item, angle, photo_set=1):
    if photo_set == 2:
        p = DATASET / "items" / item / "set2" / f"{angle}.jpeg"
        if p.exists():
            return p
    return DATASET / "items" / item / f"{angle}.jpeg"


def has_second_set(item):
    return (DATASET / "items" / item / "set2").is_dir()


def load(item, angle, photo_set=1):
    img = ImageOps.exif_transpose(Image.open(photo_path(item, angle, photo_set))).convert("RGB")
    img.thumbnail((MAX_SIDE, MAX_SIDE), Image.LANCZOS)
    return img


def simulate_recapture(img, rng):
    """Return a plausible second photo of the same physical scene."""
    w, h = img.size
    edges = np.array(img)[[0, -1]].reshape(-1, 3)  # top and bottom rows ~ floor/background colour
    fill = tuple(int(c) for c in np.median(edges, axis=0))

    img = img.rotate(rng.uniform(-12, 12), resample=Image.BICUBIC, fillcolor=fill)

    scale = rng.uniform(0.88, 1.08)
    dx, dy = rng.uniform(-0.05, 0.05) * w, rng.uniform(-0.05, 0.05) * h
    cw, ch = w / scale, h / scale
    left, top = (w - cw) / 2 + dx, (h - ch) / 2 + dy
    canvas = Image.new("RGB", (int(cw), int(ch)), fill)
    canvas.paste(img, (int(-left), int(-top)))
    img = canvas.resize((w, h), Image.LANCZOS)

    img = ImageEnhance.Brightness(img).enhance(rng.uniform(0.8, 1.2))
    img = ImageEnhance.Contrast(img).enhance(rng.uniform(0.85, 1.15))
    img = ImageEnhance.Color(img).enhance(rng.uniform(0.85, 1.15))
    r, g, b = img.split()
    r = r.point(lambda v, k=rng.uniform(0.94, 1.06): min(255, int(v * k)))
    b = b.point(lambda v, k=rng.uniform(0.94, 1.06): min(255, int(v * k)))
    img = Image.merge("RGB", (r, g, b))

    if rng.random() < 0.5:
        img = img.filter(ImageFilter.GaussianBlur(rng.uniform(0.3, 1.0)))

    buf = io.BytesIO()
    img.save(buf, "JPEG", quality=rng.randint(70, 90))
    return Image.open(io.BytesIO(buf.getvalue())).convert("RGB")


def pair_type(delivered, returned, real=False):
    if delivered == returned:
        return "same_item_real_recapture" if real else "same_item_recapture"
    a, b = ITEMS[delivered], ITEMS[returned]
    if a["model"] == b["model"]:
        return "same_model_other_unit"
    if a["category"] == b["category"]:
        return "same_category_other_model"
    return "different_category"


def write_pair(label, pair_id, delivered, returned, seed=None, delivery_set=1, return_set=1):
    """seed=None means real photos on both sides: no simulated capture noise."""
    rng = random.Random(seed) if seed is not None else None
    for angle in ANGLES:
        for phase, item, photo_set in (("delivery_images", delivered, delivery_set), ("return_images", returned, return_set)):
            out = DATASET / label / phase / angle
            out.mkdir(parents=True, exist_ok=True)
            img = load(item, angle, photo_set)
            if phase == "return_images" and rng is not None:
                img = simulate_recapture(img, rng)
            img.save(out / f"{pair_id}.jpeg", "JPEG", quality=92)


def main():
    for label in ("same", "different"):
        shutil.rmtree(DATASET / label, ignore_errors=True)

    rows = []
    pair_id = 0
    for item in ITEMS:                                   # simulated re-captures of set 1
        for _ in range(RECAPTURES_PER_ITEM):
            pair_id += 1
            write_pair("same", pair_id, item, item, seed=1000 + pair_id)
            rows.append(["same", pair_id, item, item, pair_type(item, item), 1, 1])
    for item in ITEMS:                                   # real re-captures across photo sets
        if not has_second_set(item):
            continue
        for d_set, r_set in ((1, 2), (2, 1)):
            pair_id += 1
            write_pair("same", pair_id, item, item, seed=None, delivery_set=d_set, return_set=r_set)
            rows.append(["same", pair_id, item, item, pair_type(item, item, real=True), d_set, r_set])

    original = list(permutations(ORIGINAL_ITEMS, 2))
    added = [p for p in permutations(ITEMS, 2) if p not in original]
    for pair_id, (d, r) in enumerate(original + added, start=1):
        write_pair("different", pair_id, d, r, seed=2000 + pair_id)
        rows.append(["different", pair_id, d, r, pair_type(d, r), 1, 1])

    with open(DATASET / "pairs.csv", "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["label", "pair_id", "delivery_item", "return_item", "pair_type", "delivery_set", "return_set"])
        writer.writerows(rows)

    counts = {}
    for row in rows:
        counts[row[4]] = counts.get(row[4], 0) + 1
    print(f"{len(rows)} pairs -> {DATASET}")
    for k, v in counts.items():
        print(f"  {k:28s} {v}")


if __name__ == "__main__":
    sys.exit(main())