-
Notifications
You must be signed in to change notification settings - Fork 630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
to_luma methods can have floating point issues #1214
Comments
I've come up with two ways to fix this: one that keeps using floating point math, and one that uses integers. The first just rounds the luma value up before converting it to its original type: const SRGB_LUMA: [f32; 3] = [0.2126, 0.7152, 0.0722];
#[inline]
fn rgb_to_luma<T: Primitive>(rgb: &[T]) -> T {
let l = SRGB_LUMA[0] * rgb[0].to_f32().unwrap()
+ SRGB_LUMA[1] * rgb[1].to_f32().unwrap()
+ SRGB_LUMA[2] * rgb[2].to_f32().unwrap();
NumCast::from(l.round()).unwrap()
} The second instead uses integer math, avoiding all the jank that comes with floats as well as running faster (I benchmarked it as running about twice as fast). The only primitives that are used in pixels are const SRGB_LUMA: [u32; 3] = [2126, 7152, 722];
const SRGB_LUMA_DIV: u32 = 10000;
#[inline]
fn rgb_to_luma<T: Primitive>(rgb: &[T]) -> T {
let l = SRGB_LUMA[0] * rgb[0].to_u32().unwrap()
+ SRGB_LUMA[1] * rgb[1].to_u32().unwrap()
+ SRGB_LUMA[2] * rgb[2].to_u32().unwrap();
NumCast::from(l / SRGB_LUMA_DIV).unwrap()
} I'd love feedback on these ideas! |
I don't like the use of floating point multiplication here as well. A fraction based-approach would definitely make the finer details more obvious and avoid these subtle inconsistencies. (I am reminded of how unexpectedly difficult it is to find the mid-point of an interval). It would certainly be possible to do (more) precise arithmetic with floating point here as well. However, considering a) the extra observation that it already is slower than an integer method and b) that there is no color space information and this transformation c) that it was not intended to be 100% precise, there specialized libraries such as |
One problem I can see with the integer idea: The function is templated over arbitrary In the long term this may be solved with specialization, or by implementing the trait only for a small internal set of type parameters, but in the short term both of these are infeasible. The former requires nightly, the latter is not possible in a point release in any case. It's something to keep in mind for the next major release where we plan to address color spaces in some capacity. Which leaves us with the rounding solution or another form of floating point precision correction. |
I'm also unhappy with the float conversion here, but settled for that for the reasons @HeroicKatora mentions to do with needing to support different primitive types and depths in the absence of specialization. If we can find a trick to avoid it for integral types, that'd be great. A problem with the rounding solution for float types: conventionally in images, floats are in [0, 1]. Rounding would binarize the output. |
Back in #720 i provided an Addendum: I'm not sure |
This is an attempt to fix image-rs#1214, using the `Enlargeable` trait to select a suitable type for the calculation in `rgb_to_luma` and `bgr_to_luma`. Integer pixel types will use suitable integer pixel types for the calculation, while floating point pixel types will use floating point types. This PR also provides the `Enlargeable` trait for all `Primitive` types (luckily, `Primitive` is not implemented for 128 bit numeric types).
@CrackedP0t , is the benchmark you used in the repository? I would be interested to see if #1215 is as fast as the code you tested for integer pixel types (and no slower than the current code for floating-point pixel types). |
Here's a Gist with the code I used With this benchmark, the old float method takes ~70 ms, and both the int and enlarge methods take ~20 ms, with the enlarge method usually running a few ms faster. |
This is an attempt to fix image-rs#1214, using the `Enlargeable` trait to select a suitable type for the calculation in `rgb_to_luma` and `bgr_to_luma`. Integer pixel types will use suitable integer pixel types for the calculation, while floating point pixel types will use floating point types. This PR also provides the `Enlargeable` trait for all `Primitive` types (luckily, `Primitive` is not implemented for 128 bit numeric types). The `Color` implementation of all library-defined structs is then restricted to a subset of Primitive channel types. This is necessary to allow its conversion operations to utilize the private `Enlargeable`. Notably however we _relax_ the type itself to not utilize any trait bound. It's a simple wrapper around an array now.
In the
rgb_to_luma
andbgr_to_luma
methods, the floating point arithmetic can result in incorrect results. For example:This is because in
rgb_to_luma
,l
is equal to12.99999904632568359375
before being converted back to au8
.The text was updated successfully, but these errors were encountered: