Skip to content

Commit 10e089c

Browse files
committed
test: add unit tests for new utility functions
Add comprehensive unit tests for refactoring utilities: - TensorHelpersTest: Test toSpan<T> for Tensor and EValue conversions * Float and int32 tensors * Multidimensional tensors * Empty tensors * Type safety and const correctness - ComputerVisionProcessingTest: Test extractDetectionData * Single and multiple detections * Various indices and label formats * Edge cases (negative coords, fractional values) - FrameTransformTest: Test inverseRotateBboxes batch helper * Batch rotation of multiple detections * Empty containers and single detection * Preservation of non-bbox fields Updated CMakeLists.txt to register new test executables.
1 parent 4cd6d70 commit 10e089c

9 files changed

Lines changed: 379 additions & 183 deletions

File tree

packages/react-native-executorch/common/rnexecutorch/models/BaseModel.h

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -53,84 +53,24 @@ class BaseModel {
5353

5454
std::size_t memorySizeLowerBound{0};
5555

56-
/**
57-
* @brief Ensures the specified method is loaded, unloading any previous
58-
* method if necessary.
59-
*
60-
* This helper is useful for models that support multiple methods with
61-
* different input sizes (e.g., "forward_384", "forward_512", "forward_640").
62-
*
63-
* @param methodName Name of the method to load (e.g., "forward",
64-
* "forward_384").
65-
* @throws RnExecutorchError if the method cannot be loaded or if methodName
66-
* is empty.
67-
*/
56+
/// Loads methodName, unloading any previously loaded method first.
57+
/// Useful for multi-method models (e.g., "forward_384", "forward_640").
6858
void ensureMethodLoaded(const std::string &methodName);
6959

70-
/**
71-
* @brief Validate and get input shape for model
72-
*
73-
* Validates that the model has at least one input tensor and that the first
74-
* input has the minimum required dimensions.
75-
*
76-
* @param methodName Method to get shapes for (default: "forward")
77-
* @param minDimensions Minimum expected dimensions (default: 2)
78-
* @throws RnExecutorchError if validation fails (no inputs or insufficient
79-
* dimensions)
80-
* @return The first input shape vector
81-
*/
8260
std::vector<int32_t>
8361
validateAndGetInputShape(const std::string &methodName = "forward",
8462
size_t minDimensions = 2) const;
8563

86-
/**
87-
* @brief Execute forward and throw on error
88-
*
89-
* Convenience helper that calls forward() and throws RnExecutorchError if
90-
* the result is not ok. Reduces error-checking boilerplate in model
91-
* implementations.
92-
*
93-
* @param input Single input value for the forward method
94-
* @param contextMessage Custom error message (default: generic message)
95-
* @return std::vector<EValue> The successful forward result
96-
* @throws RnExecutorchError if forward fails
97-
*/
9864
std::vector<EValue>
9965
forwardOrThrow(const EValue &input,
10066
const std::string &contextMessage =
10167
"Model forward failed. Ensure input is correct.") const;
10268

103-
/**
104-
* @brief Execute forward with multiple inputs and throw on error
105-
*
106-
* Convenience helper that calls forward() and throws RnExecutorchError if
107-
* the result is not ok. Reduces error-checking boilerplate in model
108-
* implementations.
109-
*
110-
* @param inputs Vector of input values for the forward method
111-
* @param contextMessage Custom error message (default: generic message)
112-
* @return std::vector<EValue> The successful forward result
113-
* @throws RnExecutorchError if forward fails
114-
*/
11569
std::vector<EValue>
11670
forwardOrThrow(const std::vector<EValue> &inputs,
11771
const std::string &contextMessage =
11872
"Model forward failed. Ensure input is correct.") const;
11973

120-
/**
121-
* @brief Execute named method and throw on error
122-
*
123-
* Convenience helper that calls execute() and throws RnExecutorchError if
124-
* the result is not ok. Reduces error-checking boilerplate in model
125-
* implementations.
126-
*
127-
* @param methodName Name of the method to execute
128-
* @param inputs Vector of input values for the method
129-
* @param contextMessage Custom error message (default: auto-generated from
130-
* method name)
131-
* @return std::vector<EValue> The successful execution result
132-
* @throws RnExecutorchError if execution fails
133-
*/
13474
std::vector<EValue>
13575
executeOrThrow(const std::string &methodName,
13676
const std::vector<EValue> &inputs,

packages/react-native-executorch/common/rnexecutorch/models/VisionModel.h

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,11 @@ class VisionModel : public BaseModel {
8585
/// Set once by each subclass constructor to avoid per-frame metadata lookups.
8686
std::vector<int32_t> modelInputShape_;
8787

88-
/// Normalization mean values (RGB channels).
89-
/// Optional: set via initNormalization() for models expecting normalized
90-
/// inputs (e.g., ImageNet preprocessing). Leave as nullopt for models with
91-
/// built-in normalization or raw pixel input expectations.
88+
/// Per-channel normalization mean (RGB). nullopt = no normalization applied.
9289
std::optional<cv::Scalar> normMean_;
9390

94-
/// Normalization standard deviation values (RGB channels).
95-
/// Optional: set via initNormalization() for models expecting normalized
96-
/// inputs (e.g., ImageNet preprocessing). Leave as nullopt for models with
97-
/// built-in normalization or raw pixel input expectations.
91+
/// Per-channel normalization std-dev (RGB). nullopt = no normalization
92+
/// applied.
9893
std::optional<cv::Scalar> normStd_;
9994

10095
/**
@@ -135,57 +130,30 @@ class VisionModel : public BaseModel {
135130
virtual cv::Size modelInputSize() const;
136131

137132
/**
138-
* @brief Get model input spatial dimensions for a specific method.
133+
* @brief Get input size for a specific method (last two shape dims).
139134
*
140135
* Useful for multi-method models with different input sizes per method.
141-
* Returns the last two dimensions of the input shape as cv::Size.
142-
*
143-
* @param methodName Method to query (uses currentlyLoadedMethod_ if empty)
144-
* @return Size (width, height) of the model input for the specified method
145-
* @throws RnExecutorchError if method metadata cannot be retrieved
136+
* Falls back to currentlyLoadedMethod_ when methodName is empty.
146137
*/
147138
cv::Size getModelInputSize(const std::string &methodName = "") const;
148139

149140
/**
150-
* @brief Initialize normalization parameters from vectors
151-
*
152-
* Validates size == 3 and converts to cv::Scalar.
153-
* Logs warning if invalid but non-empty. Sets nullopt if empty/invalid.
141+
* @brief Set normMean_/normStd_ from float vectors.
154142
*
155-
* @param normMean Mean values for RGB channels (expected size: 3)
156-
* @param normStd Standard deviation values for RGB channels (expected size:
157-
* 3)
143+
* Expects size == 3. Logs a warning and ignores if non-empty but wrong size.
158144
*/
159145
void initNormalization(const std::vector<float> &normMean,
160146
const std::vector<float> &normStd);
161147

162-
/**
163-
* @brief Create input tensor from preprocessed image
164-
*
165-
* Applies normalization if normMean_ and normStd_ are set.
166-
*
167-
* @param preprocessed Preprocessed image (resized, RGB format)
168-
* @return TensorPtr ready for model input
169-
*/
148+
/// Builds input tensor from a preprocessed image.
149+
/// Applies normalization if normMean_/normStd_ are set, skips it otherwise.
170150
TensorPtr createInputTensor(const cv::Mat &preprocessed) const;
171151

172-
/**
173-
* @brief Load and convert image from path to RGB format
174-
*
175-
* Common preprocessing: readImage (BGR) → convert to RGB
176-
*
177-
* @param imageSource Path to the image file
178-
* @return cv::Mat in RGB format
179-
*/
152+
/// Reads image from path and converts BGR → RGB.
180153
cv::Mat loadImageToRGB(const std::string &imageSource) const;
181154

182-
/**
183-
* @brief Process camera frame with rotation support
184-
*
185-
* @param runtime JSI runtime
186-
* @param frameData JSI value containing frame data from VisionCamera
187-
* @return Tuple of {rotated RGB frame, orientation info, original size}
188-
*/
155+
/// Extracts a camera frame, applies rotation, and returns
156+
/// {rotated frame, orientation, original size}.
189157
std::tuple<cv::Mat, utils::FrameOrientation, cv::Size>
190158
loadFrameRotated(jsi::Runtime &runtime, const jsi::Value &frameData) const;
191159

packages/react-native-executorch/common/rnexecutorch/tests/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ add_rn_test(FrameTransformTests unit/FrameTransformTest.cpp
171171
LIBS opencv_deps
172172
)
173173

174+
add_rn_test(TensorHelpersTest unit/TensorHelpersTest.cpp)
175+
176+
add_rn_test(ComputerVisionProcessingTest unit/ComputerVisionProcessingTest.cpp
177+
SOURCES
178+
${RNEXECUTORCH_DIR}/utils/computer_vision/Processing.cpp
179+
)
180+
174181
add_rn_test(BaseModelTests integration/BaseModelTest.cpp)
175182

176183
add_rn_test(VisionModelTests integration/VisionModelTest.cpp
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#include <gtest/gtest.h>
2+
#include <rnexecutorch/utils/computer_vision/Processing.h>
3+
#include <rnexecutorch/utils/computer_vision/Types.h>
4+
#include <vector>
5+
6+
using namespace rnexecutorch::utils::computer_vision;
7+
8+
// ============================================================================
9+
// extractDetectionData — Extract bbox, score, label from raw tensor data
10+
// ============================================================================
11+
12+
TEST(ExtractDetectionData, SingleDetection) {
13+
// Format: bboxData = [x1, y1, x2, y2] per detection
14+
// scoresData = [score, label] per detection
15+
std::vector<float> bboxData = {10.0f, 20.0f, 100.0f, 200.0f};
16+
std::vector<float> scoresData = {0.95f, 5.0f};
17+
18+
auto [bbox, score, label] =
19+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
20+
21+
EXPECT_FLOAT_EQ(bbox.x1, 10.0f);
22+
EXPECT_FLOAT_EQ(bbox.y1, 20.0f);
23+
EXPECT_FLOAT_EQ(bbox.x2, 100.0f);
24+
EXPECT_FLOAT_EQ(bbox.y2, 200.0f);
25+
EXPECT_FLOAT_EQ(score, 0.95f);
26+
EXPECT_EQ(label, 5);
27+
}
28+
29+
TEST(ExtractDetectionData, MultipleDetections_FirstIndex) {
30+
std::vector<float> bboxData = {
31+
10.0f, 20.0f, 100.0f, 200.0f, // Detection 0
32+
150.0f, 50.0f, 250.0f, 150.0f, // Detection 1
33+
300.0f, 100.0f, 400.0f, 300.0f // Detection 2
34+
};
35+
std::vector<float> scoresData = {
36+
0.95f, 5.0f, // Detection 0: score, label
37+
0.85f, 3.0f, // Detection 1
38+
0.75f, 12.0f // Detection 2
39+
};
40+
41+
auto [bbox, score, label] =
42+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
43+
44+
EXPECT_FLOAT_EQ(bbox.x1, 10.0f);
45+
EXPECT_FLOAT_EQ(bbox.y1, 20.0f);
46+
EXPECT_FLOAT_EQ(bbox.x2, 100.0f);
47+
EXPECT_FLOAT_EQ(bbox.y2, 200.0f);
48+
EXPECT_FLOAT_EQ(score, 0.95f);
49+
EXPECT_EQ(label, 5);
50+
}
51+
52+
TEST(ExtractDetectionData, MultipleDetections_SecondIndex) {
53+
std::vector<float> bboxData = {
54+
10.0f, 20.0f, 100.0f, 200.0f, // Detection 0
55+
150.0f, 50.0f, 250.0f, 150.0f, // Detection 1
56+
300.0f, 100.0f, 400.0f, 300.0f // Detection 2
57+
};
58+
std::vector<float> scoresData = {
59+
0.95f, 5.0f, // Detection 0
60+
0.85f, 3.0f, // Detection 1
61+
0.75f, 12.0f // Detection 2
62+
};
63+
64+
auto [bbox, score, label] =
65+
extractDetectionData(bboxData.data(), scoresData.data(), 1);
66+
67+
EXPECT_FLOAT_EQ(bbox.x1, 150.0f);
68+
EXPECT_FLOAT_EQ(bbox.y1, 50.0f);
69+
EXPECT_FLOAT_EQ(bbox.x2, 250.0f);
70+
EXPECT_FLOAT_EQ(bbox.y2, 150.0f);
71+
EXPECT_FLOAT_EQ(score, 0.85f);
72+
EXPECT_EQ(label, 3);
73+
}
74+
75+
TEST(ExtractDetectionData, MultipleDetections_ThirdIndex) {
76+
std::vector<float> bboxData = {
77+
10.0f, 20.0f, 100.0f, 200.0f, // Detection 0
78+
150.0f, 50.0f, 250.0f, 150.0f, // Detection 1
79+
300.0f, 100.0f, 400.0f, 300.0f // Detection 2
80+
};
81+
std::vector<float> scoresData = {
82+
0.95f, 5.0f, // Detection 0
83+
0.85f, 3.0f, // Detection 1
84+
0.75f, 12.0f // Detection 2
85+
};
86+
87+
auto [bbox, score, label] =
88+
extractDetectionData(bboxData.data(), scoresData.data(), 2);
89+
90+
EXPECT_FLOAT_EQ(bbox.x1, 300.0f);
91+
EXPECT_FLOAT_EQ(bbox.y1, 100.0f);
92+
EXPECT_FLOAT_EQ(bbox.x2, 400.0f);
93+
EXPECT_FLOAT_EQ(bbox.y2, 300.0f);
94+
EXPECT_FLOAT_EQ(score, 0.75f);
95+
EXPECT_EQ(label, 12);
96+
}
97+
98+
TEST(ExtractDetectionData, LowConfidenceDetection) {
99+
std::vector<float> bboxData = {50.0f, 60.0f, 150.0f, 160.0f};
100+
std::vector<float> scoresData = {0.05f, 1.0f}; // Very low confidence
101+
102+
auto [bbox, score, label] =
103+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
104+
105+
EXPECT_FLOAT_EQ(score, 0.05f);
106+
EXPECT_EQ(label, 1);
107+
}
108+
109+
TEST(ExtractDetectionData, ZeroBasedLabelIndex) {
110+
std::vector<float> bboxData = {0.0f, 0.0f, 100.0f, 100.0f};
111+
std::vector<float> scoresData = {0.9f, 0.0f}; // Label index 0
112+
113+
auto [bbox, score, label] =
114+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
115+
116+
EXPECT_EQ(label, 0);
117+
}
118+
119+
TEST(ExtractDetectionData, LargeLabelIndex) {
120+
std::vector<float> bboxData = {0.0f, 0.0f, 100.0f, 100.0f};
121+
std::vector<float> scoresData = {0.9f, 999.0f}; // Large label index
122+
123+
auto [bbox, score, label] =
124+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
125+
126+
EXPECT_EQ(label, 999);
127+
}
128+
129+
TEST(ExtractDetectionData, FloatToInt32Conversion) {
130+
std::vector<float> bboxData = {0.0f, 0.0f, 100.0f, 100.0f};
131+
std::vector<float> scoresData = {0.9f, 42.7f}; // Float label gets truncated
132+
133+
auto [bbox, score, label] =
134+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
135+
136+
EXPECT_EQ(label, 42); // Should truncate, not round
137+
}
138+
139+
TEST(ExtractDetectionData, NegativeCoordinates) {
140+
std::vector<float> bboxData = {-10.0f, -20.0f, 50.0f, 60.0f};
141+
std::vector<float> scoresData = {0.8f, 2.0f};
142+
143+
auto [bbox, score, label] =
144+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
145+
146+
EXPECT_FLOAT_EQ(bbox.x1, -10.0f);
147+
EXPECT_FLOAT_EQ(bbox.y1, -20.0f);
148+
EXPECT_FLOAT_EQ(bbox.x2, 50.0f);
149+
EXPECT_FLOAT_EQ(bbox.y2, 60.0f);
150+
}
151+
152+
TEST(ExtractDetectionData, FractionalCoordinates) {
153+
std::vector<float> bboxData = {10.5f, 20.75f, 100.25f, 200.9f};
154+
std::vector<float> scoresData = {0.88f, 7.0f};
155+
156+
auto [bbox, score, label] =
157+
extractDetectionData(bboxData.data(), scoresData.data(), 0);
158+
159+
EXPECT_FLOAT_EQ(bbox.x1, 10.5f);
160+
EXPECT_FLOAT_EQ(bbox.y1, 20.75f);
161+
EXPECT_FLOAT_EQ(bbox.x2, 100.25f);
162+
EXPECT_FLOAT_EQ(bbox.y2, 200.9f);
163+
}

0 commit comments

Comments
 (0)