Skip to content

v-code01/grayu8

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

grayu8

kornia's rgb_to_grayscale on a uint8 image overflows. It multiplies the channels by the 8-bit fixed-point luma weights [76, 150, 29] in native uint8 arithmetic, with no divide by 256 and no wider accumulator, so every product and the accumulation wrap mod 256. The result is the true luminance times 256 taken mod 256, not the luminance, so a white image becomes value 1, near black. The float path returns the correct value.

The code

kornia/color/gray.py, rgb_to_grayscale, current main (identical in 0.8.3):

if image.dtype == torch.uint8:
    rgb_weights = torch.tensor([76, 150, 29], device=image.device, dtype=torch.uint8)
...
out = r * w_r                          # uint8 * uint8, wraps mod 256
out = torch.addcmul(out, g, w_g)
out = torch.addcmul(out, b, w_b)
return out.unsqueeze(-3)

The weights [76, 150, 29] are the luma coefficients [0.299, 0.587, 0.114] in 8-bit fixed-point (each coefficient times 256), and they sum to 255. Using them requires accumulating in a wider integer and dividing by 256, a right shift by 8. The code does neither, so r * w_r is a uint8 by uint8 product that overflows, and the additions overflow again.

What it does

Driving the real kornia conversion, uint8 versus its own float path versus the luma:

rgb=(0, 0, 0):       kornia uint8 =   0, kornia float =    0.0, expected luma = 0.0
rgb=(255, 255, 255): kornia uint8 =   1, kornia float =  255.0, expected luma = 255.0
rgb=(100, 100, 100): kornia uint8 = 156, kornia float =  100.0, expected luma = 100.0
rgb=(200, 100, 50):  kornia uint8 = 162, kornia float =  124.2, expected luma = 124.2
rgb=(255, 0, 0):     kornia uint8 = 180, kornia float =   76.2, expected luma = 76.2

White becomes 1 instead of 255, a mid-gray becomes 156 instead of 100, and pure red becomes 180 instead of 76. The step is exact: for white, 255 * 76 = 19380 = 180 mod 256, then + 255 * 150 = 106 mod 256 -> 30, then + 255 * 29 = 227 mod 256 -> 1. kornia's own float branch returns the correct luma on the same pixels, so only the uint8 branch is wrong. The base case confirms the harness: black (0, 0, 0) maps to 0, a fixed point of the overflow, so the machinery is exercised and only the non-zero pixels reveal the wrap.

Scope

The bug is on current main, and torch.uint8 is an explicit, intentional branch (a dedicated uint8 weight tensor). uint8 is the natural dtype of a freshly loaded image, and rgb_to_grayscale, RgbToGrayscale, and bgr_to_grayscale are public API, so any grayscale of a uint8 image is corrupted. The fix is to accumulate in a wider integer and shift, for example ((r.int() * 76 + g.int() * 150 + b.int() * 29) >> 8).to(torch.uint8).

Layout

  • excerpt.py: the uint8 branch and its fixed-point weights quoted with the two flags (Apache-2.0).
  • gray.py: the uint8-arithmetic conversion (wrapping mod 256) and the correct fixed-point conversion (wide accumulate then shift by 8).
  • consequence.py: the real kornia uint8 grayscale versus its float path versus the luma, the white-to-near-black case, and the black base case.
  • test_grayu8.py: the weights are fixed-point and sum to 255, the uint8 arithmetic wraps white to one, the correct conversion matches the luma, the real kornia uint8 values are far from the luma while the float values match, and the black base case is correct.

Reproduce

python gray.py
python consequence.py
python test_grayu8.py

The branch is quoted from current main; the values are produced by the real kornia rgb_to_grayscale. The fix is to accumulate the fixed-point products in a wider integer and divide by 256.

About

kornia rgb_to_grayscale uint8 branch multiplies by fixed-point weights [76,150,29] in uint8 with no /256, so it overflows mod 256: white (255,255,255) becomes 1 (near-black); float path correct

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages