1 puan yazan GN⁺ 7 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • RGB normalizasyonunda, bilinmeyen bir görüntü dosyasını işleyip yeniden 8 bit olarak kaydetmenin genel durumunda 255’e bölme şeklindeki standart yöntem uygundur
  • 255 yöntemi, 0’ı 0.0’a ve 255’i 1.0’a eşleyerek siyah ve beyazı doğrudan işlemeyi kolaylaştırır; ayrıca GPU’nun UNORM-to-float dönüşüm biçimiyle de uyumludur
  • 256 yöntemi, (img + 0.5) / 256.0 ile her değeri aralığın merkezine yerleştirerek dithering gibi işlemlerde sınır işlemeyi basitleştirebilir; ancak 0, 0.0 olmadığı için işleme mantığı 8 bit girdiye bağlı kalır
  • 255 yönteminde uçlardaki aralıklar yarım genişlikte olduğundan, tekdüze [0, 1] rastgele sayıları yeniden 8 bite yuvarladığınızda 0 ve 255 diğer değerlerin yarı sıklığında görünür; buna rağmen gerçek görüntü gidiş-dönüş dönüşümü kayıpsız çalışır
  • 256 yöntemi teoride ortalama mutlak hatayı 1 / 1024 ile 255 yöntemindeki 1 / 1020 değerinden küçük tutar; ancak zaten 255 yöntemiyle kuantalanmış bir görüntüyü yanlış ölçekle okumak hatayı tersine artırır

Problem tanımı

Görüntü işleme programı 8 bit görüntüyü kayan noktalıya dönüştürür, işlemleri yapar ve ardından tekrar 8 bit renk olarak kaydeder

İki dönüşüm yöntemi şöyledir

# Standart: 255'e bölme
pixels = img / 255.0
result = process(pixels)
output = np.trunc(result * 255 + 0.5)


# Alternatif: 0.5 ekleyip 256'ya bölme
pixels = (img + 0.5) / 256.0
result = process(pixels)
output = np.trunc(result * 256)

Her iki yöntemde de son dönüşümden önce değerler 0~255 aralığına sınırlandırılır

output_8bit = output.clip(0, 255).astype(np.uint8)

Standart yöntem, tamsayı 0’ı 0.0’a ve 255’i 1.0’a eşler; GPU’nun UNORM-to-float dönüşümü ile aynıdır

Alternatif yöntem ise 0’ı 0.5 / 256 = 0.001953125 değerine eşlediği için, siyah pikseli tespit etmek istiyorsanız bu sabiti bilmeniz gerekir

255’e bölmenin standart yöntem olarak özellikleri

Standart yöntemde [0, 1] aralığı içinde uç değerlerin aralıkları diğerlerine göre fiilen yarım genişliktedir

Tekdüze [0, 1] rastgele sayılar üretip trunc(result * 255 + 0.5) ile yuvarlarsanız, 0 ve 255 diğer tamsayılara göre yarı sıklıkta ortaya çıkar

Buna rağmen özgün 8 bit görüntü, uint8 → float → uint8 gidiş-dönüş dönüşümünde kayıpsız biçimde geri döner

Ayrıca işleme sonucu 0.0 ya da 1.0 sınırlarını az da olsa aşsa bile, clamp ve yuvarlama sayesinde doğru tamsayı aralığına girebilir

Örneğin kayan noktalı renkten 0.005 çıkarılırsa, standart yöntemde siyah negatif olur; ancak nihai sonuç yine de tamsayı 0 olur

trunc(255 * (-0.005) + 0.5) = 0

Kayan nokta doğruluğu ve aralık merkezine yerleştirme

255 yöntemindeki bazı değerler tam olarak temsil edilemez

Örneğin 128 / 255.0 ≈ 0.501961 iken 128 / 256.0 = 0.5 olur

Bu fark, 32 bit kayan noktanın 23 bit mantissasında en düşük anlamlı bit düzeyindeki yuvarlama hatasıdır ve büyüklüğü 2^-23ten küçüktür

Bu nedenle bu tür temsil hatası pratikte teknik bir sorundan çok estetik bir meseleye yakındır

256 yöntemi, her kayan noktalı değeri iki tamsayı arasındaki tam merkeze yerleştirir

Bu özellik, özgün kuantalanmış değerin tam olarak ne olduğunun bilinmediği durumda iki ardışık tamsayı arasındaki ortalamayı kullanan bir uzlaşma olarak görülebilir

Andrew Kesler’ın 2015 tarihli “Converting Color Depth” yazısı, bu yöntemin dithering sırasında gürültü eklerken sınır işlemeyi daha az dert haline getirdiğini savunur

Buna karşılık standart yöntemde uçlardaki aralıklar, gürültü dağılımını tutarlı korumak için dikkatli işlemeyi gerektirir

Kuantalama perspektifi

İki yöntem de tekdüze skaler kuantalayıcılar (uniform scalar quantizer) olarak görülebilir

Wikipedia’daki quantization açıklaması) signed input data için tekdüze kuantalayıcıları çoğunlukla mid-riser ve mid-tread olarak ayırır

mid-tread, 0 değerinde bir yeniden yapılandırma seviyesi taşırken; mid-riser, 0 değerinde bir sınıflandırma eşiğine sahiptir

Formüller şu şekilde karşılık gelir

Yöntem Kodlama Kod çözme
mid-tread k = trunc(x L + 0.5) y_k = k / L
mid-riser k = trunc(x L) y_k = (k + 0.5) / L

Standart yöntem L=255 kullanan bir mid-tread biçimidir; alternatif yöntem ise L=256 kullanan bir mid-riser biçimidir

Standart yöntem, 0.0 ve 1.0 uçlarını tam hizalayarak programlama kolaylığı sağlar; bunun karşılığında 8 bit girdi için en uygun aralık yerleşimiyle tam örtüşmez

Yeniden yapılandırma hatası ve gerçek görüntü işleme

x ∈ [0, 1] aralığında tekdüze dağılmış bir gerçeği 8 bit tamsayıya kodlayıp sonra yeniden gerçek değere dönüştüren bir sistemi baştan siz tasarlıyor olsaydınız, 256 yöntemi teorik olarak daha hassas olurdu

Standart yöntemin temsil edilebilir aralığı [-0.5 / 255, 255.5 / 255] olur; bu da [0, 1] için gerçekten gerekli olana kıyasla aralık boşluklarını genişletir

StackOverflow kullanıcısı Peter Mudrievskij’nin hesabına göre ortalama mutlak hata 255’e bölmede 1 / 1020, 256’ya bölmede ise 1 / 1024 olur

Ancak zaten kaydedilmiş 8 bit RGB görüntüleri okuyup işlediğiniz durumda, kayıt sırasında kaybolan bilgi geri gelmez

Görüntü 255 ile çarpıp yuvarlama yöntemiyle kuantalandıysa, yüklerken 256’ya bölmek hassasiyeti geri kazandırmaz

Başkalarının ürettiği görüntülerin çoğu büyük olasılıkla standart yöntemle kuantalandığı için, bunları alternatif formülle okumak teorik olarak yanlış bir ölçek katsayısı kullanmak anlamına gelir

Pratikte ise renkler mutlak ölçüm değeri gibi davranmadığından, bu durum biraz daha dar bir aralıkta ve küçük bir ofsetle işlem yapmak demektir

İki kuantalayıcının kodlama ve kod çözme aşamalarını karıştırmak bozuk koda yol açar

Sonuç

Tanımadığınız biri tarafından sağlanan görüntüleri işliyorsanız, RGB değerlerini 255’e göre normalize etmelisiniz

Kayan noktalı değerlerin tam temsil edilememesi ya da soyut yeniden yapılandırma hatasının daha büyük görünmesi, 256 yöntemini seçmek için güçlü gerekçeler değildir

Görüntünün kaydedilmesini ve yüklenmesini tamamen siz kontrol ediyorsanız, 0’ın 0’a eşlenmesi gerekmiyorsa ve işleme kodunun 8 bit dinamik aralığına bağlı kalması sorun değilse, 256’ya bölerek teoride biraz daha yüksek hassasiyet hedefleyebilirsiniz

1 yorum

 
GN⁺ 7 시간 전
Lobste.rs görüşleri
  • Dağınık görünüyor ama doğru; değer 255 olmalı
    Sezgisel gelmiyorsa bunu 2 bitlik dejenere bir örnek üzerinden düşünebilirsiniz. Olası tamsayı değerleri yalnızca 0, 1, 2, 3 olduğunda tamsayı→kayan nokta dönüşümünü tamamen hesaplarsanız, siyah/beyazın gerçekten siyah/beyaz olmaması ya da aralıkların bariz biçimde eşit dağılmaması gibi tuhaf davranışlardan kaçınmak için sonuç 0.0, 0.33..., 0.66..., 1.0 olur
    Dolayısıyla ters dönüşüm de 4(2^2) ile değil, 3 ile çarpma şeklinde olur
    • İlk kısmı doğru, ama buradan “o hâlde ters dönüşümde 4 değil 3 ile çarpmalısın” sonucu çıkmaz
      Ters dönüşümde kuantalama (yuvarlama) gerekir ve simetriyi bozan kritik nokta tam da budur
      0..=1 aralığında eşit bir gerçek sayı gradyanı oluşturup bunu 0, 1, 2, 3'e kuantalarsanız, 3 ile çarpmanın eşit sonuç vermediğini görürsünüz. ×3 sonrası round() 1 ve 2'yi aşırı temsil eder; ×3 sonrası floor ya da ceil ise 0 veya 3'ü tekillik gibi içeri katlayarak gradyanın 4 renkten yalnızca 3'ünü kullanıyormuş gibi görünmesine yol açar
      /3 ve ×3 mantığı, tam sayıları gidiş-dönüş çevirmek için kulağa doğru geliyor olabilir; ama ara değerler yuvarlama seçimine büyük ölçüde bağlıdır ve veri işlemeye başladığınız anda bu önemli hâle gelir
      Tamsayı oranlarının eşit olması ancak (4-ε) ile çarpıp aşağı yuvarladığınızda gerçekleşir; bu da ×4, floor(), clamp() ile aynıdır. Garip bir 1 farkı ya da ε farkı hatası gibi hissettirse de sezgisel olarak en iyi görünen çözüm budur
  • Başlık yüzünden çok kafam karıştı. Kasıtlı mı bilmiyorum ama sonuçta soru “0..1, [0..255.0] aralığına mı karşılık geliyor, yoksa [0.5..255.5] aralığına mı?” gibi görünüyor
    Benim için cevap her zaman “elbette” [0.0..255.0] olmuştu, ama görünüşe göre bu herkes için o kadar da bariz değil
    Yazıda “uç” aralıkların diğer aralıkların yalnızca yarısı kadar kapasiteye sahip olduğu söyleniyor; bence bu çerçeveleme de doğru değil
    Eğer [0..1] dışında değer yoksa, bunun daha dar bir aralık gibi görünmesi render etmenin bir yan ürünü. Kovaları, aralığın dışında değer olmadığını bilerek kestiğiniz için sadece daha dar render ediliyor
    Tersine, eğer [0..1] dışında değerler varsa bu aralık sonsuzdur. Yazı ikincisini kabul ediyor ama birincisini etmiyor
    Birincisini kabul ettiğiniz anda doğru davranış oldukça açık görünüyor; ama böyle bir yazının ortaya çıkmış olması bile bunun nesnel olarak o kadar “açık” bir mesele olmadığını gösteriyor :D
    • Eğer gerçekten 0…255.0'ın apaçık doğru olduğunu düşünüyorsanız, hangi kayan nokta değer aralığı tamsayı 0'a dönmeli ve hangi aralık tamsayı 255'e dönmeli?
      0..<1 tamsayı 0'a gidiyor, 254>..255.0 da tamsayı 255'e gidiyor derseniz 128 arada kaybolur. Muhtemelen 127.5..128.5 aralığının 128'e gitmesini istersiniz; peki o zaman bu yarımlar nereye gitmeli?
      128'i doğru yere koymak için tüm aralığı biraz kaydırırsanız, 0..0.99609375 tamsayı 0'a eşlenmiş olur
  • Standart yaklaşım da sanki insanların doğal olarak round() çağırmasından doğmuş gibi görünüyor
    İnsanlara bu yöntem oldukça doğal geldiği için, sadeliği yüzünden standart hâline gelmiş gibi
  • 256 ile elde edilmeye çalışılan şeyin ters yaklaşımının da işe yarayıp yaramayacağını merak ediyorum. Yani 0.0'ı 0'a, 1.0'ı 255'e gönderip diğer kayan nokta değerlerini 1 ile 254 arasına eşlemek
    uint8_t output = 0.0f >= result  
                     ? 0  
                     : 1.0f <= result  
                     ? 255  
                     : 1 + 253*result;  
    
    İşleme sırasında da siyahın siyah, beyazın beyaz olarak kalması güzel olurdu
    • Bunu yaparsanız 0 ve 255, birim aralıkta diğer sayılardan daha büyük bir pay alır. Yaklaşık %0,8, yani 255/253 kadar
  • İlk görsel bende bozuk görünüyor
    • Yazının yazarı burada. Görsel dosyasının bozuk olduğunu mu kastediyorsunuz? pngcrush ile sıkıştırmıştım. Yoksa görsel içeriğinde bir sorun mu olduğunu söylüyorsunuz?