-
-
Notifications
You must be signed in to change notification settings - Fork 7
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
Add default to bilinear interpolation #6
base: main
Are you sure you want to change the base?
Conversation
First of all thank you for the contributions. can you create a separate PR for the typo fix? I would merge that right away, but the extrapolation behavior needs some more work which we can address in this PR.
yes! But I think it should be named enum OutOfBoundsBehavior<T, D: Dimension>{
/// Return an error
Error,
/// Extrapolate the function
Extrapolate,
/// Return nearest known value
Nearest,
/// Return a default value
Default(T),
// or maybe:
/// Return a default value for each data row
Default(Array<T, D>)
}
maybe we rename the enum BoundsCheck{
InRange,
BelowBounds(usize),
AboveBounds(usize),
} But that also does not solve the jaggies properly, as we need some out of bounds x value at which we assume the default value is reached. so there would be three different cases:
at which value do we transition from 2. to 3.? |
Made a separate PR for the typo fix, see here. For the
Here's how I'm currently solving the jaggies issue. I had to switch away from using this crate for the moment because of this problem: pub fn warp_array3_into(
mapping: &Mapping,
data: &Array3<f32>,
out: &mut Array3<f32>,
background: Option<Array1<f32>>
) {
let [out_c, out_h, out_w] = out.shape() else {
unreachable!("Data is Array3, should have 3 dims!")
};
let [_data_c, data_h, data_w] = data.shape() else {
unreachable!("Data is Array3, should have 3 dims!")
};
// Points is a Nx2 array of xy pairs
let points = Array::from_shape_fn((out_w * out_h, 2), |(i, j)| {
if j == 0 {
i % out_w
} else {
i / out_w
}
});
// Warp all points and determine indices of in-bound ones
let (background, padding) = if background.is_some() {
(background.unwrap(), 1.0)
} else {
(Array1::<f32>::zeros(*out_c), 0.0)
};
let warpd = mapping.warp_points(&points);
let in_range_x = |x: f32| -padding <= x && x <= (*data_w as f32) - 1.0 + padding;
let in_range_y = |y: f32| -padding <= y && y <= (*data_h as f32) - 1.0 + padding;
let left_top = warpd.mapv(|v| v.floor());
let right_bottom = left_top.mapv(|v| v + 1.0);
let right_bottom_weights = &warpd - &left_top;
// Perform interpolation into buffer
let get_pix_or_bkg = |x: f32, y: f32| {
if x < 0f32 || x >= *data_w as f32 || y < 0f32 || y >= *data_h as f32 {
background.to_owned()
} else {
data.slice(s![.., y as i32, x as i32]).to_owned()
}
};
let mut out_view = ArrayViewMut::from_shape(
(*out_c, out_h*out_w),
out.as_slice_mut().unwrap()
).unwrap();
(
out_view.axis_iter_mut(Axis(1)),
points.axis_iter(Axis(0)),
warpd.axis_iter(Axis(0)),
left_top.axis_iter(Axis(0)),
right_bottom.axis_iter(Axis(0)),
right_bottom_weights.axis_iter(Axis(0))
).into_par_iter().for_each(|(mut out_slice, _dst_p, src_p, lt, rb, rb_w)| {
let [src_x, src_y] = src_p.to_vec()[..] else {unreachable!("XY pair has only two values")};
if !in_range_x(src_x) || !in_range_y(src_y) {
return
}
let [l, t] = lt.to_vec()[..] else {unreachable!("XY pair has only two values")};
let [r, b] = rb.to_vec()[..] else {unreachable!("XY pair has only two values")};
let [rw, bw] = rb_w.to_vec()[..] else {unreachable!("XY pair has only two values")};
// Do the interpolation
let (tl, tr, bl, br) = (
get_pix_or_bkg(l, t),
get_pix_or_bkg(r, t),
get_pix_or_bkg(l, b),
get_pix_or_bkg(r, b),
);
let top = (1.0 - rw) * tl + rw * tr;
let bottom = (1.0 - rw) * bl + rw * br;
let value = (1.0 - bw) * top + bw * bottom;
out_slice.assign(&value)
});
} This is probably not the best way of doing this (again, I'm fairly new to rust) and it's more tailored and less general than this crate, but here the "in-bounds" detection actually depends on the The above code works well but is a bit slower than ndarray-interp. I'm not sure where the performance difference comes from or how the above could be improved. Do let me know if you have ideas. |
One more thing, I noticed a few more typos in #5, specifically the changelog you wrote CubicSpline as QubicSpline. Cheers! |
I am not sure if I understand you correctly. All the interpolation functions return
It just uses the last two in bounds values and calculates the linear function with those. So it just extends the last in bounds segment. From what I can tell about your issue I feel like the following is a good approach: |
Agreed, |
I intend to keep the return type as is. While a boolean array can be useful I do not think it should be the default case. Given the ability to set a default value for out-of-bounds query points the user can construct the boolean array himself. and also your problem can be solved without to much work: let original: Array3<f32> = .. ;// the data
let mut padded_dim = original.raw_dim(); // use raw_dim to preserve the type information
padded_dim[0] += 2;
padded_dim[1] += 2;
let mut padded_original = Array::zeros(padded_dim);
// set whatever background color we like
padded_original.slice_mut(s![1..-1, 1..-1, ..]).assign(original);
// we need to set axis manually as the padding changed the coordinates
// the coordinates now go from -1 to n+1
let mut x = Array::linspace(padded_dim[0], -1.0, padded_dim[0] as f32 - 1.0);
let mut y = Array::linspace(padded_dim[1], -1.0, padded_dim[1] as f32 - 1.0);
// by changing the first and last value of the axis we can change how big the padding is
// that changes how fast we fade to black at the edge of the image
x[0] -= 2.0; // fade to black over 3 pixel
x[padded_dim[0] - 1] += 2.0;
let strat = Bilinear::new().extrapolate(OutOfBoundsBehavior::Default(0.0)); // or maybe set a unique sentinel like f32::NAN
let interp = Interp2D::builder(padded_original)
.strategy(strat)
.x(x)
.y(y)
.build()
.unwrap();
let new_x_coords: Array2<f32> = .. ; // transformed x coordinates
let new_y_coords: Array2<f32> = .. ;
let transformed_image = interp.interp_array(&new_x_coords, &new_y_coords); This might not be 100% correct, and it requires this PR to implement some way to set the out-of-bounds behavior. Alternatively we can do it without this PR by setting the padding to two pixels on each side and setting |
This is a standard use case to me, but clearly, I'm biased... The above solution requires allocating a whole new array which might be very costly depending on the size. It would be nice to have this be part of the API. |
I am happy to review Proposals and changes to better facilitate your use-case. As I mentioned above I support the idea of customizing the out of bounds behavior. But I won't be implementing these changes myself.
Does that proposal not solve your problem?
If you are concerned about performance I assume you have thousands of images to process. I am also assuming they all have the same size, or can be batched into batches with the same size.
|
The first commit fixes the typo bilinier to bilinear and can be easily merged in. Although, you might want to add a type alias for backward compatibility and eventually deprecate it.
The second enables the 2d bilinear interpolation strategy to return a default value if the query is out of bounds, like scipy's fill_value. This works as is, but there are a few limitations to the current implementation that I would like your feedback on:
allow_default
boolean flag, meaning that there are some checks to ensure that bothextrapolate
andallow_default
aren't both enabled simultaneously. Maybe aBoundaryConditions
enum would be better?Default::default()
at the moment, but maybe a user would want the default to be something else. I tried enabling this by adding theSd
generic to theBilinear
struct and adding in a new field to capture what the default should be. I'm fairly new to Rust and have not yet been able to figure out all the correct trait bounds needed to pull this off.Vec
s this indirection layer makes this not as trivial as I originally thought. However, for default coordinates (where the index to coord mapping is just a range) this could be done more easily.