1 पॉइंट द्वारा GN⁺ 7 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • RGB normalization में अगर आप किसी अपरिचित image file को प्रोसेस करके फिर 8-bit में सेव कर रहे हैं, तो 255 से divide करने वाला standard तरीका उपयुक्त है
  • 255 वाला तरीका 0 को 0.0 और 255 को 1.0 पर map करता है, इसलिए काले और सफेद को सीधे संभालना आसान होता है, और यह GPU के UNORM-to-float conversion तरीके से भी मेल खाता है
  • 256 वाला तरीका (img + 0.5) / 256.0 के जरिए हर value को interval के center में रखता है, जिससे dithering जैसी processing में boundary handling सरल हो सकती है, लेकिन 0, 0.0 नहीं रहता, इसलिए processing logic 8-bit input से बंध जाता है
  • 255 वाले तरीके में दोनों सिरों के interval आधी चौड़ाई के होते हैं, इसलिए uniform [0, 1] random numbers को वापस 8-bit में round करने पर 0 और 255, बाकी मानों की तुलना में आधी बार आते हैं, लेकिन वास्तविक image round-trip conversion lossless काम करता है
  • 256 वाला तरीका सिद्धांततः mean absolute error 1 / 1024 देता है, जो 255 वाले तरीके के 1 / 1020 से थोड़ा कम है, लेकिन अगर पहले से 255 तरीके से quantize की गई image को गलत scale से पढ़ा जाए, तो यह उल्टा error बढ़ा देता है

समस्या की रूपरेखा

Image processing programs 8-bit images को floating-point में बदलते हैं, processing करते हैं, और फिर उन्हें वापस 8-bit colors के रूप में सेव करते हैं

दो conversion तरीके इस प्रकार हैं

# Standard: 255 से divide करना
pixels = img / 255.0
result = process(pixels)
output = np.trunc(result * 255 + 0.5)


# Alternative: 0.5 जोड़कर 256 से divide करना
pixels = (img + 0.5) / 256.0
result = process(pixels)
output = np.trunc(result * 256)

दोनों तरीकों में final conversion से पहले values को 0~255 तक सीमित किया जाता है

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

Standard तरीका integer 0 को 0.0 और 255 को 1.0 पर map करता है, और GPU के UNORM-to-float conversion तरीके जैसा है

Alternative तरीका 0 को 0.5 / 256 = 0.001953125 पर map करता है, इसलिए काले pixel को detect करने के लिए इस constant को जानना पड़ता है

255 से divide करने वाले standard तरीके की विशेषताएँ

Standard तरीके में [0, 1] range के भीतर दोनों सिरों के values के interval, बाकी interval की तुलना में प्रभावी रूप से आधी चौड़ाई के होते हैं

अगर uniform [0, 1] random numbers बनाए जाएँ और trunc(result * 255 + 0.5) से round किया जाए, तो 0 और 255 बाकी integers की तुलना में आधी frequency से दिखाई देंगे

लेकिन मूल 8-bit images uint8 → float → uint8 round-trip conversion में बिना किसी loss के वापस आ जाती हैं

इसके अलावा, अगर processing result 0.0 या 1.0 से थोड़ा बाहर भी चला जाए, तो clamp और rounding के बाद वह सही integer range में आ सकता है

उदाहरण के लिए, अगर floating-point color से 0.005 घटाया जाए, तो standard तरीके में black negative हो जाएगा, लेकिन final result फिर भी integer 0 ही रहेगा

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

Floating-point precision और interval center placement

255 वाले तरीके की कुछ values exactly represent नहीं होतीं

उदाहरण के लिए 128 / 255.0 ≈ 0.501961 है, जबकि 128 / 256.0 = 0.5 है

यह अंतर 32-bit floating-point के 23-bit mantissa में सबसे निचले bit स्तर की rounding error है, और इसका आकार 2^-23 से छोटा है

इसलिए यह inaccuracy व्यावहारिक तकनीकी समस्या से ज़्यादा एक सौंदर्यगत मुद्दे के करीब है

256 वाला तरीका हर floating-point value को दो integers के बीच के सटीक center पर रखता है

जब यह पता न हो कि मूल quantized value वास्तव में क्या थी, तब इस गुण को दो लगातार integers के बीच के average point का एक समझौता माना जा सकता है

Andrew Kesler की 2015 की पोस्ट “Converting Color Depth” के अनुसार, यह तरीका dithering में noise जोड़ते समय boundary handling को कम चिंताजनक बना सकता है

इसके उलट, standard तरीके में दोनों सिरों के interval के लिए noise distribution को consistent रखने हेतु सावधानीपूर्वक handling की ज़रूरत होती है

Quantization के नज़रिए से

दोनों तरीकों को uniform scalar quantizer के रूप में देखा जा सकता है

Wikipedia की quantization व्याख्या) signed input data के uniform quantizer को मुख्यतः mid-riser और mid-tread में बाँटती है

mid-tread में 0 value reconstruction level होता है, जबकि mid-riser में 0 value classification threshold होता है

संबंधित formulas इस प्रकार हैं

तरीका 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

Standard तरीका L=255 का उपयोग करने वाला mid-tread रूप है, और alternative तरीका L=256 का उपयोग करने वाला mid-riser रूप है

Standard तरीका programming convenience के लिए 0.0 और 1.0 पर endpoints को align करता है, लेकिन यह 8-bit input के लिए सबसे उपयुक्त interval placement नहीं है

Reconstruction error और वास्तविक image processing

अगर आप uniform distribution वाली real values x ∈ [0, 1] को 8-bit integers में encode करके फिर real values में reconstruct करने वाली system को सीधे design कर रहे हैं, तो 256 वाला तरीका सिद्धांततः अधिक precise है

Standard तरीके की representable range [-0.5 / 255, 255.5 / 255] बनती है, जिससे [0, 1] के लिए ज़रूरी सीमा की तुलना में interval spacing थोड़ी चौड़ी हो जाती है

StackOverflow उपयोगकर्ता Peter Mudrievskij की गणना के अनुसार mean absolute error, 255 से divide करने पर 1 / 1020 और 256 से divide करने पर 1 / 1024 है

लेकिन पहले से सेव की गई 8-bit RGB images को पढ़कर प्रोसेस करने की स्थिति में, सेव करते समय खोई हुई जानकारी वापस नहीं आती

अगर image को 255 से multiply और round करने वाले तरीके से quantize किया गया था, तो load करते समय 256 से divide करने पर precision वापस नहीं मिलेगी

दूसरों द्वारा बनाई गई images अधिकतर standard तरीके से quantize की गई होने की संभावना है, इसलिए alternative formula से पढ़ने पर सैद्धांतिक रूप से गलत scale factor इस्तेमाल होगा

व्यवहार में इसका मतलब यह है कि colors को थोड़ा छोटे range और छोटे offset के साथ process किया जाएगा

दो quantizer के encoding और decoding चरणों को मिलाना टूटा हुआ code बन जाता है

निष्कर्ष

अगर आप किसी अपरिचित स्रोत से मिली image को प्रोसेस कर रहे हैं, तो RGB values को 255 से normalize करना चाहिए

सिर्फ इस वजह से कि floating-point values exact नहीं हैं, या abstract reconstruction error थोड़ा बड़ा लगता है, 256 वाले तरीके को चुनने का आधार कमजोर है

अगर आप image saving और loading दोनों को नियंत्रित करते हैं, 0 का 0 पर map होना ज़रूरी नहीं है, और processing code का 8-bit dynamic range से बँधा होना स्वीकार्य है, तो 256 से divide करके थोड़ी अधिक सैद्धांतिक precision हासिल की जा सकती है

1 टिप्पणियां

 
GN⁺ 7 시간 전
Lobste.rs की राय
  • देखने में बेतरतीब लगता है, लेकिन सही जवाब 255 ही है
    अगर यह सहज न लगे, तो इसे 2-बिट के साधारण उदाहरण से देख सकते हैं। जब संभव integer values सिर्फ 0, 1, 2, 3 हों, तो integer→floating-point रूपांतरण के सभी मामलों को गिनने पर पता चलता है कि काला/सफेद को सचमुच काला/सफेद बनाए रखने और intervals को बराबर रखने के लिए मान 0.0, 0.33..., 0.66..., 1.0 होने चाहिए
    इसलिए reverse conversion में 4(2^2) नहीं, बल्कि 3 से गुणा करना चाहिए
    • शुरुआत तक बात सही है, लेकिन वहाँ से “reverse conversion में 3 से गुणा होना चाहिए, 4 नहीं” यह निष्कर्ष नहीं निकलता
      reverse conversion में quantization (rounding) चाहिए, और symmetry यहीं टूटती है
      अगर 0..=1 रेंज में एक uniform real gradient बनाकर उसे 0, 1, 2, 3 में quantize करें, तो दिखेगा कि 3 से गुणा करने पर नतीजा uniform नहीं रहता। ×3 के बाद round() करने पर 1 और 2 ज़्यादा represent होते हैं, और ×3 के बाद floor या ceil करने पर 0 या 3 ऐसे fold हो जाते हैं कि gradient मानो 4 रंगों में से सिर्फ 3 ही इस्तेमाल कर रहा हो
      /3 और ×3 वाली logic exact numbers के round-trip conversion के लिए ठीक लग सकती है, लेकिन बीच के मान rounding के चुनाव से बहुत प्रभावित होते हैं, और डेटा processing शुरू करते ही यही महत्वपूर्ण हो जाता है
      integers का uniform proportion सिर्फ (4-ε) से गुणा करके floor लेने पर मिलता है, जो ×4, floor(), clamp() के बराबर है। यह 1 के अजीब अंतर या ε-error जैसा लग सकता है, लेकिन सहज रूप से यही सबसे अच्छा दिखने वाला हल है
  • शीर्षक की वजह से मैं बहुत उलझ गया। पता नहीं यह जानबूझकर था या नहीं, लेकिन आखिरकार सवाल कुछ ऐसा लगता है: “क्या 0..1 [0..255.0] से मेल खाता है, या [0.5..255.5] से?”
    मेरे लिए जवाब हमेशा “बिलकुल” [0.0..255.0] रहा है, लेकिन शायद यह सबके लिए उतना स्पष्ट नहीं है
    लेख में कहा गया है कि “extreme” intervals की क्षमता बाकी intervals की आधी है, लेकिन मुझे यह framing भी सही नहीं लगती
    अगर [0..1] के बाहर कोई values हैं ही नहीं, तो उनका संकरा interval दिखना rendering का प्रभाव है। आपने यह जानते हुए कि range के बाहर कोई value नहीं है, buckets को काट दिया है, इसलिए वे संकरे render हुए हैं
    उलटे, अगर [0..1] के बाहर values मौजूद हैं, तो वह range अनंत है। लेख बाद वाली बात मानता है, लेकिन पहली नहीं
    जैसे ही पहली बात मान लें, सही behavior काफ़ी स्पष्ट लगता है, लेकिन इस तरह का लेख आया ही है, यह बताता है कि यह वस्तुनिष्ठ रूप से इतना “स्पष्ट” सवाल नहीं है :D
    • अगर सचमुच 0…255.0 ही स्पष्ट जवाब है, तो कौन-सी floating-point value range integer 0 पर वापस जानी चाहिए, और कौन-सी integer 255 पर?
      अगर 0..<1 integer 0 पर जाए, और 254>..255.0 integer 255 पर, तो 128 बीच में गायब हो जाता है। शायद आप चाहेंगे कि 127.5..128.5, 128 पर जाए, लेकिन फिर ये आधे हिस्से कहाँ जाएंगे?
      अगर 128 को सही बैठाने के लिए पूरी range को थोड़ा shift करें, तो 0..0.99609375 integer 0 पर map होता है
  • standard approach भी शायद इसलिए बनी क्योंकि लोग सहज रूप से round() बुला देते हैं
    लोगों को वह तरीका काफ़ी natural लगता है, इसलिए शायद सादगी की वजह से वही standard बन गया
  • सोच रहा हूँ कि 256 वाले लक्ष्य का उल्टा तरीका भी उपयोगी हो सकता है या नहीं। यानी 0.0 को 0 पर, 1.0 को 255 पर भेजें, और बाकी floating-point values को 1 से 254 के बीच map करें
    uint8_t output = 0.0f >= result  
                     ? 0  
                     : 1.0f <= result  
                     ? 255  
                     : 1 + 253*result;  
    
    अच्छा होगा अगर processing के दौरान भी black, black ही रहे और white, white ही रहे
    • ऐसा करने पर 0 और 255 को unit interval में बाकी संख्याओं की तुलना में बड़ा हिस्सा मिलता है। लगभग 0.8%, यानी 255/253 के बराबर
  • पहली image मेरे environment में टूटी हुई दिखती है
    • मैं लेख का लेखक हूँ। क्या आपका मतलब image file के corrupt होने से है? मैंने इसे pngcrush से compress किया था। या आपका मतलब है कि image की सामग्री में कुछ गड़बड़ है?