İçeriğe geç
academia.sh

Ders 13 / 15

Görsel Doğrulama

Ekran görüntüsünün taban görüntüyle piksel karşılaştırması; eşiğin taranarak yanlış alarm ve kaçırma oranlarının ölçülmesi, değişken bölgenin karşılaştırma dışına alınmasının etkisi ve görsel doğrulamanın göremediği kusur sınıfı.

İçindekiler

Buraya kadar bütün iddialar bir değer üzerineydi: bir durum kodu, bir metin, bir tarih. Arayüzün asıl çıktısı ise bir görüntüdür ve bir düğmenin gölgesinin kaybolması, kontrastın düşmesi ya da kartın bir piksel kayması hiçbir değer iddiasında görünmez. Görsel gerileme testi bu boşluğu doldurmak ister: ekran görüntüsü, kabul edilmiş bir taban görüntüyle (baseline image) karşılaştırılır.

Soru bir eşitlik sorusu gibi durur ve değildir. İki ekran görüntüsü hiçbir zaman aynı piksellerden oluşmaz; yazı tipi düzleştirme, alt piksel yerleşimi ve ekranda değişen bölgeler her koşumda fark üretir. Bu yüzden karşılaştırma bir eşiğe bağlanır ve eşik seçimi doğrudan iki oranı belirler: yanlış alarm (gerileme yokken kırmızıya dönme) ve kaçırma (gerileme varken yeşil kalma).

Bu ders ekran görüntüsü almaz. Karşılaştırma gerçek bir piksel dizisi üzerinde yapılır; ekran, ödünç kartının küçük bir modelidir.

Ekranın Piksel Modeli

Kırk sütun, yirmi satır, tek kanal ton. Kart üç şey çizer: başlık şeridi, kitabın durum metni ve ödünç düğmesi. Sağ üstte de her koşumda değişen bir bölge vardır — kalan süre göstergesi.

// goruntu.mjs — kart ekraninin kucuk piksel modeli ve karsilastirma
export const EN = 40;
export const BOY = 20;

const dikdortgen = (p, x0, y0, x1, y1, ton) => {
  for (let y = y0; y <= y1; y += 1) for (let x = x0; x <= x1; x += 1) p[y * EN + x] = ton;
};

export function ciz(d) {
  const p = new Uint8Array(EN * BOY).fill(240);
  dikdortgen(p, 2, 2, 30 + (d.baslikKaymasi ?? 0), 4, 90);
  dikdortgen(p, 2, 7, 1 + (d.durumEni ?? 12), 8, 120);
  if (d.dugmeGorunur !== false) {
    const k = d.kaydirma ?? 0;
    dikdortgen(p, 2 + k, 12, 16 + k, 16, d.dugmeTonu ?? 30);
  }
  if (d.koseYuvarlama) for (const [x, y] of [[2, 12], [16, 12], [2, 16], [16, 16]]) p[y * EN + x] = 240;
  dikdortgen(p, 32, 0, 39, 1, d.saatTonu ?? 40);
  return p;
}

export const dislananBolge = () => {
  const m = new Uint8Array(EN * BOY);
  for (let y = 0; y <= 1; y += 1) for (let x = 32; x < EN; x += 1) m[y * EN + x] = 1;
  return m;
};

export const karsilastir = (a, b, pikselEsigi = 12, dislanan = null) => {
  let farkli = 0;
  let sayilan = 0;
  for (let i = 0; i < a.length; i += 1) {
    if (dislanan && dislanan[i] === 1) continue;
    sayilan += 1;
    if (Math.abs(a[i] - b[i]) > pikselEsigi) farkli += 1;
  }
  return { farkli, oran: (farkli / sayilan) * 100 };
};

Karşılaştırmanın iki eşiği vardır ve karıştırılmamalıdır. Piksel eşiği tek bir pikselin ne kadar ton farkının önemsiz sayılacağını söyler; burada on iki. Oran eşiği ise kaç pikselin farklı çıkmasının gerileme sayılacağını söyler ve asıl kararı o verir. Aşağıdaki tarama piksel eşiğini sabit tutup oran eşiğini tarar.

Eşik Taraması

Tarama iki küme üretir. Birinci kümede gerileme yoktur: aynı kabul edilmiş durum, değişen süre göstergesiyle ve onda bir olasılıkla bir piksel kayan başlıkla yeniden çizilir. İkinci kümede beş gerçek gerileme vardır. Her eşik için birinci kümenin kaçında alarm verildiği ve ikinci kümenin kaçının kaçırıldığı sayılır. Sağ üstteki değişken bölge bir kez içeride, bir kez karşılaştırma dışında bırakılarak iki ölçüm alınır.

// esik.mjs — esik taramasi: yanlis alarm ve kacirma oranlari, tohum 31
import { ciz, karsilastir, dislananBolge } from './goruntu.mjs';

function uretec(tohum) {
  let durum = (tohum * 2654435761) % 2147483647;
  return () => {
    durum = (durum * 48271) % 2147483647;
    return durum / 2147483647;
  };
}

const KABUL = { dugmeGorunur: true, dugmeTonu: 30, durumEni: 12, saatTonu: 40 };
const taban = ciz(KABUL);

const gerilemeler = {
  'dugme yok': { dugmeGorunur: false },
  'dusuk kontrast': { dugmeTonu: 200 },
  'dugme kaydi': { kaydirma: 1 },
  'durum kisaldi': { durumEni: 8 },
  'kose yuvarlandi': { koseYuvarlama: true },
};

const esikler = [0, 0.25, 0.5, 1, 2, 5, 10];
const KOSUM = 200;

const denk = (rastgele) => ciz({
  ...KABUL,
  saatTonu: 60 + Math.floor(rastgele() * 8) * 20,
  baslikKaymasi: rastgele() < 0.1 ? 1 : 0,
});

const olc = (dislanan) => {
  const yanlisAlarm = esikler.map(() => 0);
  const kacirma = esikler.map(() => 0);
  const rastgele = uretec(31);
  for (let i = 0; i < KOSUM; i += 1) {
    const oran = karsilastir(taban, denk(rastgele), 12, dislanan).oran;
    esikler.forEach((e, j) => { if (oran > e) yanlisAlarm[j] += 1; });
  }
  const gerilemeAdlari = Object.keys(gerilemeler);
  for (const ad of gerilemeAdlari) {
    for (let i = 0; i < KOSUM / gerilemeAdlari.length; i += 1) {
      const bozuk = ciz({
        ...KABUL, ...gerilemeler[ad], saatTonu: 60 + Math.floor(rastgele() * 8) * 20,
      });
      const oran = karsilastir(taban, bozuk, 12, dislanan).oran;
      esikler.forEach((e, j) => { if (oran <= e) kacirma[j] += 1; });
    }
  }
  return { yanlisAlarm, kacirma };
};

const dislamasiz = olc(null);
const dislamali = olc(dislananBolge());
const s = (n, g) => String(n).padStart(g);
const yuzde = (n) => `%${((n / KOSUM) * 100).toFixed(1)}`;
console.log(`${'oran esigi'.padEnd(11)}${s('yanlis alarm', 14)}${s('kacirma', 9)}`
  + `${s('dislamali alarm', 17)}${s('dislamali kacirma', 19)}`);
esikler.forEach((e, j) => {
  console.log(`${`%${e.toFixed(2)}`.padEnd(11)}${s(yuzde(dislamasiz.yanlisAlarm[j]), 14)}`
    + `${s(yuzde(dislamasiz.kacirma[j]), 9)}${s(yuzde(dislamali.yanlisAlarm[j]), 17)}`
    + `${s(yuzde(dislamali.kacirma[j]), 19)}`);
});
for (const [ad, fark] of Object.entries(gerilemeler)) {
  const oran = karsilastir(taban, ciz({ ...KABUL, ...fark }), 12, dislananBolge()).oran;
  console.log(`${ad.padEnd(16)} dislamali fark orani %${oran.toFixed(2)}`);
}
oran esigi   yanlis alarm  kacirma  dislamali alarm  dislamali kacirma
%0.00              %100.0     %0.0             %5.5               %0.0
%0.25              %100.0     %0.0             %5.5               %0.0
%0.50              %100.0     %0.0             %0.0               %0.0
%1.00              %100.0     %0.0             %0.0              %20.0
%2.00                %5.5     %0.0             %0.0              %60.0
%5.00                %0.0    %60.0             %0.0              %60.0
%10.00               %0.0    %60.0             %0.0             %100.0
dugme yok        dislamali fark orani %9.57
dusuk kontrast   dislamali fark orani %9.57
dugme kaydi      dislamali fark orani %1.28
durum kisaldi    dislamali fark orani %1.02
kose yuvarlandi  dislamali fark orani %0.51

İlk iki sütun, değişken bölge içerideyken hiçbir eşiğin işe yaramadığını gösteriyor. Süre göstergesi tek başına on altı piksel, yani ekranın yüzde ikisidir; bu yüzden yüzde ikinin altındaki her eşik her koşumda alarm verir, üstündeki her eşik ise küçük gerilemelerin hepsini yutar. Yüzde iki eşiği rastlantısal olarak iyi görünür — kaçırma sıfır, alarm yüzde beş virgül beş — ama bu, eşiğin gürültünün büyüklüğüne oturtulmasıdır: gösterge bir hane büyüdüğünde eşik yeniden ayarlanmak zorunda kalır.

Değişken bölge karşılaştırma dışına alındığında tablo okunur hâle geliyor. Yüzde yarım eşiğinde yanlış alarm sıfıra iniyor ve beş gerilemenin beşi de yakalanıyor. Eşik yükseldikçe kaçırma sırayla açılıyor: yüzde birde köşe yuvarlaması, yüzde ikide durum metni ve düğme kayması, yüzde onda düğmenin tümden kaybolması bile. Son satırlar bu sıranın nedenini veriyor — her gerilemenin ürettiği fark oranı sabittir ve eşik onu geçtiği anda kusur görünmez olur.

Buradan çıkan kural şudur: eşik, kaçırılabilecek en küçük gerçek gerilemenin altında tutulur; gürültü o eşiği aşıyorsa eşik yükseltilmez, gürültünün kaynağı karşılaştırmadan çıkarılır. Değişken bölgeleri dışlamak eşiği düşük tutmanın bedelidir ve dışlanan her bölge, orada olabilecek gerilemenin de görülmemesi demektir.

Yakaladığı ve Kaçırdığı Kusur

Görsel doğrulamanın yakaladığı sınıf, hiçbir değer iddiasında görünmeyen kusurdur. Biçem katmanında düğme tonunun açılması böyledir: ağaç aynıdır, seçici çalışır, akış tamamlanır, düğme okunamayacak kadar soluktur.

// bicem.mjs — surum 1: dugme tonu acildi
export const bicem = { dugmeTonu: 200 };
// gorsel.test.mjs — kart ekrani taban goruntuyle karsilastirilir
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { ciz, karsilastir, dislananBolge } from './goruntu.mjs';
import { bicem } from './bicem.mjs';

const KABUL = { dugmeGorunur: true, dugmeTonu: 30, durumEni: 12, saatTonu: 40 };
const taban = ciz(KABUL);
const dislanan = dislananBolge();
const ESIK = 0.5;

test('kart ekrani taban goruntuye uyar', () => {
  const simdi = ciz({ ...KABUL, dugmeTonu: bicem.dugmeTonu, saatTonu: 120 });

  const { farkli, oran } = karsilastir(taban, simdi, 12, dislanan);

  assert.ok(oran <= ESIK, `${farkli} piksel farkli, oran %${oran.toFixed(2)}`);
});

test('etkisiz dugme goruntude fark uretmez', () => {
  const etkin = ciz({ ...KABUL, dugmeTonu: bicem.dugmeTonu, etkin: true, saatTonu: 120 });
  const etkisiz = ciz({ ...KABUL, dugmeTonu: bicem.dugmeTonu, etkin: false, saatTonu: 200 });

  assert.equal(karsilastir(etkin, etkisiz, 12, dislanan).farkli, 0);
});
node --test --test-reporter=tap gorsel.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
not ok 1 - kart ekrani taban goruntuye uyar
ok 2 - etkisiz dugme goruntude fark uretmez
# tests 2
# pass 1
# fail 1

İkinci test kaçan sınıfı kurar ve iki koşumda da yeşildir: düğmenin etkin ya da etkisiz olması aynı pikselleri üretir, çünkü modelde etkisizliğin görsel bir karşılığı yoktur. Görsel doğrulama görünenin doğruluğunu sorar; dokunulduğunda ne olduğunu sormaz. Ödünç düğmesi tıpatıp doğru görünüp hiçbir istek göndermiyorsa bu test yeşil kalır, ve o kusur ancak önceki derslerin akış testleriyle yakalanır.

Düzeltme, tonun taban görüntüdeki değere döndürülmesidir.

// bicem.mjs — surum 2: dugme tonu taban goruntudeki degere donduruldu
export const bicem = { dugmeTonu: 30 };
node --test --test-reporter=tap gorsel.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
ok 1 - kart ekrani taban goruntuye uyar
ok 2 - etkisiz dugme goruntude fark uretmez
# tests 2
# pass 2
# fail 0

Bedelin koşuma bağlı yanı bu koşumda küçüktür: iki test 29,1 milisaniye sürdü, eşik taraması sekiz yüz karşılaştırma yaptı ve her karşılaştırmada yedi yüz seksen dört piksel okundu. Koşumdan bağımsız bedel ise taban görüntülerin sayısıdır. Ödünç akışının dört ekranı, önceki dersin kapsamaya göre seçilmiş altı hücresinde tutulursa yirmi dört taban görüntü saklanır. Düğmenin tonunu bilerek değiştiren bir tasarım kararı, bu yirmi dördünün tamamını yeniden onaylamayı gerektirir — görsel doğrulamanın asıl bedeli koşum değil, bu onay işidir.

Özet

  • Görsel doğrulama, ekran görüntüsünü kabul edilmiş taban görüntüyle karşılaştırır ve karar iki eşiğe bağlanır: piksel ton eşiği ve farklı piksel oranı eşiği.
  • Değişken bölge karşılaştırmanın içindeyken yüzde ikinin altındaki her eşik yüzde yüz yanlış alarm verdi, üstündeki eşikler küçük gerilemeleri kaçırdı.
  • Değişken bölge dışlandığında yüzde yarım eşiği sıfır yanlış alarmla beş gerilemenin beşini de yakaladı.
  • Eşik yükseldikçe kaçırma, gerilemenin ürettiği fark oranı sırasına göre açılır: yüzde birde köşe yuvarlaması, yüzde ikide bir piksellik kayma, yüzde onda düğmenin kaybolması.
  • Görsel doğrulama görünenin doğruluğunu sorar; etkisiz bir düğme aynı pikselleri ürettiği için bu testten yeşil geçer.

Sonraki Adım

Son üç ders aynı gerçeği ayrı yüzlerinden gördü: bekleme politikası düşme oranını değiştiriyordu, aygıt seçimi kusur kapsamasını, eşik seçimi yanlış alarmı. Üçü de tek bir sorunun parçalarıdır — uçtan uca testler, alt seviyelerdeki testlerin taşımadığı bir kararsızlık taşır. Sonraki ders bu kararsızlığı toplu olarak ele alır: bir uçtan uca takımında düşüşlerin nedenleri hangi oranda dağılıyor, karantina sinyali temizlerken kaçan hatayı ne kadar artırıyor.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat