1 poin oleh GN⁺ 7 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Dalam normalisasi RGB, untuk situasi umum saat memproses file gambar yang asal-usulnya tidak diketahui lalu menyimpannya kembali sebagai 8-bit, pendekatan standar dengan membagi 255 adalah yang paling tepat
  • Metode 255 memetakan 0 ke 0.0 dan 255 ke 1.0 sehingga warna hitam dan putih mudah ditangani secara langsung, serta sesuai dengan cara GPU melakukan konversi UNORM-to-float
  • Metode 256 menempatkan setiap nilai di tengah interval dengan (img + 0.5) / 256.0, sehingga penanganan batas pada pekerjaan seperti dithering bisa lebih sederhana, tetapi 0 tidak menjadi 0.0 sehingga logika pemrosesan terikat pada input 8-bit
  • Pada metode 255, interval di kedua ujung hanya selebar setengah interval lain, sehingga jika bilangan acak uniform [0, 1] dibulatkan kembali ke 8-bit, 0 dan 255 muncul dengan frekuensi setengah dari nilai lain, tetapi konversi bolak-balik gambar nyata tetap bekerja tanpa kehilangan
  • Secara teoretis, metode 256 memiliki galat absolut rata-rata 1 / 1024, sedikit lebih kecil daripada 1 / 1020 pada metode 255, tetapi jika gambar yang sudah dikuantisasi dengan metode 255 dibaca dengan skala yang keliru, justru akan menambah galat

Pengaturan masalah

Program pemrosesan gambar mengubah gambar 8-bit menjadi bilangan floating point, melakukan pemrosesan, lalu menyimpannya kembali sebagai warna 8-bit

Dua metode konversinya adalah sebagai berikut

# Standar: bagi dengan 255
pixels = img / 255.0
result = process(pixels)
output = np.trunc(result * 255 + 0.5)


# Alternatif: tambah 0.5 lalu bagi dengan 256
pixels = (img + 0.5) / 256.0
result = process(pixels)
output = np.trunc(result * 256)

Kedua metode sama-sama membatasi nilai ke rentang 0~255 sebelum konversi akhir

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

Metode standar memetakan bilangan bulat 0 ke 0.0 dan 255 ke 1.0, dan sama dengan cara GPU melakukan konversi UNORM-to-float

Metode alternatif memetakan 0 ke 0.5 / 256 = 0.001953125, sehingga untuk mendeteksi piksel hitam perlu mengetahui konstanta ini

Karakteristik metode standar yang membagi dengan 255

Metode standar membuat interval untuk nilai di kedua ujung dalam rentang [0, 1] secara efektif hanya selebar setengah dari interval lainnya

Jika membuat bilangan acak uniform [0, 1] lalu membulatkannya dengan trunc(result * 255 + 0.5), maka 0 dan 255 akan muncul dengan frekuensi setengah dari bilangan bulat lainnya

Namun, gambar 8-bit asli tetap dapat kembali tanpa kehilangan dalam konversi bolak-balik uint8 → float → uint8

Selain itu, meskipun hasil pemrosesan sedikit keluar dari 0.0 atau 1.0, melalui clamp dan pembulatan nilainya tetap bisa masuk ke interval bilangan bulat yang benar

Sebagai contoh, jika 0.005 dikurangi dari warna floating point, hitam pada metode standar menjadi negatif, tetapi hasil akhirnya tetap bilangan bulat 0

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

Presisi floating point dan penempatan di tengah interval

Sebagian nilai pada metode 255 tidak dapat direpresentasikan secara eksak

Sebagai contoh, 128 / 255.0 ≈ 0.501961, sedangkan 128 / 256.0 = 0.5

Perbedaan ini hanyalah galat pembulatan pada tingkat bit paling rendah dalam mantissa 23-bit floating point 32-bit, dengan besar kurang dari 2^-23

Karena itu, ketidakakuratan ini lebih dekat ke persoalan estetika daripada masalah teknis nyata

Metode 256 menempatkan setiap nilai floating point tepat di tengah antara dua bilangan bulat

Sifat ini bisa dipandang sebagai kompromi berupa penggunaan titik rata-rata antara dua bilangan bulat berurutan ketika nilai hasil kuantisasi aslinya tidak diketahui secara pasti

Tulisan Andrew Kesler tahun 2015, “Converting Color Depth”, menilai bahwa pendekatan ini membuat penanganan batas saat menambahkan noise untuk dithering menjadi lebih mudah dipikirkan

Sebaliknya, interval ujung pada metode standar memerlukan penanganan yang cermat agar distribusi noise tetap konsisten

Sudut pandang kuantisasi

Kedua metode dapat dipandang sebagai uniform scalar quantizer

Penjelasan quantization di Wikipedia) terutama membagi uniform quantizer untuk signed input data menjadi mid-riser dan mid-tread

mid-tread memiliki level rekonstruksi bernilai 0, sedangkan mid-riser memiliki ambang klasifikasi bernilai 0

Rumusnya berkaitan sebagai berikut

Metode Encoding Decoding
mid-tread k = trunc(x L + 0.5) y_k = k / L
mid-riser k = trunc(x L) y_k = (k + 0.5) / L

Metode standar adalah bentuk mid-tread dengan L=255, sedangkan metode alternatif adalah bentuk mid-riser dengan L=256

Metode standar memperoleh kemudahan pemrograman karena kedua ujung tepat berada di 0.0 dan 1.0, tetapi penempatan intervalnya tidak optimal untuk input 8-bit

Galat rekonstruksi dan pemrosesan gambar nyata

Jika Anda merancang langsung sistem yang meng-encode bilangan riil berdistribusi uniform x ∈ [0, 1] menjadi bilangan bulat 8-bit lalu merekonstruksinya kembali menjadi bilangan riil, maka metode 256 secara teoretis lebih presisi

Rentang yang dapat direpresentasikan oleh metode standar menjadi [-0.5 / 255, 255.5 / 255], sehingga jarak antar interval menjadi lebih lebar daripada yang benar-benar dibutuhkan untuk [0, 1]

Menurut perhitungan pengguna StackOverflow Peter Mudrievskij, galat absolut rata-rata adalah 1 / 1020 untuk pembagian 255 dan 1 / 1024 untuk pembagian 256

Namun, dalam situasi membaca gambar RGB 8-bit yang sudah disimpan lalu memprosesnya, informasi yang hilang saat penyimpanan tidak dapat dipulihkan

Jika gambar dikuantisasi dengan mengalikan 255 lalu membulatkan, membaginya dengan 256 saat dimuat tidak akan mengembalikan presisi

Sebagian besar gambar yang dibuat pihak lain kemungkinan besar dikuantisasi dengan metode standar, sehingga membacanya dengan rumus alternatif berarti secara teoretis memakai faktor skala yang keliru

Dalam praktiknya, warna tidak berperilaku seperti nilai ukur absolut, sehingga hasilnya hanyalah pemrosesan pada rentang yang sedikit lebih kecil dengan offset kecil

Mencampur tahap encoding dan decoding dari dua quantizer ini akan menghasilkan kode yang rusak

Kesimpulan

Jika Anda memproses gambar yang disediakan pihak lain dan asal-usulnya tidak diketahui, nilai RGB sebaiknya dinormalisasi dengan 255

Alasan bahwa nilai floating point tidak eksak atau perasaan abstrak bahwa galat rekonstruksinya lebih besar bukan dasar yang kuat untuk memilih metode 256

Jika Anda mengendalikan sepenuhnya penyimpanan dan pemuatan gambar, tidak perlu memetakan 0 ke 0, dan tidak masalah bila kode pemrosesan terikat pada rentang dinamis 8-bit, maka membagi dengan 256 bisa dipilih untuk mengejar presisi teoretis yang sedikit lebih tinggi

1 komentar

 
GN⁺ 7 jam lalu
Pendapat di Lobste.rs
  • Terlihat berantakan, tapi memang benar, nilainya harus 255
    Kalau terasa tidak intuitif, lihat kasus terdegenerasi 2-bit. Jika satu-satunya nilai integer yang mungkin adalah 0, 1, 2, 3, lalu kita hitung seluruh konversi integer→floating point, maka untuk menghindari perilaku aneh seperti hitam/putih yang bukan benar-benar hitam/putih atau jarak yang jelas tidak merata, hasilnya menjadi 0.0, 0.33..., 0.66..., 1.0
    Karena itu, konversi baliknya bukan dengan mengalikan 4(2^2), melainkan mengalikan 3
    • Bagian awalnya benar, tetapi dari situ tidak otomatis mengikuti bahwa “konversi balik harus dikali 3, bukan 4”
      Konversi balik membutuhkan kuantisasi (pembulatan), dan justru di situlah simetri pecah
      Jika Anda membuat gradien bilangan real yang seragam pada rentang 0..=1 lalu mengkuantisasinya menjadi 0, 1, 2, 3, akan terlihat bahwa mengalikan 3 menghasilkan distribusi yang tidak merata. round() setelah ×3 membuat 1 dan 2 terwakili berlebihan, sementara floor atau ceil setelah ×3 membuat 0 atau 3 seperti titik singular sehingga gradien tampak hanya memakai 3 dari 4 warna
      Logika /3 dan ×3 tampak baik untuk konversi bolak-balik angka yang tepat, tetapi nilai antara sangat dipengaruhi oleh pilihan pembulatan, dan itu menjadi penting begitu Anda mulai memproses data
      Proporsi integer hanya menjadi seragam jika Anda mengalikan dengan (4-ε) lalu floor, yang setara dengan ×4, floor(), dan clamp(). Ini terasa seperti kesalahan aneh selisih 1 atau ε, tetapi secara intuitif itulah solusi yang paling enak dilihat
  • Saya cukup bingung karena judulnya. Tidak tahu apakah memang disengaja, tetapi ujung-ujungnya jadi terlihat seperti “apakah 0..1 dipetakan ke [0..255.0], atau ke [0.5..255.5]?”
    Bagi saya jawabannya selalu “jelas” [0.0..255.0], tetapi tampaknya itu tidak sejelas itu bagi semua orang
    Tulisan itu mengatakan bahwa rentang “ekstrem” hanya memiliki setengah kapasitas dibanding rentang lain, dan menurut saya framing ini juga kurang tepat
    Jika tidak ada nilai di luar [0..1], maka yang terlihat seperti rentang sempit itu hanyalah artefak perenderan. Itu hanya dirender lebih sempit karena bucket-nya dipotong dengan pengetahuan bahwa tidak ada nilai di luar rentang
    Sebaliknya, jika ada nilai di luar [0..1], maka rentang itu tak terbatas. Tulisan tersebut mengakui yang kedua, tetapi tidak mengakui yang pertama
    Begitu yang pertama diakui, perilaku yang benar tampak jelas, tetapi fakta bahwa tulisan seperti ini bisa muncul juga berarti masalah ini secara objektif tidak benar-benar “jelas” :D
    • Jika 0…255.0 benar-benar sejelas itu, maka rentang nilai floating point mana yang seharusnya kembali menjadi integer 0, dan rentang mana yang seharusnya kembali menjadi integer 255?
      Jika 0..<1 pergi ke integer 0, dan 254>..255.0 pergi ke integer 255, maka 128 akan hilang. Mungkin yang diinginkan adalah 127.5..128.5 menjadi 128, tetapi lalu setengah rentang ini harus pergi ke mana?
      Jika seluruhnya digeser sedikit demi menyesuaikan 128, maka 0..0.99609375 akan dipetakan ke integer 0
  • Pendekatan standar juga tampaknya muncul karena orang secara alami memanggil round()
    Karena cara itu terasa cukup alami bagi banyak orang, sepertinya ia menjadi standar karena kesederhanaannya
  • Saya penasaran apakah pendekatan kebalikan dari yang ingin dicapai dengan 256 juga berguna. Artinya, 0.0 dikirim ke 0, 1.0 dikirim ke 255, dan nilai floating point lainnya dipetakan ke 1 sampai 254
    uint8_t output = 0.0f >= result  
                     ? 0  
                     : 1.0f <= result  
                     ? 255  
                     : 1 + 253*result;  
    
    Akan bagus jika selama pemrosesan pun hitam tetap hitam, dan putih tetap putih
    • Dengan cara ini, 0 dan 255 mendapat porsi lebih besar dalam interval satuan dibanding angka lain. Kira-kira 0.8%, yaitu 255/253
  • Gambar pertama terlihat rusak di lingkungan saya
    • Penulis artikelnya di sini. Maksudnya file gambarnya rusak? Saya memang mengompresnya dengan pngcrush. Atau maksudnya isi gambarnya ada yang salah?