- 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 daripada1 / 1020pada 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
Pendapat di Lobste.rs
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
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, sementaraflooratauceilsetelah ×3 membuat 0 atau 3 seperti titik singular sehingga gradien tampak hanya memakai 3 dari 4 warnaLogika
/3dan×3tampak baik untuk konversi bolak-balik angka yang tepat, tetapi nilai antara sangat dipengaruhi oleh pilihan pembulatan, dan itu menjadi penting begitu Anda mulai memproses dataProporsi integer hanya menjadi seragam jika Anda mengalikan dengan (4-ε) lalu floor, yang setara dengan ×4,
floor(), danclamp(). Ini terasa seperti kesalahan aneh selisih 1 atau ε, tetapi secara intuitif itulah solusi yang paling enak dilihatBagi 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..<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
round()Karena cara itu terasa cukup alami bagi banyak orang, sepertinya ia menjadi standar karena kesederhanaannya
pngcrush. Atau maksudnya isi gambarnya ada yang salah?