- เมื่อแปลงค่าสีจำนวนเต็ม 8 บิตเป็นเลขทศนิยมแบบ floating-point จะมีความต่างระหว่าง วิธีมาตรฐานที่หารด้วย 255 กับ วิธีทางเลือกที่บวกไบแอส 0.5 แล้วหารด้วย 256
- วิธี 255 จะแมปจำนวนเต็ม 0 ไปเป็น 0.0 และ 255 ไปเป็น 1.0 ทำให้จัดการกับ สีดำและสีขาว ได้ตรงไปตรงมา และยังสอดคล้องกับวิธีแปลง UNORM-to-float ของ GPU
- วิธี 256 ใช้
(img + 0.5) / 256.0เพื่อวางแต่ละค่าไว้ที่กึ่งกลางของช่วง ทำให้งานอย่าง dithering จัดการขอบเขตได้ง่ายขึ้น แต่ 0 จะไม่ใช่ 0.0 จึงทำให้ตรรกะการประมวลผลผูกกับอินพุต 8 บิต - วิธี 255 มีช่วงปลายทั้งสองด้านกว้างเพียงครึ่งเดียว ดังนั้นหากปัดเศษเลขสุ่มสม่ำเสมอในช่วง
[0, 1]กลับไปเป็น 8 บิต ค่า 0 และ 255 จะเกิดขึ้นด้วยความถี่เพียงครึ่งหนึ่งของค่าอื่น ๆ แต่การแปลงภาพไปกลับจริงยังทำได้โดยไม่สูญเสียข้อมูล - หากคุณกำลังประมวลผลภาพของผู้อื่น คำตอบที่ถูกต้องคือ normalize ด้วย 255 และควรพิจารณาวิธี 256 เฉพาะเมื่อคุณควบคุมทั้งการบันทึกและการโหลดได้เท่านั้น
การตั้งปัญหา
- ในโปรแกรมที่รับภาพมาแปลงเป็น floating-point ประมวลผล แล้วบันทึกกลับเป็นค่าสี 8 บิต ประเด็นอยู่ที่ วิธีแปลงระหว่างจำนวนเต็มกับ floating-point
- มีอยู่สองแนวทาง
- วิธีมาตรฐาน (หารด้วย 255):
pixels = img / 255.0→ ประมวลผล →output = np.trunc(result * 255 + 0.5) - วิธีทางเลือก (หารด้วย 256):
pixels = (img + 0.5) / 256.0→ ประมวลผล →output = np.trunc(result * 256) - ทั้งสองกรณีจะจำกัดค่าก่อนแปลงชนิดสุดท้ายให้อยู่ในช่วง 0~255:
output.clip(0, 255).astype(np.uint8)
- วิธีมาตรฐาน (หารด้วย 255):
- วิธีมาตรฐานจะแมปจำนวนเต็ม 0 ไปเป็น 0.0 และ 255 ไปเป็น 1.0 ซึ่งเหมือนกับวิธี UNORM-to-float conversion ของ GPU
- วิธีทางเลือกจะบวกไบแอส 0.5 ทำให้จำนวนเต็ม 0 ถูกแมปเป็น
0.5/256 = 0.001953125- ด้วยเหตุนี้ หากไม่รู้ค่าคงที่นี้ก็จะ ตรวจจับพิกเซลดำไม่ได้
- แม้จะคำนวณด้วย floating-point แต่ตรรกะก็ยังผูกติดกับอินพุต 8 บิต
- ในวิธีมาตรฐาน สามารถถือว่าสีดำคือ 0.0 ได้เสมอ
ข้อโต้แย้งต่อ 255.0
- หากวาดวิธีมาตรฐานลงบนเส้นจำนวน จะดูแปลกอยู่บ้าง
-
มี bin ที่ปลายทั้งสองด้านเล็กกว่า
- bin ปลายทั้งสองของสูตรมาตรฐานยื่นออกไปนอกช่วง [0,1] ทำให้มีลักษณะเป็นช่วงที่ ถูก “ยืด” ออก
- เมื่อแปลง floating-point กลับเป็นจำนวนเต็ม ความกว้างของ bin ปลายทั้งสองจะมีเพียงครึ่งหนึ่งของ bin อื่น
- ทำให้ในอัลกอริทึม “ยากกว่า” ที่จะได้ค่าเอ็กซ์ตรีมเป็นเอาต์พุต
- หากสร้าง noise สม่ำเสมอในช่วง [0,1] แล้วปัดเศษด้วยสูตรมาตรฐาน ค่า 0 และ 255 จะเกิดขึ้นเพียงครึ่งหนึ่งของความถี่ของจำนวนเต็มอื่น ๆ
- หากดู histogram ของเลขสุ่มสม่ำเสมอ 1 ล้านค่า จะเห็นได้ว่าความถี่ของ 0 และ 255 สูงเพียงครึ่งเดียวของ bin อื่น
- อย่างไรก็ตาม นึกสถานการณ์จริงที่อคติแบบหลีกเลี่ยงค่าเอ็กซ์ตรีมนี้จะเป็นปัญหาได้ยาก
- ภาพต้นฉบับยังคงแปลงไปกลับได้โดยไม่สูญเสียข้อมูล (
uint8 → float → uint8) - แม้ผลลัพธ์จะเกิน 0.0 หรือ 1.0 ไปเล็กน้อย ก็ยังถูกปัดกลับเข้า bin ที่ถูกต้อง ทำให้การกระจายของเอาต์พุตสมดุล
- ตัวอย่าง: หากในขั้นประมวลผลลบ 0.005 ออกจากสี วิธีมาตรฐานจะทำให้สีดำต่ำกว่า 0 ส่วนวิธีทางเลือกยังคงเป็นบวก แต่ทั้งสองวิธีก็สุดท้ายให้จำนวนเต็ม 0 เหมือนกัน
- ภาพต้นฉบับยังคงแปลงไปกลับได้โดยไม่สูญเสียข้อมูล (
-
ความไม่แม่นตรงตัว
- ค่า floating-point ของวิธีมาตรฐานไม่ตรงเป๊ะ เช่น
128/255.0 ≈ 0.501961แต่128/256.0 = 0.5 - ระยะห่างระหว่างค่า floating-point อาจแปรผันเล็กน้อยจากความคลาดเคลื่อนในการปัดเศษ แต่ความผิดพลาดเล็กมากจนแทบไม่มีผลในทางปฏิบัติ
- floating-point 32 บิตมี mantissa 23 บิต และความผิดพลาดอยู่ในระดับบิตล่างสุด ต่ำกว่า
2⁻²³ - ค่าความคลาดเคลื่อนสัมพัทธ์ 0.00001% ไม่มีนัยสำคัญแม้ในงานประมวลผลภาพที่ละเอียด ความไม่ตรงนี้เป็นปัญหาเชิงความรู้สึกมากกว่าปัญหาเชิงเทคนิค
- floating-point 32 บิตมี mantissa 23 บิต และความผิดพลาดอยู่ในระดับบิตล่างสุด ต่ำกว่า
- ค่า floating-point ของวิธีมาตรฐานไม่ตรงเป๊ะ เช่น
-
ค่าที่ไม่ได้อยู่บนช่วงจำนวนเต็มพอดี
- วิธีทางเลือกจะวางค่า floating-point แต่ละค่าไว้ ตรงกึ่งกลางระหว่างจำนวนเต็มสองค่า
- เนื่องจากไม่รู้ค่าควอนไทซ์เดิม จุดกึ่งกลางของจำนวนเต็มที่ติดกันจึงเป็นการประนีประนอมที่ดีในฐานะค่าประมาณ
- มีผู้เสนอว่าสะดวกกว่าสำหรับ dithering (บล็อกโพสต์ปี 2015 ของ Andrew Kesler เรื่อง "Converting Color Depth")
- สามารถเพิ่ม noise ได้โดยไม่ต้องกังวล edge case
- ในทางกลับกัน ค่าเอ็กซ์ตรีมที่ดูแปลกของสูตรมาตรฐานอาจต้องจัดการอย่างระมัดระวังเพื่อให้การกระจายของ noise สม่ำเสมอ
- วิธีทางเลือกจะวางค่า floating-point แต่ละค่าไว้ ตรงกึ่งกลางระหว่างจำนวนเต็มสองค่า
ควอนไทเซอร์สองชนิด
- ทั้งสองแนวทางสามารถมองได้ว่าเป็น uniform scalar quantizer สองชนิด
- ตามบทความเรื่อง Quantization บน Wikipedia uniform quantizer สำหรับข้อมูลอินพุตแบบมีเครื่องหมายแบ่งได้เป็นสองประเภท
- mid-tread: แมป 0 ไปยังระดับ reconstructed value ที่เป็น 0 (เหมือนพื้นขั้นบันได)
- mid-riser: แมป 0 ไปยัง threshold สำหรับจำแนกค่า 0 (เหมือนหน้าลูกตั้งของขั้นบันได)
- Wikipedia อ้างอิงบทความปี 1977 ของ Allen Gresho ชื่อ "Quantization"
- สูตรของ quantizer (L คือจำนวนระดับเอาต์พุต เช่น 256)
- staircase quantizer แบบ mid-tread: เข้ารหัส
k = trunc(xL + 0.5), ถอดรหัสyₖ = k/L - staircase quantizer แบบ mid-riser: เข้ารหัส
k = trunc(xL), ถอดรหัสyₖ = (k+0.5)/L
- staircase quantizer แบบ mid-tread: เข้ารหัส
- เมื่อนำมาใช้กับสองวิธีนี้
- สูตรมาตรฐาน = mid-tread (L=255)
- สูตรทางเลือก = mid-riser (L=256)
- วิธีมาตรฐานเป็นการใช้ mid-tread กับอินพุตแบบไม่มีเครื่องหมายพร้อมเลือกโค้ด L=255 ซึ่งไม่ใช่ตัวเลือกที่เหมาะที่สุดสำหรับอินพุต 8 บิต
- เป็นการเลือกเพื่อความสะดวกในการเขียนโปรแกรม เพราะสามารถแมปปลายทั้งสองไปยัง 0.0 และ 1.0 ได้
-
ความคลาดเคลื่อนจากการควอนไทซ์ที่สูงกว่า แต่ในทางปฏิบัติไม่ใช่ประเด็น
- หากเป็นระบบที่เข้ารหัสจำนวนจริงแบบกระจายสม่ำเสมอ x∈[0,1] เป็นจำนวนเต็ม 8 บิตแล้วถอดรหัสกลับเป็นจำนวนจริง สูตรมาตรฐานจะ สิ้นเปลืองแบนด์วิดท์
- ช่วงที่แทนได้ของวิธีมาตรฐานคือ
[-0.5/255, 255.5/255]ซึ่งกว้างเกินความจำเป็นสำหรับอินพุต [0,1] และทำให้ reconstruction error เพิ่มขึ้น - จากการคำนวณของผู้ใช้ StackOverflow ชื่อ Peter Mudrievskij ค่าเฉลี่ยความคลาดเคลื่อนสัมบูรณ์คือ ตัวหาร 255 ได้
1/1020ส่วนตัวหาร 256 ได้1/1024ดังนั้น การหารด้วย 256 แม่นยำกว่าเล็กน้อยในเชิงทฤษฎี
- ช่วงที่แทนได้ของวิธีมาตรฐานคือ
- แต่ในทางปฏิบัติ สิ่งที่เราทำไม่ใช่การ reconstruct แบบนั้น
- สมมุติฐานคือโหลดภาพ RGB 8 บิตมาประมวลผลแล้วบันทึกกลับ โดยตอนบันทึกเราไม่สามารถควบคุมวิธี quantization เดิมได้ และข้อมูลที่สูญหายไปแล้วจะหายไปถาวร
- หากภาพถูกบันทึกด้วยการคูณและปัดเศษตามสูตรมาตรฐาน ต่อให้โหลดด้วยการหาร 256 ก็ไม่สามารถกู้ precision กลับมาได้
- ข้ออ้างเรื่อง reconstruction error ที่ต่ำกว่าจะมีความหมายก็ต่อเมื่อคุณควบคุมทั้งการบันทึกและการโหลดได้เท่านั้น
- การโหลดภาพของผู้อื่นด้วยสูตรทางเลือกกลับจะ สร้างความผิดพลาดมากกว่าเดิม
- เพราะมีโอกาสสูงที่ภาพส่วนใหญ่ถูก quantize ด้วยสูตรมาตรฐาน การถอดรหัสด้วยสเกลที่ผิดจึงไม่ถูกต้องในทางทฤษฎี
- ในทางปฏิบัติ สีไม่ได้เป็นค่าที่วัดแบบสัมบูรณ์ จึงเท่ากับแค่ประมวลผลในช่วงที่เล็กลงเล็กน้อยพร้อมมีออฟเซ็ตเล็กน้อย
- ไม่ควรนำขั้นเข้ารหัสและถอดรหัสของ quantizer สองแบบมาปะปนกัน เพราะจะได้โค้ดที่พังซึ่งพบได้บ่อย
- หากเป็นระบบที่เข้ารหัสจำนวนจริงแบบกระจายสม่ำเสมอ x∈[0,1] เป็นจำนวนเต็ม 8 บิตแล้วถอดรหัสกลับเป็นจำนวนจริง สูตรมาตรฐานจะ สิ้นเปลืองแบนด์วิดท์
บทสรุป
- หากคุณกำลังประมวลผลภาพที่คนอื่นให้มา ควร normalize ค่า RGB ด้วย 255
- ความกังวลเรื่องค่า floating-point ที่ไม่เป๊ะหรือ reconstruction error เชิงนามธรรม ไม่ใช่เหตุผลที่ดีพอในการเลือกวิธีทางเลือก
- หากคุณควบคุมทั้งการบันทึกและการโหลดภาพได้ทั้งหมด ไม่จำเป็นต้องแมป 0 ไปเป็น 0 และยอมให้โค้ดประมวลผลผูกกับ dynamic range แบบ 8 บิตได้ ก็อาจ หารด้วย 256 เพื่อได้ precision เพิ่มขึ้นเล็กน้อย
- แต่ต้องระวังว่าคนร่วมงานอาจโหลดภาพด้วยสูตรมาตรฐานแล้วทำให้แผนพังได้
มุมมองอื่น ๆ
- บทความปี 2002 ของ Jonathan Blow กล่าวถึง quantizer แบบ mid-riser และ mid-tread โดยไม่เรียกชื่อ ซึ่งเป็นที่มาของไอเดียแผนภาพ
- บล็อกโพสต์ปี 2015 ของ Andrew Kesler สนับสนุนสูตรทางเลือก
- อย่างไรก็ตาม สิ่งที่นำมาเปรียบเทียบคือสูตรมาตรฐานแบบไม่ปัดเศษ ทำให้บทวิเคราะห์ส่วนใหญ่ใช้ไม่ได้
2 ความคิดเห็น
ความเห็นจาก Hacker News
สำหรับค่าสี ความหมายที่แท้จริงคืออะไรกันแน่ ในระบบ 8 บิตต่อองค์ประกอบ โดยมากไม่ใช่เรื่องใหญ่อะไร ความคลาดเคลื่อนที่เกิดจากการใช้ตัวหาร 255 หรือ 256 นั้นเล็กมาก และกว่าจะมองเห็นความต่างได้ก็ต้องเป็นคนที่แยกสีเก่งและเอาหน้าเข้าไปใกล้จอมาก ๆ อีกทั้งจอมอนิเตอร์หรือหน้าจอโทรศัพท์ก็มักไม่ได้คาลิเบรตกันอยู่แล้ว
แต่ถ้าคุณกำลัง สร้างสัญญาณ VGA ด้วยไมโครคอนโทรลเลอร์ และมีขาส่งออกสีแค่ 8 ขาเท่านั้น (แดง 3, เขียว 3, น้ำเงิน 2) มันจะชวนปวดหัวขึ้นมาทันที ในกรณีนี้ค่าสีก็คือระดับแรงดันไฟฟ้า 0V~0.7V ที่ต้องส่งไปยังจอ VGA โดยตรง
ช่องสีน้ำเงินแมปเป็น 0→0V, 1→0.23V, 2→0.47V, 3→0.7V ส่วนแดง/เขียวแมปเป็น 0→0V, 1→0.1V, …, 7→0.7V เมื่อตัดค่าปลายทั้งสองด้านออก แรงดันของสีน้ำเงินจะไม่ตรงกับแดง/เขียวเลยแม้แต่น้อย ทำให้มอง สีเทาบริสุทธิ์ ไม่ได้ และสีที่ใกล้ที่สุดก็จะติดโทนน้ำเงินหรือเหลืองเล็กน้อย ขึ้นอยู่กับทิศทางของความต่าง
แถมกราเดียนต์แทบทั้งหมดที่ผสมสีน้ำเงินกับช่องอื่นก็ดูเพี้ยนไปด้วย เช่น สีที่ใกล้ที่สุดตามแนวเส้นจากแดงล้วนไปขาวล้วนจะดูออกส้ม ๆ หรือม่วง ๆ เล็กน้อย
โค้ดสำหรับส่งออก VGA สี 8 บิตบน Raspberry Pi Pico 2 ด้วยเฟรมบัฟเฟอร์คู่ขนาด 320x240 อยู่ที่นี่: https://github.com/moefh/pico-vga-8bit-demo
แบบนี้ความต่างระหว่างค่าน้อยกับค่ามากจะเด่นชัดขึ้นมาก: 2^2.2 = 4.595, 255^2.2 = 196,964.699
ถ้าเปลี่ยนที่ 30Hz มนุษย์น่าจะแยกความต่างระหว่างโทนน้ำเงินนิด ๆ กับเหลืองนิด ๆ ได้ยาก
เหตุผลที่สนับสนุน 255 ดูได้จากกรณีสุดขั้วอย่างภาพขาวดำ ในข้อมูล 1 บิต 0 คือดำ และ 1 คือขาว
ค่อนข้างชัดเจนว่าควรแมป 0 ไปที่ 0.0 และ 1 ไปที่ 1.0 เพราะมันเป็นขาวดำ ไม่ใช่เทาอ่อน (0.25) กับเทาเข้ม (0.75) กล่าวคือภาพขาวดำต้องนอร์มัลไลซ์ด้วย 1 ไม่ใช่ 2
ถ้าเป็น 2 บิต โดยทั่วไปจะได้ 0=ดำ, 1=เทาอ่อน, 2=เทาเข้ม, 3=ขาว ดังนั้นการแมปเป็น 0.0, 0.33, 0.66, 1.0 จึงเป็นธรรมชาติ ดำต้องเป็นดำ ขาวต้องเป็นขาว และระยะห่างก็ควรเท่ากัน จึงนอร์มัลไลซ์ด้วย 3
ถ้าขยายตรรกะนี้ต่อไปถึง 8 บิต ก็จะได้ว่า ควรนอร์มัลไลซ์ด้วย 255 เพราะถึงใน 8 บิตความต่างจะเล็กมาก แต่ดำก็ควรเป็น 0.0 และขาวก็ควรเป็น 1.0
ถ้าใช้อีกวิธีคือใน 8 บิตนอร์มัลไลซ์ด้วย 256 ช่วงเอาต์พุตจะเปลี่ยนไปตามจำนวนบิต เช่น 1 บิตจะเป็น [0.25, 0.75], 2 บิตจะเป็น [0.125, 0.875] เป็นต้น ปกติสิ่งที่ต้องการคือยิ่งบิตมากก็ยิ่งได้เฉดละเอียดขึ้น ไม่ใช่ให้คอนทราสต์เปลี่ยนไป
เป็นบทความที่ชวนคิดมาก และทำให้ผมต้องกลับมาทบทวนสมมติฐานที่ตัวเองเคยมี
ถ้ามองจากพื้นฐานวิศวกรรมไฟฟ้า ผมเห็นด้วยยากกับการเสนอว่าเป็น “ตัวควอนไทเซอร์สองชนิด” แม้ในเชิงคณิตศาสตร์จะเคร่งครัด แต่ไม่ใช่คำอธิบายที่ตั้งอยู่บนระบบจริง
ADC มี ความไม่แน่นอนของการควอนไทซ์แบบ ±1/2 LSB ติดตัวอยู่เสมอ ลักษณะการถ่ายโอนจะเป็นการสุ่มตัวอย่างแบบ mid-tread ตลอด อย่างน้อยผมก็ยังไม่เคยเห็นตัวอย่างโต้แย้ง ไม่ว่าจะเป็น ADC แบบ bipolar หรือ unipolar ก็เหมือนกัน
โค้ดต่ำสุดอ้างอิงกับแรงดันลบ และโค้ดสูงสุดอ้างอิงกับแรงดันบวก กราฟลักษณะการถ่ายโอนแสดงให้เห็นว่าช่วงบนสุด/ล่างสุดนั้นมีความกว้างจริง ๆ แค่ 1/2 LSB ตามที่บทความแสดงไว้
ในระบบ unipolar จะไม่สามารถแทนแรงดันกึ่งกลางได้อย่างแม่นยำ หรือพูดอีกอย่างคือเกิดปัญหาสีเทา ส่วนในระบบ bipolar นั้น 0V คือค่า N/2 ของ mid-tread แต่ก็ไม่ได้แปลว่ามี “256 ช่วง” อยู่
เพราะอย่างนั้นผมจึงยังคงใช้ (VREF+ - VREF-) * k / (2^N - 1) ต่อไป กล่าวคือผมเห็นด้วยกับ การนอร์มัลไลซ์ด้วย 255 ท้ายที่สุดมันก็เหมือน ข้อผิดพลาดแบบเสารั้ว มีค่าอยู่ N ค่า แต่มีช่วงอยู่ N-1 ช่วง ถ้าช่วงมีน้อยกว่าค่า ก็ต้องแบ่งช่วงหนึ่งไประหว่างสองค่า และนั่นทำให้เกิดช่วง 1/2 LSB ที่ปลายทั้งสองด้าน
จุดเปลี่ยนจาก 126 ไป 127 จะเกิดขึ้นที่ตำแหน่งซึ่งห่างจากช่วงบวกเต็มสเกลอยู่ 1.5 LSB ความต่าง 1 LSB จึงหมายถึง 1/128=0.00781V ไม่ใช่ 2/255=0.00784V
แต่ในทางปฏิบัติ ถ้าสิ่งที่สำคัญจริง ๆ คือแรงดันและความไม่แน่นอน ความต่างระดับนี้ส่วนใหญ่ก็ไม่มีนัยสำคัญนัก แรงดันอ้างอิงมีไบแอส และยังมีข้อผิดพลาดด้านเชิงเส้นอีก 1 LSB ไม่ได้ตรงกับทั้ง 1/128 หรือ 2/255 แบบเป๊ะ ๆ และสุดท้ายก็ต้องมีพารามิเตอร์สำหรับการคาลิเบรตอยู่ดี
เรื่องนี้คล้ายกับความต่างระหว่างการสุ่มตัวอย่างแบบ node-centered และ cell-centered ในงานคำนวณเชิงวิทยาศาสตร์ เมื่อมองใน 1 มิติ ต้องตัดสินใจว่าค่านั้นอยู่ที่กึ่งกลางของช่วง (หรือกึ่งกลางของสามเหลี่ยม/เตตระฮีดรอน) หรืออยู่ที่ขอบของช่วง (หรือมุมของสามเหลี่ยม/เตตระฮีดรอน)
ในงานคำนวณเชิงวิทยาศาสตร์ การเริ่มประมวลผลข้อมูลทั้งที่ยังไม่รู้ว่าควรตีความค่าอย่างไรถือว่าไม่สมเหตุสมผลเลย ในการประมวลผลสัญญาณเสียงก็เช่นกัน ถ้าคุณได้รับมาแค่สตรีมจำนวนเต็ม คุณจำเป็นต้องรู้ว่าเจตนาในการแทนค่านั้นคืออะไร เช่นเป็นการเข้ารหัสแบบ mu-law หรือเป็นเชิงเส้น จึงจะคำนวณกับสัญญาณต้นฉบับได้ และเราก็คาดหวังให้เมทาดาทาที่ติดมากับค่าช่วยตอบคำถามนี้
แต่กับค่าพิกเซล 8 บิต หากไม่มีเมทาดาทาจากฟอร์แมตไฟล์ที่เหมาะสมมาช่วยบอกเจตนาในการแทนค่า เราก็จะหลงทางและไม่มีคำตอบที่ถูกต้องเพียงหนึ่งเดียว อย่างที่ผู้เขียนบอก การเลือกวิธีที่ให้ผลดีกว่ากับงานของตัวเองไม่ใช่เรื่องที่ควรถูกตำหนิ แต่ก็บอกได้ว่า บิตที่ไร้บริบท นั้นทำให้ความหมายเสียหาย
โดยคร่าว ๆ จะเป็นแบบนี้: Digital Number DN=0 ถูกเก็บไว้เป็นค่า “NO_DATA” และเมื่อ DN อยู่ในช่วง [1; 1;215-1] ค่ารีเฟลกแตนซ์ L2A SR จะเป็น L2A_SRi = (L2A_DNi + BOA_ADD_OFFSETi) / QUANTIFICATION_VALUE
https://sentiwiki.copernicus.eu/web/s2-products
ตรงนี้มีความผิดพลาดจากการสมมติว่ามี 256 ขั้น ตั้งแต่ 0 ถึง 255 จริง ๆ แล้วคือมีค่าที่แทนได้ด้วย 8 บิตอยู่ 256 ค่า และมีช่วงห่าง 255 ช่วงจาก 0 (สีดำ) ถึง 255 (สีขาวล้วน)
ดังนั้นการหารด้วย 255 จึงไม่ใช่ปัญหา แน่นอนว่า 128 ไม่ใช่สีเทากลางที่แม่นยำ และค่าควอนไทซ์แบบ 8 บิตในช่วง 0~255 ก็แทบจะอยู่ใน sRGB ไม่ใช่ในปริภูมิการรับรู้เชิงเส้น
ความสับสนคล้ายกันนี้ก็เกิดขึ้นใน API สมัยใหม่เวลาอ้างอิงตำแหน่งการสุ่มตัวอย่าง เพราะตำแหน่งถูกระบุเป็นพิกัด ไม่ใช่ศูนย์กลางพิกเซล
ถ้ามองเชิงพีชคณิต คำตอบชัดเจนว่า f(x) -> [0, 255]
ถ้า f(n * 0) == n * f(0) ไม่เป็นจริง ก็จะเกิดเรื่องแปลก ๆ ตัวอย่างเช่น ถ้า f(x) -> [0, 255] แล้ว f(0) + f(0) + f(0) = 0 + 0 + 0 = 0 = f(0)
แต่ถ้า f(x) -> [0.5/8, 7.5/8] จะได้ว่า f(0) + f(0) + f(0) = 0.5/8 + 0.5/8 + 0.5/8 = 1.5/8 != f(0)
ถ้าเลือกแบบหลัง ก็ไม่อาจคาดหวังได้ว่าการคำนวณฝั่ง x กับการคำนวณฝั่ง f(x) จะสอดคล้องกัน กล่าวคือ ความสอดคล้องเชิงพีชคณิต พังลง
ฉันอยากสนับสนุนแนวทาง +0.5 อย่างแรกคือฉันไม่ชอบช่วงครึ่งขนาดที่ขอบ และอย่างที่สอง การแทนค่าแบบฐาน 255 โดยทั่วไปไม่ได้ใช้กับ HDR แต่ใช้กับ ภาพ SDR
ค่า RGB แทนความสว่างสัมพันธ์กับสถานะการปรับตัวบางอย่าง และ “0” ในฉากเวลากลางวันก็ไม่ได้หมายถึง “ความสว่าง 0” มันเป็นเพียงประมาณ 0.001 เท่าของจุดที่สว่างที่สุด และยังมีโฟตอนอยู่อีกหลายล้าน ไม่ได้ใกล้ 0 เลย
ในแง่หนึ่ง ดวงตารับรู้คอนทราสต์ในลักษณะเป็นสเกลที่เลื่อนไปมา และไม่มี 0 แบบสัมบูรณ์อยู่ในระบบ ตัวอย่างเช่น ระบบกระจายภาพเคยใช้ช่วงความสว่าง SDR ที่ 16~235 มาแต่เดิม ฉันมองว่าตรรกะประเภท “ต้องมี 0 เสมอ” ทำให้เกิดอคติ และคิดว่าในกรณีส่วนใหญ่ 0 ไม่จำเป็น
นอกจากนี้ เวิร์กโฟลว์การประมวลผลภาพและคอมโพสิตจำนวนมาก ไม่ว่าจะถูกหรือผิด ก็มักสมมติว่า 0 หมายถึง 0 ดังนั้นจึงมักถือว่าค่า 0u ใน 8 บิตแมปเป็น 0.0f และ 255 แมปเป็น 1.0f ถ้าค่า 0 ในมาสก์หรืออัลฟ่ากลายเป็นมากกว่า 0.0 เพียงเล็กน้อย ก็อาจเกิดอาร์ติแฟกต์เมื่อโค้ดบางจุดใช้ค่า threshold แบบตายตัวที่ 0.0 เพื่อมาสก์การคำนวณอื่น ๆ ในทางกลับกัน ถ้า 255 ในอัลฟ่าไม่ใช่ 1.0f อีกต่อไป วัตถุหลัง premultiply ก็จะโปร่งใสเล็กน้อย
เรื่องเดียวกันนี้ก็เกิดได้ถ้า +0.5 ทำให้ 254 กลายเป็น 1.0f ในการมาสก์
ประเด็นสำคัญไม่ใช่ว่าจะแทน 0 โฟตอนได้หรือไม่ แต่คือจะทำให้ข้อมูลที่เก็บใน 1 ไบต์มีประสิทธิภาพสูงสุดได้อย่างไร ตามอุดมคติแล้วไม่ควรใช้งานค่าไบต์ 0 ให้น้อยลง และไม่ควรเพิ่มไบแอสให้ข้อมูลที่ควรตกอยู่ในบักเก็ตที่ 0 แม้จะเป็นปริภูมิสีที่ไล่จากสว่างไปสว่างมาก ก็ยังควรให้ทุกค่าไบต์แทนชิ้นส่วนของช่วงความสว่างที่มีขนาดเท่ากัน
ถ้าไม้บรรทัดยาวถึง 12 นิ้ว ก็ควรนอร์มัลไลซ์ด้วย ความยาว L ไม่ใช่ด้วยจำนวนจุดบนไม้บรรทัดซึ่งเป็น 13 จุด
>> 8เร็วกว่ามากเป็นบทความที่อ่านสนุกเพราะพูดถึงเรื่องที่ฉันไม่ได้คิดถึงมาสักพักแล้ว ทำให้นึกถึงตอนพัฒนาเกมที่ตรรกะของเกมใช้คณิตศาสตร์แบบ floating-point แต่ พิกเซลอาร์ต ต้องวาดลงบนพิกัดจำนวนเต็ม
ฉันเคยใช้วิธีคล้าย +0.5 ในหลายจุดเพื่อให้ภาพดูแปลกน้อยลง โดยเฉพาะตอนมีกล้องเคลื่อนที่ และตัวกล้องเองก็ต้องถูกตัดเศษด้วย
บทความปี 2002 ของ Jonathan Blow ที่ลิงก์ไว้ด้านล่าง [1] ก็น่าสนใจเหมือนกัน ภาพประกอบในบทความแรกช่วยได้มากเวลาเริ่มลงลึกในประเด็นนี้
[1] https://web.archive.org/web/20240706043551/https://number-no...
ความเห็นจาก Lobste.rs
ถ้ายังไม่ค่อยเป็นธรรมชาติ ลองดูกรณีเสื่อมแบบ 2 บิตก็ได้ เมื่อมีค่า integer ที่เป็นไปได้แค่ 0, 1, 2, 3 แล้วคำนวณการแปลง integer→floating point ทั้งหมด จะเห็นว่าถ้าอยากหลีกเลี่ยงพฤติกรรมประหลาดอย่างสีดำ/สีขาวไม่เป็นดำ/ขาวจริง หรือระยะห่างที่ไม่สม่ำเสมออย่างชัดเจน ค่าที่ได้จะเป็น 0.0, 0.33..., 0.66..., 1.0
ดังนั้นการแปลงกลับจึงเป็นการ คูณด้วย 3 ไม่ใช่ 4(2^2)
การแปลงกลับต้องมี การควอนไทซ์ (ปัดเศษ) และนี่แหละคือจุดสำคัญที่ทำให้สมมาตรถูกทำลาย
ถ้าสร้างไล่ระดับของจำนวนจริงแบบสม่ำเสมอในช่วง 0..=1 แล้วควอนไทซ์เป็น 0, 1, 2, 3 จะเห็นว่าถ้าคูณ 3 ผลลัพธ์จะไม่สม่ำเสมอ หลัง ×3 แล้ว
round()จะทำให้ 1 และ 2 ถูกแทนมากเกินไป ส่วนfloorหรือceilหลัง ×3 จะบีบ 0 หรือ 3 ให้กลายเป็นค่าพิเศษ จนดูเหมือนไล่ระดับนั้นใช้แค่ 3 สีจากทั้งหมด 4 สีตรรกะ
/3กับ×3อาจดูใช้ได้เมื่อแปลงค่าตัวเลขที่แน่นอนแบบไปกลับ แต่ค่าระหว่างกลางได้รับผลจากการเลือกวิธีปัดเศษอย่างมาก และจะสำคัญทันทีที่เริ่มประมวลผลข้อมูลวิธีเดียวที่จะทำให้อัตราส่วนของจำนวนเต็มสม่ำเสมอคือ คูณด้วย (4-ε) แล้วปัดลง ซึ่งเทียบเท่ากับ ×4,
floor(),clamp()ถึงจะให้ความรู้สึกเหมือนมีข้อผิดพลาดแปลก ๆ แบบคลาด 1 หรือคลาด ε แต่ในเชิงสัญชาตญาณมันคือทางแก้ที่ดูดีที่สุดสำหรับผมคำตอบคือ “แน่นอนอยู่แล้ว” ว่าเป็น [0.0..255.0] แต่ดูเหมือนคงไม่ใช่สิ่งที่ชัดเจนสำหรับทุกคน
ในบทความบอกว่าช่วง “ปลายสุด” มีความจุเพียงครึ่งหนึ่งของช่วงอื่น ๆ แต่ผมว่ากรอบแบบนั้นก็ไม่ถูก
ถ้าไม่มีค่าใดอยู่นอก [0..1] การที่มันดูเหมือนช่วงแคบกว่าก็เป็นผลจากการเรนเดอร์เท่านั้น เป็นเพียงเพราะคุณตัดบัคเก็ตโดยอาศัยความรู้ว่าไม่มีค่านอกช่วง
ในทางกลับกัน ถ้ามีค่าที่อยู่นอก [0..1] ช่วงนั้นก็ไม่มีที่สิ้นสุด บทความยอมรับอย่างหลัง แต่ไม่ยอมรับอย่างแรก
ทันทีที่ยอมรับข้อแรก พฤติกรรมที่ถูกต้องก็ดูชัดเจน แต่การที่มีบทความแบบนี้ออกมาเองก็หมายความว่าโดยเชิงวัตถุวิสัยแล้วมันไม่ใช่ปัญหาที่ “ชัดเจน” ขนาดนั้น :D
ถ้า 0..<1 ไปเป็น integer 0 และ 254>..255.0 ไปเป็น integer 255 อย่างนั้น 128 ก็จะหายไป คุณคงอยากให้ 127.5..128.5 ไปเป็น 128 แต่ถ้าอย่างนั้นครึ่งช่วงเหล่านี้ควรไปอยู่ที่ไหน?
ถ้าขยับทั้งระบบนิดหน่อยเพื่อให้ 128 ถูกต้อง ก็จะกลายเป็นว่า 0..0.99609375 ถูกแมปเป็น integer 0
round()กันตามธรรมชาติวิธีนั้นให้ความรู้สึกเป็นธรรมชาติพอสมควรสำหรับผู้คน เลยอาจกลายเป็นมาตรฐานเพราะความเรียบง่าย
pngcrushหรือหมายถึงว่ามีอะไรผิดกับเนื้อหาของภาพ?