8 คะแนน โดย GN⁺ 12 일 전 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อแปลงค่าสีจำนวนเต็ม 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)
  • วิธีมาตรฐานจะแมปจำนวนเต็ม 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 แต่ละค่าไว้ ตรงกึ่งกลางระหว่างจำนวนเต็มสองค่า
      • เนื่องจากไม่รู้ค่าควอนไทซ์เดิม จุดกึ่งกลางของจำนวนเต็มที่ติดกันจึงเป็นการประนีประนอมที่ดีในฐานะค่าประมาณ
    • มีผู้เสนอว่าสะดวกกว่าสำหรับ dithering (บล็อกโพสต์ปี 2015 ของ Andrew Kesler เรื่อง "Converting Color Depth")
      • สามารถเพิ่ม noise ได้โดยไม่ต้องกังวล edge case
      • ในทางกลับกัน ค่าเอ็กซ์ตรีมที่ดูแปลกของสูตรมาตรฐานอาจต้องจัดการอย่างระมัดระวังเพื่อให้การกระจายของ noise สม่ำเสมอ

ควอนไทเซอร์สองชนิด

  • ทั้งสองแนวทางสามารถมองได้ว่าเป็น 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
  • เมื่อนำมาใช้กับสองวิธีนี้
    • สูตรมาตรฐาน = 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 สองแบบมาปะปนกัน เพราะจะได้โค้ดที่พังซึ่งพบได้บ่อย

บทสรุป

  • หากคุณกำลังประมวลผลภาพที่คนอื่นให้มา ควร 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 ความคิดเห็น

 
GN⁺ 11 일 전
ความเห็นจาก 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

    • ตอนเด็ก ๆ จำได้ว่ามองจอ CRT ที่มีนอยส์แล้วเห็น เส้นสีน้ำเงินกับสีเหลืองจาง ๆ ตามขอบภาพ ผมสงสัยมาตลอดว่าทำไมถึงเป็นสองสีนั้นพอดี ถ้าเกิดจากสาเหตุเดียวกัน ก็ถือว่าเพิ่งเข้าใจตอนนี้เอง
    • ยังไม่ได้พูดถึง การแกมมาคอร์เรกชัน ปกติก่อนแปลงค่าจากช่วง 0~255 ให้เป็นแรงดัน PC จะยกค่านั้นกำลัง 2.2 ก่อน
      แบบนี้ความต่างระหว่างค่าน้อยกับค่ามากจะเด่นชัดขึ้นมาก: 2^2.2 = 4.595, 255^2.2 = 196,964.699
    • สำหรับปัญหานี้ temporal dithering ดูจะเป็นทางออกที่ดีที่สุด การทำ delta-sigma modulation รายพิกเซลก็ค่อนข้างทำได้ง่าย
      ถ้าเปลี่ยนที่ 30Hz มนุษย์น่าจะแยกความต่างระหว่างโทนน้ำเงินนิด ๆ กับเหลืองนิด ๆ ได้ยาก
    • แบบนี้เองสินะที่ทำให้ สีแบบ RGBI พบได้บ่อยมากในยุค 80
  • เหตุผลที่สนับสนุน 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 ที่ปลายทั้งสองด้าน

    • เอกสาร ADC ทุกฉบับที่ผมเคยเห็นระบุว่าไม่สามารถแทนค่า positive full-scale ได้ ยกตัวอย่าง ADC 8 บิต ±1V ค่า -128 หมายถึง -1V และ +127 หมายถึง 127/128=0.99219V
      จุดเปลี่ยนจาก 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 บิต หากไม่มีเมทาดาทาจากฟอร์แมตไฟล์ที่เหมาะสมมาช่วยบอกเจตนาในการแทนค่า เราก็จะหลงทางและไม่มีคำตอบที่ถูกต้องเพียงหนึ่งเดียว อย่างที่ผู้เขียนบอก การเลือกวิธีที่ให้ผลดีกว่ากับงานของตัวเองไม่ใช่เรื่องที่ควรถูกตำหนิ แต่ก็บอกได้ว่า บิตที่ไร้บริบท นั้นทำให้ความหมายเสียหาย

    • ทำให้นึกถึงค่านอร์มัลไลซ์ที่ใช้ในการควอนไทซ์ภาพถ่ายดาวเทียม Sentinel-2 level-2 ของ ESA
      โดยคร่าว ๆ จะเป็นแบบนี้: 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 สมัยใหม่เวลาอ้างอิงตำแหน่งการสุ่มตัวอย่าง เพราะตำแหน่งถูกระบุเป็นพิกัด ไม่ใช่ศูนย์กลางพิกเซล

    • API ของ BeOS อิงจากศูนย์กลางพิกเซล แม้ว่าตอนนี้คงไม่มีใครสนใจแล้วก็ตาม
  • ถ้ามองเชิงพีชคณิต คำตอบชัดเจนว่า 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 ไม่จำเป็น

    • จากประสบการณ์ที่ทำงานประมวลผลภาพและเรนเดอร์สำหรับ VFX มาเยอะ ดูเหมือนจะลืมไปว่ามี การแปลงปริภูมิสี ตามมาหลังจากนี้ ใน SDR แบบเก่าก็เช่นแปลงไปเป็น Rec.709 เชิงเส้นจาก sRGB ส่วนฟอร์แมตรุ่นใหม่ก็อาจแปลงไปเป็นช่วงสีกว้างกว่า ดังนั้นการบีบช่วงไดนามิกจะเกิดขึ้นหลังจากโหลดข้อมูลแล้ว
      นอกจากนี้ เวิร์กโฟลว์การประมวลผลภาพและคอมโพสิตจำนวนมาก ไม่ว่าจะถูกหรือผิด ก็มักสมมติว่า 0 หมายถึง 0 ดังนั้นจึงมักถือว่าค่า 0u ใน 8 บิตแมปเป็น 0.0f และ 255 แมปเป็น 1.0f ถ้าค่า 0 ในมาสก์หรืออัลฟ่ากลายเป็นมากกว่า 0.0 เพียงเล็กน้อย ก็อาจเกิดอาร์ติแฟกต์เมื่อโค้ดบางจุดใช้ค่า threshold แบบตายตัวที่ 0.0 เพื่อมาสก์การคำนวณอื่น ๆ ในทางกลับกัน ถ้า 255 ในอัลฟ่าไม่ใช่ 1.0f อีกต่อไป วัตถุหลัง premultiply ก็จะโปร่งใสเล็กน้อย
      เรื่องเดียวกันนี้ก็เกิดได้ถ้า +0.5 ทำให้ 254 กลายเป็น 1.0f ในการมาสก์
    • บทความนี้โฟกัสที่ RGB แต่ ปัญหาการควอนไทซ์ แบบเดียวกันนี้มีอยู่ในสัญญาณทุกชนิดที่ต้องแมประหว่างการแทนค่าแบบไม่ต่อเนื่องกับแบบต่อเนื่อง
      ประเด็นสำคัญไม่ใช่ว่าจะแทน 0 โฟตอนได้หรือไม่ แต่คือจะทำให้ข้อมูลที่เก็บใน 1 ไบต์มีประสิทธิภาพสูงสุดได้อย่างไร ตามอุดมคติแล้วไม่ควรใช้งานค่าไบต์ 0 ให้น้อยลง และไม่ควรเพิ่มไบแอสให้ข้อมูลที่ควรตกอยู่ในบักเก็ตที่ 0 แม้จะเป็นปริภูมิสีที่ไล่จากสว่างไปสว่างมาก ก็ยังควรให้ทุกค่าไบต์แทนชิ้นส่วนของช่วงความสว่างที่มีขนาดเท่ากัน
    • การที่ระบบกระจายภาพในอดีตใช้ช่วงความสว่าง SDR เป็น 16~235 นั่นแหละคือปัญหา น่าเสียดายที่แม้แต่ HDMI ที่ “ทันสมัย” ก็ยังคงติดกับธรรมเนียมประหลาดนี้อยู่ ถ้าจอแสดงผลกับต้นทางไม่ตกลงกัน ภาพก็อาจดูซีดจางหรือ ดำจม
    • ทั้งสองวิธีต่างก็มีการบวก 0.5 ความต่างคือมันเกิดขึ้นตรงไหนของกระบวนการ
    • เป็นแนวคิดที่น่าสนใจ แต่ก็ให้ความรู้สึกเหมือนโลกกำลังสั่นคลอน เพราะสำหรับโปรแกรมประมวลผลแล้ว สีดำแบบเดิม (0.0) กับสีขาว (1.0) จะกลายเป็นสีเทาเข้มมากกับสีเทาสว่างมากแทน
  • ถ้าไม้บรรทัดยาวถึง 12 นิ้ว ก็ควรนอร์มัลไลซ์ด้วย ความยาว L ไม่ใช่ด้วยจำนวนจุดบนไม้บรรทัดซึ่งเป็น 13 จุด

    • อุปมานี้ทำให้งง ไม่แน่ใจว่า “ไม้บรรทัด” หมายถึงไม้บรรทัดยาว 255 นิ้วที่มีจุด 256 จุดกำกับ 0~255 หรือเป็นไม้บรรทัดยาว 256 นิ้วที่มีช่วงละ 1 นิ้วอยู่ 256 ช่วง ทำให้ L = 256×1
    • ถ้าสิ่งที่ตั้งใจจะนับจริง ๆ คือเสารั้ว งั้น fencepost error ก็ไม่ใช่ข้อผิดพลาด
    • ก็จริง แต่ >> 8 เร็วกว่ามาก
    • ใครเป็นคนกำหนดว่าตัวเลขพวกนี้แทนจุดล่ะ มันอาจแทนช่วงระหว่างจุดก็ได้
    • หรือว่าฉันโง่เอง 0 มันไม่ได้เริ่มที่จุดเริ่มต้นหรอกหรือ
  • เป็นบทความที่อ่านสนุกเพราะพูดถึงเรื่องที่ฉันไม่ได้คิดถึงมาสักพักแล้ว ทำให้นึกถึงตอนพัฒนาเกมที่ตรรกะของเกมใช้คณิตศาสตร์แบบ floating-point แต่ พิกเซลอาร์ต ต้องวาดลงบนพิกัดจำนวนเต็ม
    ฉันเคยใช้วิธีคล้าย +0.5 ในหลายจุดเพื่อให้ภาพดูแปลกน้อยลง โดยเฉพาะตอนมีกล้องเคลื่อนที่ และตัวกล้องเองก็ต้องถูกตัดเศษด้วย
    บทความปี 2002 ของ Jonathan Blow ที่ลิงก์ไว้ด้านล่าง [1] ก็น่าสนใจเหมือนกัน ภาพประกอบในบทความแรกช่วยได้มากเวลาเริ่มลงลึกในประเด็นนี้
    [1] https://web.archive.org/web/20240706043551/https://number-no...

 
GN⁺ 12 일 전
ความเห็นจาก Lobste.rs
  • ถึงจะดูเลอะเทอะแต่ถูกต้องแล้ว ค่าที่ถูกต้องคือ 255
    ถ้ายังไม่ค่อยเป็นธรรมชาติ ลองดูกรณีเสื่อมแบบ 2 บิตก็ได้ เมื่อมีค่า integer ที่เป็นไปได้แค่ 0, 1, 2, 3 แล้วคำนวณการแปลง integer→floating point ทั้งหมด จะเห็นว่าถ้าอยากหลีกเลี่ยงพฤติกรรมประหลาดอย่างสีดำ/สีขาวไม่เป็นดำ/ขาวจริง หรือระยะห่างที่ไม่สม่ำเสมออย่างชัดเจน ค่าที่ได้จะเป็น 0.0, 0.33..., 0.66..., 1.0
    ดังนั้นการแปลงกลับจึงเป็นการ คูณด้วย 3 ไม่ใช่ 4(2^2)
    • ช่วงต้นถูกต้อง แต่จากตรงนั้นไม่ได้แปลว่า “ตอนแปลงกลับต้องคูณ 3 ไม่ใช่ 4”
      การแปลงกลับต้องมี การควอนไทซ์ (ปัดเศษ) และนี่แหละคือจุดสำคัญที่ทำให้สมมาตรถูกทำลาย
      ถ้าสร้างไล่ระดับของจำนวนจริงแบบสม่ำเสมอในช่วง 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..1 สอดคล้องกับ [0..255.0] หรือ [0.5..255.5] กันแน่?”
    สำหรับผมคำตอบคือ “แน่นอนอยู่แล้ว” ว่าเป็น [0.0..255.0] แต่ดูเหมือนคงไม่ใช่สิ่งที่ชัดเจนสำหรับทุกคน
    ในบทความบอกว่าช่วง “ปลายสุด” มีความจุเพียงครึ่งหนึ่งของช่วงอื่น ๆ แต่ผมว่ากรอบแบบนั้นก็ไม่ถูก
    ถ้าไม่มีค่าใดอยู่นอก [0..1] การที่มันดูเหมือนช่วงแคบกว่าก็เป็นผลจากการเรนเดอร์เท่านั้น เป็นเพียงเพราะคุณตัดบัคเก็ตโดยอาศัยความรู้ว่าไม่มีค่านอกช่วง
    ในทางกลับกัน ถ้ามีค่าที่อยู่นอก [0..1] ช่วงนั้นก็ไม่มีที่สิ้นสุด บทความยอมรับอย่างหลัง แต่ไม่ยอมรับอย่างแรก
    ทันทีที่ยอมรับข้อแรก พฤติกรรมที่ถูกต้องก็ดูชัดเจน แต่การที่มีบทความแบบนี้ออกมาเองก็หมายความว่าโดยเชิงวัตถุวิสัยแล้วมันไม่ใช่ปัญหาที่ “ชัดเจน” ขนาดนั้น :D
    • ถ้า 0…255.0 เป็นสิ่งที่แน่นอนจริง ๆ แล้ว ช่วงของค่า floating point แบบไหนควรย้อนกลับไปเป็น integer 0 และค่าแบบไหนควรย้อนกลับไปเป็น integer 255?
      ถ้า 0..<1 ไปเป็น integer 0 และ 254>..255.0 ไปเป็น integer 255 อย่างนั้น 128 ก็จะหายไป คุณคงอยากให้ 127.5..128.5 ไปเป็น 128 แต่ถ้าอย่างนั้นครึ่งช่วงเหล่านี้ควรไปอยู่ที่ไหน?
      ถ้าขยับทั้งระบบนิดหน่อยเพื่อให้ 128 ถูกต้อง ก็จะกลายเป็นว่า 0..0.99609375 ถูกแมปเป็น integer 0
  • แนวทางมาตรฐานก็ดูเหมือนจะเกิดจากการที่คนเรียก round() กันตามธรรมชาติ
    วิธีนั้นให้ความรู้สึกเป็นธรรมชาติพอสมควรสำหรับผู้คน เลยอาจกลายเป็นมาตรฐานเพราะความเรียบง่าย
  • สงสัยว่าวิธีตรงข้ามกับสิ่งที่พยายามทำด้วย 256 จะมีประโยชน์ไหม กล่าวคือส่ง 0.0 ไปเป็น 0, 1.0 ไปเป็น 255 และแมปค่า floating point อื่น ๆ ทั้งหมดไปอยู่ระหว่าง 1 ถึง 254
    uint8_t output = 0.0f >= result  
                     ? 0  
                     : 1.0f <= result  
                     ? 255  
                     : 1 + 253*result;  
    
    อยากให้ระหว่างการประมวลผล สีดำยังคงเป็นสีดำ และสีขาวยังคงเป็นสีขาว
    • ถ้าทำแบบนี้ 0 และ 255 จะได้ส่วนแบ่งในช่วงหน่วยมากกว่าตัวเลขอื่น ๆ ประมาณ 0.8% หรือก็คือ 255/253
  • ภาพแรกในเครื่องผมดูเสีย
    • ผู้เขียนบทความเองครับ หมายถึงไฟล์ภาพเสียหรือเปล่า? ผมบีบอัดด้วย pngcrush หรือหมายถึงว่ามีอะไรผิดกับเนื้อหาของภาพ?