Przejdź do treści

Przewodniki

Samouczek: obrazy, ze sprawdzeniami i sędzią, który je widzi

Oceń funkcję, która tworzy obrazy, tutaj generator miniatur produktów, który pisze też podpis. Sprawdź miniatury pomiarami, które nie wymagają modelu, oceniaj podpisy modelem, który widzi każdą miniaturę, zmierz tego sędziego względem człowieka, a potem wdróż prawdziwą zmianę. Każdy krok działa na tej maszynie bez modelu i bez sieci, a opcjonalny krok podstawia prawdziwy model wizyjny.

Co zbudujesz

Usługa katalogowa przyjmuje zdjęcie produktu, zapisuje miniaturę i pisze jednowierszowy podpis podający kolor produktu. O tym, czy wersja może zostać wydana, decydują trzy pytania:

  • Czy każda miniatura pasuje do katalogu: format webowy, najwyżej 256 na 256 pikseli, najwyżej 50 kB? To pomiar, wykonywany przez media_check na bajtach miniatury.
  • Czy każda miniatura jest nadal zdjęciem produktu, a nie pustym obrazem, wycinkiem lub innym produktem? image_matches_reference porównuje ją ze zdjęciem za pomocą hasza percepcyjnego.
  • Czy każdy podpis podaje kolor, który widzi kupujący? To wymaga oczu, więc sędziemu z rubryką pokazuje się miniaturę.

Zdjęcia i miniatury zostają w folderze projektu. System zapisuje je przez odwołanie (media/v1): gdzie jest każdy plik, jego typ, rozmiar i sha256. Terminy takie jak przypadek, przebieg, sędzia i bramka są zdefiniowane w Pojęciach; wszystko o multimediach jest w Obrazy, dźwięk i wideo.

Wymagania wstępne

  • Python 3.11 lub nowszy oraz Oloproof z dodatkiem media, który dodaje Pillow do porównywania obrazów:
python3 -m venv .venv
. .venv/bin/activate
pip install 'oloproof[media]'
  • Przykładowy projekt, dostarczany z pakietem. Skopiuj go do nowego katalogu i pracuj tam:
oloproof init --example media product-thumbnails
cd product-thumbnails
  • Wolny port 8798 dla zastępczego sędziego (jeśli nie, zmień go w obu miejscach).

Każdy krok aż do "Opcjonalnie: prawdziwy model wizyjny" działa offline i deterministycznie: bez klucza API, bez konta u dostawcy, bez kosztów.

Pliki

product-thumbnails/
  app.py                              the thumbnailer under test (baseline)
  app_v2.py                           the candidate change
  judge_server.py                     a stand-in vision judge speaking the OpenAI API on 127.0.0.1
  rubrics/caption_names_colour.md     the judge's rubric
  oloproof.yaml                       the suite
  release.yaml                        rules for a run
  compare.yaml                        a rule for a comparison
  data/products.jsonl                 20 cases
  photos/                             the 20 product photos the cases name
  make_photos.py                      how the photos were drawn
  labels/reviewer_verdicts.csv        one person's verdicts on the baseline's captions
  fill_labels.py                      copies those verdicts into a labelling sheet

Uruchamiaj każde polecenie z product-thumbnails/.

Aplikacja zapisuje swoje obrazy

# app.py (excerpt)
@system(name="thumbnailer", version=VERSION, records=("media/v1",))
def describe(case: dict[str, Any]) -> dict[str, str]:
    photo_path = HERE / case["photo"]
    photo = Image.open(photo_path).convert("RGB")
    thumb = photo.copy()
    thumb.thumbnail((320, 320))
    out = HERE / "thumbs" / VERSION / photo_path.name
    out.parent.mkdir(parents=True, exist_ok=True)
    thumb.save(out)
    given = media_file(photo_path, role="input")
    made = media_file(out, width=thumb.width, height=thumb.height)
    current_case().media([given, made])
    centre = photo.getpixel((photo.width // 2, photo.height // 2))
    return {"caption": f"A {colour_name(centre)} product."}

media_file(path) opisuje plik: jego lokalizację file://, typ MIME, rozmiar i sha256, nazwany od nazwy pliku. role="input" oznacza zdjęcie, które system otrzymał; miniatura jest wynikiem, co jest wartością domyślną. current_case().media(...) zapisuje oba jako jeden artefakt media/v1, a records: [media/v1] w oloproof.yaml deklaruje, że system go zapisuje. Nic nie jest kopiowane: sprawdzenie lub sędzia czyta później każdy plik ponownie i odrzuca taki, którego bajty nie zgadzają się już z zapisem.

Ten punkt odniesienia ma dwie wady. Jego miniatury mają szerokość 320 pikseli, a jego podpis podaje kolor środkowego piksela zdjęcia, którym jest tło za każdym razem, gdy produkt stoi z boku. Każda wersja zapisuje swoje miniatury pod thumbs/<version>/, więc późniejszy przebieg nigdy nie nadpisuje plików zapisanych przez wcześniejszy przebieg.

Zbiór danych

{"id": "p03", "input": {"photo": "photos/p03.png"}, "expected": {"photo": "photos/p03.png", "colour": "yellow", "shape": "triangle"}}

input to to, co otrzymuje system. expected.photo to wzorzec, z którym porównuje się miniaturę; colour i shape są tam dla osoby czytającej przypadek, a żaden ewaluator ich nie czyta.

Wybór ewaluatorów

evaluators:
  # The catalogue's limits, measured from the thumbnail's bytes: a web format, at most 256 by
  # 256 pixels, at most 50 kB.
  - type: media_check
    criterion: thumbnail_fits
    kind: image
    formats: [png, jpeg, webp]
    max_width: 256
    max_height: 256
    max_bytes: 50000
  # The thumbnail is still the product's photo: a perceptual hash within 10 of 64 bits.
  - type: image_matches_reference
    criterion: same_photo
    reference_field: photo
    max_distance: 10
  # The caption, judged by a model that sees the thumbnail.
  - type: rubric_judge
    criterion: caption_names_colour
    provider: openai_compatible
    model: stand-in-vision
    base_url: http://127.0.0.1:8798/v1
    rubric_file: rubrics/caption_names_colour.md
    media: output

media_check mierzy każdy obraz wyjściowy: format z jego początkowych bajtów, rozmiar z jego nagłówka, nigdy na podstawie tego, co zadeklarował system. image_matches_reference haszuje miniaturę i zdjęcie wskazane przez expected.photo i zalicza, gdy hasze różnią się co najwyżej 10 z 64 bitów; przeskalowanie lub ponowna kompresja przesuwa hasz o kilka bitów, inny obraz przesuwa go o wiele. Żadne z nich nie wywołuje modelu, więc oba rozstrzygają od pierwszego przebiegu.

media: output dołącza obrazy wyjściowe przypadku do żądania sędziego, każdy nazwany w jego prompcie. Sędzia nie rozstrzyga niczego, dopóki etykiety nadane przez ludzi nie pokażą, że się z nimi zgadza, więc trzecia reguła na to czeka.

Zastępczy sędzia

Sędzia z rubryką wysyła swój prompt i dołączone obrazy do modelu i odczytuje {"pass": true|false, "rationale": "..."}. Tutaj modelem jest judge_server.py, który mówi tym samym API na 127.0.0.1 i nie jest modelem: dekoduje dołączoną miniaturę, znajduje jej najczęstszy kolor inny niż biel i szarość tła i przepuszcza podpis, który go podaje. Uruchom go w drugim terminalu i zostaw włączonego:

python judge_server.py --port 8798

Polityka

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
rules:
  - id: thumbnails-fit
    metric: thumbnail_fits
    kind: observed_count
    max_failures: 0
  - id: same-photo
    metric: same_photo
    kind: observed_count
    max_failures: 0
  - id: caption-floor
    metric: caption_names_colour
    min: 0.80

Każda miniatura musi pasować i musi być swoim zdjęciem. Co najmniej 80% podpisów musi podawać właściwy kolor, co jest rozstrzygane dopiero wtedy, gdy dolna granica przedziału przekroczy 80%.

Uruchom

oloproof run
Gate: BLOCK (exit 1)
│ thumbnails-fit │ thumbnail_fits       │ FAIL                  │ observed_failures_exceed_limit   │
│ same-photo     │ same_photo           │ PASS                  │ observed_failures_within_limit   │
│ caption-floor  │ caption_names_colour │ INSUFFICIENT_EVIDENCE │ evaluator_not_validated          │
│ thumbnail_fits       │ 0.0%     │ [0.0%, 16.9%]   │ 0 / 20 observed · 0 missing · 0 excluded  │
│ same_photo           │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ caption_names_colour │ 60.0%    │ [36.0%, 80.9%]  │ 12 / 20 observed · 0 missing · 0 excluded │

Żadna miniatura nie pasuje, każda miniatura jest swoim zdjęciem, a 12 z 20 podpisów podaje właściwy kolor. Reguła sędziego jest nierozstrzygnięta, ponieważ nikt jeszcze nie zmierzył sędziego.

Przejrzyj niepowodzenia

oloproof inspect RUN_ID --failures
p03
  output: {"caption": "A white product."}
  thumbnail_fits: failed
    measured: p03.png: width 320 px is above 256 px
  caption_names_colour: failed
    judge text, not verified: the product is yellow; the caption says 'a white product.'

Powodem sprawdzenia jest pomiar, oznaczony measured; powodem sędziego są jego własne słowa, których nikt nie sprawdził. Zdjęcia i miniatury pojawiają się też obok każdego werdyktu na stronie przypadku w workbenchu, po oloproof export RUN_ID, czytane z tego folderu; w hostowanym obszarze roboczym pobiera je Twoja przeglądarka z oloproof collect, a obszar roboczy nigdy ich nie przechowuje (Obrazy, dźwięk i wideo).

Zmierz sędziego względem człowieka

Wylosuj próbę przypadków przebiegu do arkusza, bez werdyktów sędziego, poproś osobę, by obejrzała każdą miniaturę i podpis, i zaimportuj to, co powiedziała. Tutaj fill_labels.py zastępuje tę osobę, kopiując jej werdykty z labels/reviewer_verdicts.csv:

oloproof labels export RUN_ID --criterion caption_names_colour --sample 20 --local --out sample.csv
python fill_labels.py sample.csv
oloproof labels import sample.csv

Następnie zmierz sędziego względem tych etykiet i zapisz, kto to zrobił:

oloproof evaluators list
oloproof evaluators validate EVALUATOR_ID --by alice
caption_names_colour: sha256:53605dd6... is now VALIDATED
  agreement 100.0% [83.1%, 100.0%] · 20 of 20 labelled cases agreed · 0 labelled but not judged · kappa 1.000

Sędzia, który widzi obrazy, jest mierzony dokładnie tak jak sędzia czytający tekst. W hostowanym obszarze roboczym kolejka przeglądu pokazuje osobie przeglądającej obrazy każdego przypadku, pobierane przez jej przeglądarkę z Twojego kolektora (Sędziowie).

oloproof gate RUN_ID --policy release.yaml
thumbnails-fit: FAIL (observed_failures_exceed_limit)
same-photo: PASS (observed_failures_within_limit)
caption-floor: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
Gate: BLOCK (exit 1)

Sędzia może teraz rozstrzygać, a 60% to za mało.

Wprowadź prawdziwą zmianę

app_v2.py tworzy miniatury o szerokości 256 pikseli i podaje najczęstszy kolor zdjęcia inny niż tło, gdziekolwiek jest produkt. Skopiuj go na miejsce app.py, ustaw version: main-colour pod system w oloproof.yaml, nie wyłączaj sędziego i:

oloproof run
Gate: ALLOW (exit 0)
│ thumbnails-fit │ thumbnail_fits       │ PASS  │ observed_failures_within_limit │
│ same-photo     │ same_photo           │ PASS  │ observed_failures_within_limit │
│ caption-floor  │ caption_names_colour │ PASS  │ lower_bound_meets_minimum      │

Sędzia to ta sama wersja ewaluatora, która została zwalidowana, więc jego werdykty się liczą.

Porównaj kandydata z punktem odniesienia

oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
thumbnail_fits: +100.0 points [+52.9, +100.0] · 20 paired · 0 missing · 0 excluded
caption_names_colour: +40.0 points [+1.8, +72.7] · 20 paired · 0 missing · 0 excluded
  captions-better  caption_names_colour  superiority  PASS  difference_above_zero

Opcjonalnie: prawdziwy model wizyjny

Ten krok opuszcza ścieżkę offline. Wymaga modelu, który czyta obrazy: lokalnie Ollama z modelem wizyjnym (ollama pull qwen2.5vl), bez klucza i bez kosztów; w chmurze provider: anthropic lub openai z api_key_env, rozliczane za każdy obraz, tak jak za każde słowo.

# live_judge.yaml
type: rubric_judge
criterion: caption_names_colour
provider: openai_compatible
model: qwen2.5vl
base_url: http://localhost:11434/v1
rubric_file: rubrics/caption_names_colour.md
media: output
timeout_s: 300
oloproof evaluators try live_judge.yaml

To ocenia prawdziwym modelem przypadki, które oetykietował Twój recenzent, i podaje jego zgodność, bez przyjmowania go. Aby go przyjąć, umieść go w oloproof.yaml zamiast zastępczego sędziego: nowy model to nowa wersja ewaluatora, więc zwaliduj go względem etykiet, jak wyżej.

Rozwiązywanie problemów

ObjawPrzyczyna i rozwiązanie
caption_names_colour w całości brakująceSerwer sędziego nie działa albo nie jest pod base_url; oloproof inspect RUN_ID --failures pokazuje dlaczego.
image_matches_reference zatrzymuje przebieg, wskazując PillowZainstaluj dodatek media: pip install 'oloproof[media]'.
Przypadek jest brakujący z media_unavailableZapisany plik został przeniesiony lub usunięty po przebiegu. Przechowuj pliki zapisane przez przebieg tak długo, jak przechowujesz przebieg.
Przebieg zatrzymuje się z is not the recorded contentZapisany plik został zmieniony po przebiegu. Sprawdzenie ani sędzia nigdy nie czytają innych bajtów jako zapisanych; zapisuj pliki każdej wersji w osobnym miejscu.
evaluator_not_validated po walidacjiZmieniłeś sędziego (model, punkt końcowy, port, rubrykę, media) i powstała nowa wersja. Zwaliduj tę.

Ograniczenia

  • Zastępczy sędzia liczy piksele. Pokazuje przebieg pracy, a nie jakość oceniania.
  • Hasz percepcyjny odczytuje układ i światło, a nie kolor: odróżnia miniaturę zdjęcia od pustego obrazu lub innej sceny, ale nie jeden produkt od innego z tego samego studia. Płaskie grafiki z dużymi pustymi obszarami poruszają go bardziej niż fotografie.
  • Dwadzieścia etykiet daje szeroki przedział zgodności. Dla sędziego, na którym polegasz, etykietuj więcej, losowo i na ślepo.
  • Dźwięk i wideo działają tak samo, z innymi ustawieniami: zob. Obrazy, dźwięk i wideo.