- 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.0ile 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 / 1024ile 255 yöntemindeki1 / 1020değ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
Lobste.rs görüşleri
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
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ıfloorya daceilise 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/3ve×3mantığı, 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 gelirTamsayı 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 budurBenim 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
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
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
pngcrushile sıkıştırmıştım. Yoksa görsel içeriğinde bir sorun mu olduğunu söylüyorsunuz?