Skip to content

Add ResizeBilinear operator#9

Open
federico-pizz wants to merge 2 commits into
matteocarnelos:mainfrom
federico-pizz:resize-bilinear
Open

Add ResizeBilinear operator#9
federico-pizz wants to merge 2 commits into
matteocarnelos:mainfrom
federico-pizz:resize-bilinear

Conversation

@federico-pizz

Copy link
Copy Markdown

Added support for the resize bilinear tflite operator. Also remove the warning thrown.

Copilot AI review requested due to automatic review settings December 28, 2025 18:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for the ResizeBilinear TFLite operator, enabling bilinear image resizing with support for quantized tensors (i8 and u8). The implementation includes proper handling of alignment options (align_corners and half_pixel_centers) and requantization logic.

  • Implements bilinear interpolation algorithm with coordinate mapping and boundary clamping
  • Adds operator parsing and code generation in the macro system
  • Includes comprehensive test coverage for upscaling, downscaling, and different alignment modes

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ops/resize_bilinear.rs Core implementation of the resize_bilinear operator with bilinear interpolation algorithm and comprehensive tests
src/ops/mod.rs Exports the new resize_bilinear module
microflow-macros/src/ops/resize_bilinear.rs Macro implementation for parsing TFLite ResizeBilinear operators and generating Rust code
microflow-macros/src/ops/mod.rs Declares the resize_bilinear module in the macro crate
microflow-macros/src/lib.rs Integrates ResizeBilinear operator handling into the model macro
microflow-macros/flatbuffers/tflite_generated.rs Adds explicit lifetime annotations to Model return types in flatbuffers-generated code

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +85 to +93
// Validate for NaN/infinity and use safe conversion
let clamped = if requantized.is_nan() || requantized.is_infinite() {
0.0f32 // Default to zero for invalid results
} else {
requantized
};

// Use safe conversion with fallback to zero
out_channels[i] = T::from_superset(&clamped).unwrap_or_else(|| T::from_superset_unchecked(&0.0f32));

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling here appears more defensive than other operators in the codebase. Lines 86-90 handle NaN/infinity by setting to 0.0f32, and then line 93 uses from_superset with unwrap_or_else for safe conversion. Other operators (conv_2d, average_pool_2d, depthwise_conv_2d) simply use from_superset_unchecked after roundf. Consider simplifying this to match the pattern used elsewhere in the codebase for consistency, unless there's a specific reason resize_bilinear needs extra safety checks.

Suggested change
// Validate for NaN/infinity and use safe conversion
let clamped = if requantized.is_nan() || requantized.is_infinite() {
0.0f32 // Default to zero for invalid results
} else {
requantized
};
// Use safe conversion with fallback to zero
out_channels[i] = T::from_superset(&clamped).unwrap_or_else(|| T::from_superset_unchecked(&0.0f32));
// Convert to the quantized type, matching behavior of other operators
out_channels[i] = T::from_superset_unchecked(&requantized);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The from_superset_unchecked() function already implements some safe fallbacks for these edge cases (as you can see from this playground example). Unless you specifically want to fallback to zero in all cases, consider simplifying the validations as suggested by Copilot.

#[cfg(test)]
mod tests {
use super::*;
use quote::ToTokens;

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing import for TokenBuffer4D. The test uses TokenBuffer4D::new() on line 142 but does not import it. Based on the pattern in other operator test modules (average_pool_2d.rs, conv_2d.rs, depthwise_conv_2d.rs), the import should be added.

Suggested change
use quote::ToTokens;
use quote::ToTokens;
use crate::tensor::TokenBuffer4D;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, this needs to be fixed as it causes the test compilation to fail

Comment on lines +55 to +67
// Squeeze the coordinates to be within the input tensor's bounds
let in_r_squeezed = in_r.max(0.).min(INPUT_ROWS as f32 - 1.);
let in_c_squeezed = in_c.max(0.).min(INPUT_COLS as f32 - 1.);

// Get the four nearest neighbors
let r1 = floorf(in_r_squeezed) as usize;
let c1 = floorf(in_c_squeezed) as usize;
let r2 = (r1 + 1).min(INPUT_ROWS - 1);
let c2 = (c1 + 1).min(INPUT_COLS - 1);

// Calculate the interpolation weights
let yr = in_r_squeezed - r1 as f32;
let xr = in_c_squeezed - c1 as f32;

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term "squeezed" is misleading here. These coordinates are being clamped or bounded to the valid input range, not "squeezed" in the mathematical sense. Consider using more precise terminology like "clamped" which better describes the min/max bounding operation.

Suggested change
// Squeeze the coordinates to be within the input tensor's bounds
let in_r_squeezed = in_r.max(0.).min(INPUT_ROWS as f32 - 1.);
let in_c_squeezed = in_c.max(0.).min(INPUT_COLS as f32 - 1.);
// Get the four nearest neighbors
let r1 = floorf(in_r_squeezed) as usize;
let c1 = floorf(in_c_squeezed) as usize;
let r2 = (r1 + 1).min(INPUT_ROWS - 1);
let c2 = (c1 + 1).min(INPUT_COLS - 1);
// Calculate the interpolation weights
let yr = in_r_squeezed - r1 as f32;
let xr = in_c_squeezed - c1 as f32;
// Clamp the coordinates to be within the input tensor's bounds
let in_r_clamped = in_r.max(0.).min(INPUT_ROWS as f32 - 1.);
let in_c_clamped = in_c.max(0.).min(INPUT_COLS as f32 - 1.);
// Get the four nearest neighbors
let r1 = floorf(in_r_clamped) as usize;
let c1 = floorf(in_c_clamped) as usize;
let r2 = (r1 + 1).min(INPUT_ROWS - 1);
let c2 = (c1 + 1).min(INPUT_COLS - 1);
// Calculate the interpolation weights
let yr = in_r_clamped - r1 as f32;
let xr = in_c_clamped - c1 as f32;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, clamped would be a better name imo

@matteocarnelos
matteocarnelos self-requested a review April 25, 2026 21:23
@matteocarnelos matteocarnelos added the enhancement New feature or request label Apr 25, 2026
@matteocarnelos matteocarnelos changed the title Resize bilinear operator added Add ResizeBilinear operator Apr 25, 2026

@matteocarnelos matteocarnelos left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the contribution! And sorry for the delay in the review.

The addition of this operator seems really useful, however, there are some points I highlighted in the code comments. Also, please make sure that cargo test, cargo clippy, and cargo fmt all pass before we can merge this PR.

Comment on lines +56 to +57
let in_r_squeezed = in_r.max(0.).min(INPUT_ROWS as f32 - 1.);
let in_c_squeezed = in_c.max(0.).min(INPUT_COLS as f32 - 1.);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered using the dedicated clamp() function?

Suggested change
let in_r_squeezed = in_r.max(0.).min(INPUT_ROWS as f32 - 1.);
let in_c_squeezed = in_c.max(0.).min(INPUT_COLS as f32 - 1.);
let in_r_squeezed = in_r.clamp(0., INPUT_ROWS as f32 - 1.);
let in_c_squeezed = in_c.clamp(0., INPUT_COLS as f32 - 1.);

Comment on lines +55 to +67
// Squeeze the coordinates to be within the input tensor's bounds
let in_r_squeezed = in_r.max(0.).min(INPUT_ROWS as f32 - 1.);
let in_c_squeezed = in_c.max(0.).min(INPUT_COLS as f32 - 1.);

// Get the four nearest neighbors
let r1 = floorf(in_r_squeezed) as usize;
let c1 = floorf(in_c_squeezed) as usize;
let r2 = (r1 + 1).min(INPUT_ROWS - 1);
let c2 = (c1 + 1).min(INPUT_COLS - 1);

// Calculate the interpolation weights
let yr = in_r_squeezed - r1 as f32;
let xr = in_c_squeezed - c1 as f32;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, clamped would be a better name imo

Comment on lines +85 to +93
// Validate for NaN/infinity and use safe conversion
let clamped = if requantized.is_nan() || requantized.is_infinite() {
0.0f32 // Default to zero for invalid results
} else {
requantized
};

// Use safe conversion with fallback to zero
out_channels[i] = T::from_superset(&clamped).unwrap_or_else(|| T::from_superset_unchecked(&0.0f32));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The from_superset_unchecked() function already implements some safe fallbacks for these edge cases (as you can see from this playground example). Unless you specifically want to fallback to zero in all cases, consider simplifying the validations as suggested by Copilot.

const CONSTANTS: (f32, f32) = (1.0, 0.0);

#[test]
fn test_upscaling() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I tend to avoid prepending test_ to test functions as it's quite obvious from the surrounding context and might get wordy when running cargo test. Moreover, consider using the same verb form for all test names:

Suggested change
fn test_upscaling() {
fn upscale() {

}

#[test]
fn test_downscaling() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn test_downscaling() {
fn downscale() {

}

let input_tensor = tensors.get(inputs.get(0) as usize);
let _size_tensor = tensors.get(inputs.get(1) as usize); // Size tensor (target dimensions)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Is this explicitly-ignored variable needed? If not, consider removing it:

Suggested change
let _size_tensor = tensors.get(inputs.get(1) as usize); // Size tensor (target dimensions)

#[cfg(test)]
mod tests {
use super::*;
use quote::ToTokens;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, this needs to be fixed as it causes the test compilation to fail

use quote::ToTokens;

#[test]
fn test_to_tokens_i8() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn test_to_tokens_i8() {
fn to_tokens_i8() {

};
assert_eq!(op.to_token_stream().to_string(), expected.to_string());
}
} No newline at end of file

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Comment on lines +51 to +52
_buffers: Vector<ForwardsUOffset<Buffer>>,
_index: usize,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Are these explicitly-ignored arguments needed? If not, consider removing them:

Suggested change
_buffers: Vector<ForwardsUOffset<Buffer>>,
_index: usize,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants