Skip to content

Commit 4a3683a

Browse files
tests: add tests for new utils
1 parent 150daf9 commit 4a3683a

File tree

3 files changed

+349
-0
lines changed

3 files changed

+349
-0
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ add_rn_test(FrameProcessorTests unit/FrameProcessorTest.cpp
165165
LIBS opencv_deps android
166166
)
167167

168+
add_rn_test(FrameTransformTests unit/FrameTransformTest.cpp
169+
SOURCES
170+
${RNEXECUTORCH_DIR}/utils/FrameTransform.cpp
171+
LIBS opencv_deps
172+
)
173+
168174
add_rn_test(BaseModelTests integration/BaseModelTest.cpp)
169175

170176
add_rn_test(VisionModelTests integration/VisionModelTest.cpp

packages/react-native-executorch/common/rnexecutorch/tests/run_tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ TEST_EXECUTABLES=(
2020
"LogTests"
2121
"FileUtilsTest"
2222
"ImageProcessingTest"
23+
"FrameTransformTests"
2324
"BaseModelTests"
2425
"ClassificationTests"
2526
"ObjectDetectionTests"
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
#include <gtest/gtest.h>
2+
#include <opencv2/opencv.hpp>
3+
#include <rnexecutorch/utils/FrameTransform.h>
4+
5+
using namespace rnexecutorch::utils;
6+
7+
static FrameOrientation makeOrient(const std::string &o, bool mirrored) {
8+
return {o, mirrored};
9+
}
10+
11+
// ============================================================================
12+
// rotateFrameForModel — rotates sensor-native frame so model sees upright image.
13+
//
14+
// "up" (landscape-left) → no rotation
15+
// "left" (portrait upright) → CW
16+
// "right" (portrait upside-down) → CCW
17+
// "down" (landscape-right) → 180°
18+
// ============================================================================
19+
20+
// "up" → no rotation. 480×640 stays 480×640.
21+
TEST(RotateFrameForModel, Up_NoRotation) {
22+
cv::Mat input(480, 640, CV_8UC3, cv::Scalar(1, 2, 3));
23+
cv::Mat result = rotateFrameForModel(input, makeOrient("up", false));
24+
EXPECT_EQ(result.rows, 480);
25+
EXPECT_EQ(result.cols, 640);
26+
}
27+
28+
// "up" → no rotation. Verify pixel values preserved.
29+
TEST(RotateFrameForModel, Up_PixelsPreserved) {
30+
cv::Mat input(1, 2, CV_8UC3);
31+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0};
32+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255};
33+
cv::Mat result = rotateFrameForModel(input, makeOrient("up", false));
34+
EXPECT_EQ(result.at<cv::Vec3b>(0, 0), (cv::Vec3b{255, 0, 0}));
35+
EXPECT_EQ(result.at<cv::Vec3b>(0, 1), (cv::Vec3b{0, 0, 255}));
36+
}
37+
38+
// "left" → CW. 480×640 becomes 640×480.
39+
TEST(RotateFrameForModel, Left_CW) {
40+
cv::Mat input(480, 640, CV_8UC3, cv::Scalar(1, 2, 3));
41+
cv::Mat result = rotateFrameForModel(input, makeOrient("left", false));
42+
EXPECT_EQ(result.rows, 640);
43+
EXPECT_EQ(result.cols, 480);
44+
}
45+
46+
// "left" → CW pixel check. 1×2 [R, B] → 2×1 [R; B].
47+
// CW takes bottom-of-left-col to top: (0,0)→(0,0), (0,1)→(1,0).
48+
TEST(RotateFrameForModel, Left_CW_Pixels) {
49+
cv::Mat input(1, 2, CV_8UC3);
50+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0}; // R left
51+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255}; // B right
52+
cv::Mat result = rotateFrameForModel(input, makeOrient("left", false));
53+
EXPECT_EQ(result.rows, 2);
54+
EXPECT_EQ(result.cols, 1);
55+
EXPECT_EQ(result.at<cv::Vec3b>(0, 0), (cv::Vec3b{255, 0, 0})); // R
56+
EXPECT_EQ(result.at<cv::Vec3b>(1, 0), (cv::Vec3b{0, 0, 255})); // B
57+
}
58+
59+
// "right" → CCW. 480×640 becomes 640×480.
60+
TEST(RotateFrameForModel, Right_CCW) {
61+
cv::Mat input(480, 640, CV_8UC3, cv::Scalar(1, 2, 3));
62+
cv::Mat result = rotateFrameForModel(input, makeOrient("right", false));
63+
EXPECT_EQ(result.rows, 640);
64+
EXPECT_EQ(result.cols, 480);
65+
}
66+
67+
// "right" → CCW pixel check. 1×2 [R, B] → 2×1 [B; R].
68+
// CCW takes top-of-right-col to top: (0,1)→(0,0), (0,0)→(1,0).
69+
TEST(RotateFrameForModel, Right_CCW_Pixels) {
70+
cv::Mat input(1, 2, CV_8UC3);
71+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0}; // R left
72+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255}; // B right
73+
cv::Mat result = rotateFrameForModel(input, makeOrient("right", false));
74+
EXPECT_EQ(result.rows, 2);
75+
EXPECT_EQ(result.cols, 1);
76+
EXPECT_EQ(result.at<cv::Vec3b>(0, 0), (cv::Vec3b{0, 0, 255})); // B
77+
EXPECT_EQ(result.at<cv::Vec3b>(1, 0), (cv::Vec3b{255, 0, 0})); // R
78+
}
79+
80+
// "down" → 180°. 480×640 stays 480×640.
81+
TEST(RotateFrameForModel, Down_180) {
82+
cv::Mat input(480, 640, CV_8UC3, cv::Scalar(1, 2, 3));
83+
cv::Mat result = rotateFrameForModel(input, makeOrient("down", false));
84+
EXPECT_EQ(result.rows, 480);
85+
EXPECT_EQ(result.cols, 640);
86+
}
87+
88+
// "down" → 180° pixel check. 1×2 [R, B] → 1×2 [B, R].
89+
TEST(RotateFrameForModel, Down_180_Pixels) {
90+
cv::Mat input(1, 2, CV_8UC3);
91+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0};
92+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255};
93+
cv::Mat result = rotateFrameForModel(input, makeOrient("down", false));
94+
EXPECT_EQ(result.at<cv::Vec3b>(0, 0), (cv::Vec3b{0, 0, 255})); // B
95+
EXPECT_EQ(result.at<cv::Vec3b>(0, 1), (cv::Vec3b{255, 0, 0})); // R
96+
}
97+
98+
// isMirrored + "up" → flip only. 1×2 [R, B] → 1×2 [B, R].
99+
TEST(RotateFrameForModel, Mirrored_Up_FlipOnly) {
100+
cv::Mat input(1, 2, CV_8UC3);
101+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0};
102+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255};
103+
cv::Mat result = rotateFrameForModel(input, makeOrient("up", true));
104+
EXPECT_EQ(result.rows, 1);
105+
EXPECT_EQ(result.cols, 2);
106+
EXPECT_EQ(result.at<cv::Vec3b>(0, 0), (cv::Vec3b{0, 0, 255})); // B
107+
EXPECT_EQ(result.at<cv::Vec3b>(0, 1), (cv::Vec3b{255, 0, 0})); // R
108+
}
109+
110+
// isMirrored + "left" → flip then CW.
111+
// 1×2 [R, B] → flip → [B, R] → CW → 2×1 [B; R].
112+
TEST(RotateFrameForModel, Mirrored_Left_FlipThenCW) {
113+
cv::Mat input(1, 2, CV_8UC3);
114+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0};
115+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255};
116+
cv::Mat result = rotateFrameForModel(input, makeOrient("left", true));
117+
EXPECT_EQ(result.rows, 2);
118+
EXPECT_EQ(result.cols, 1);
119+
EXPECT_EQ(result.at<cv::Vec3b>(0, 0), (cv::Vec3b{0, 0, 255})); // B
120+
EXPECT_EQ(result.at<cv::Vec3b>(1, 0), (cv::Vec3b{255, 0, 0})); // R
121+
}
122+
123+
// Does not modify input.
124+
TEST(RotateFrameForModel, DoesNotModifyInput) {
125+
cv::Mat input(1, 2, CV_8UC3);
126+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0};
127+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255};
128+
cv::Mat inputCopy = input.clone();
129+
rotateFrameForModel(input, makeOrient("left", true));
130+
EXPECT_EQ(input.at<cv::Vec3b>(0, 0), inputCopy.at<cv::Vec3b>(0, 0));
131+
EXPECT_EQ(input.at<cv::Vec3b>(0, 1), inputCopy.at<cv::Vec3b>(0, 1));
132+
}
133+
134+
// ============================================================================
135+
// inverseRotateMat — inverse of rotateFrameForModel for matrices.
136+
//
137+
// "up" → CW (undo no-op: landscape→portrait for screen)
138+
// "left" → no-op (already in screen space after CW model rotation)
139+
// "right" → 180° (undo CCW)
140+
// "down" → CCW (undo 180°)
141+
// ============================================================================
142+
143+
// "left" → no-op. Same dims.
144+
TEST(InverseRotateMat, Left_NoOp) {
145+
cv::Mat input(640, 480, CV_8UC3, cv::Scalar(1, 2, 3));
146+
cv::Mat result = inverseRotateMat(input, makeOrient("left", false));
147+
EXPECT_EQ(result.rows, 640);
148+
EXPECT_EQ(result.cols, 480);
149+
}
150+
151+
// "up" → CW. 480×640 becomes 640×480.
152+
TEST(InverseRotateMat, Up_CW) {
153+
cv::Mat input(480, 640, CV_8UC3, cv::Scalar(1, 2, 3));
154+
cv::Mat result = inverseRotateMat(input, makeOrient("up", false));
155+
EXPECT_EQ(result.rows, 640);
156+
EXPECT_EQ(result.cols, 480);
157+
}
158+
159+
// "right" → 180°. Same dims.
160+
TEST(InverseRotateMat, Right_180) {
161+
cv::Mat input(640, 480, CV_8UC3, cv::Scalar(1, 2, 3));
162+
cv::Mat result = inverseRotateMat(input, makeOrient("right", false));
163+
EXPECT_EQ(result.rows, 640);
164+
EXPECT_EQ(result.cols, 480);
165+
}
166+
167+
// "down" → CCW. 480×640 becomes 640×480.
168+
TEST(InverseRotateMat, Down_CCW) {
169+
cv::Mat input(480, 640, CV_8UC3, cv::Scalar(1, 2, 3));
170+
cv::Mat result = inverseRotateMat(input, makeOrient("down", false));
171+
EXPECT_EQ(result.rows, 640);
172+
EXPECT_EQ(result.cols, 480);
173+
}
174+
175+
// Round-trip: rotateFrameForModel then inverseRotateMat restores pixel content.
176+
// Use "left" (CW then no-op for inverse on Android).
177+
TEST(InverseRotateMat, RoundTrip_Left) {
178+
cv::Mat input(2, 3, CV_8UC3);
179+
input.at<cv::Vec3b>(0, 0) = {10, 20, 30};
180+
input.at<cv::Vec3b>(0, 1) = {40, 50, 60};
181+
input.at<cv::Vec3b>(0, 2) = {70, 80, 90};
182+
input.at<cv::Vec3b>(1, 0) = {100, 110, 120};
183+
input.at<cv::Vec3b>(1, 1) = {130, 140, 150};
184+
input.at<cv::Vec3b>(1, 2) = {160, 170, 180};
185+
186+
cv::Mat rotated = rotateFrameForModel(input, makeOrient("left", false));
187+
// "left" → CW: 2×3 → 3×2
188+
EXPECT_EQ(rotated.rows, 3);
189+
EXPECT_EQ(rotated.cols, 2);
190+
191+
cv::Mat restored = inverseRotateMat(rotated, makeOrient("left", false));
192+
// "left" inverse → no-op on Android, so restored is still 3×2.
193+
// The inverse undoes the model rotation to get back to sensor-native layout
194+
// which for "left" means the CW was the model rotation, and inverse is no-op
195+
// because the model output is already in screen orientation.
196+
// On Android: left inverse = no-op, so result stays as rotated.
197+
EXPECT_EQ(restored.rows, 3);
198+
EXPECT_EQ(restored.cols, 2);
199+
}
200+
201+
// Does not modify input.
202+
TEST(InverseRotateMat, DoesNotModifyInput) {
203+
cv::Mat input(1, 2, CV_8UC3);
204+
input.at<cv::Vec3b>(0, 0) = {255, 0, 0};
205+
input.at<cv::Vec3b>(0, 1) = {0, 0, 255};
206+
cv::Mat inputCopy = input.clone();
207+
inverseRotateMat(input, makeOrient("up", false));
208+
EXPECT_EQ(input.at<cv::Vec3b>(0, 0), inputCopy.at<cv::Vec3b>(0, 0));
209+
EXPECT_EQ(input.at<cv::Vec3b>(0, 1), inputCopy.at<cv::Vec3b>(0, 1));
210+
}
211+
212+
// ============================================================================
213+
// inverseRotateBbox — inverse of rotateFrameForModel for axis-aligned bboxes.
214+
//
215+
// rW/rH = rotated frame dimensions (after rotateFrameForModel).
216+
// On Android (no __APPLE__), isMirrored is ignored.
217+
//
218+
// Formulas (same as inverseRotatePoints per-corner, but preserves x1<=x2, y1<=y2):
219+
// "up" → CW: nx1=h-y2, ny1=x1, nx2=h-y1, ny2=x2
220+
// "right" → 180°: nx1=w-x2, ny1=h-y2, nx2=w-x1, ny2=h-y1
221+
// "down" → CCW: nx1=y1, ny1=w-x2, nx2=y2, ny2=w-x1
222+
// "left" → no-op
223+
// ============================================================================
224+
225+
// "left" → no-op. Box unchanged.
226+
TEST(InverseRotateBbox, Left_NoOp) {
227+
float x1 = 10, y1 = 20, x2 = 100, y2 = 200;
228+
inverseRotateBbox(x1, y1, x2, y2, makeOrient("left", false), 640, 480);
229+
EXPECT_FLOAT_EQ(x1, 10);
230+
EXPECT_FLOAT_EQ(y1, 20);
231+
EXPECT_FLOAT_EQ(x2, 100);
232+
EXPECT_FLOAT_EQ(y2, 200);
233+
}
234+
235+
// "up" → CW. rW=640, rH=480. Box (10,20)-(100,200):
236+
// nx1=480-200=280, ny1=10, nx2=480-20=460, ny2=100
237+
TEST(InverseRotateBbox, Up_CW) {
238+
float x1 = 10, y1 = 20, x2 = 100, y2 = 200;
239+
inverseRotateBbox(x1, y1, x2, y2, makeOrient("up", false), 640, 480);
240+
EXPECT_FLOAT_EQ(x1, 280);
241+
EXPECT_FLOAT_EQ(y1, 10);
242+
EXPECT_FLOAT_EQ(x2, 460);
243+
EXPECT_FLOAT_EQ(y2, 100);
244+
}
245+
246+
// "right" → 180°. rW=480, rH=640. Box (10,20)-(100,200):
247+
// nx1=480-100=380, ny1=640-200=440, nx2=480-10=470, ny2=640-20=620
248+
TEST(InverseRotateBbox, Right_180) {
249+
float x1 = 10, y1 = 20, x2 = 100, y2 = 200;
250+
inverseRotateBbox(x1, y1, x2, y2, makeOrient("right", false), 480, 640);
251+
EXPECT_FLOAT_EQ(x1, 380);
252+
EXPECT_FLOAT_EQ(y1, 440);
253+
EXPECT_FLOAT_EQ(x2, 470);
254+
EXPECT_FLOAT_EQ(y2, 620);
255+
}
256+
257+
// "down" → CCW. rW=640, rH=480. Box (10,20)-(100,200):
258+
// nx1=20, ny1=640-100=540, nx2=200, ny2=640-10=630
259+
TEST(InverseRotateBbox, Down_CCW) {
260+
float x1 = 10, y1 = 20, x2 = 100, y2 = 200;
261+
inverseRotateBbox(x1, y1, x2, y2, makeOrient("down", false), 640, 480);
262+
EXPECT_FLOAT_EQ(x1, 20);
263+
EXPECT_FLOAT_EQ(y1, 540);
264+
EXPECT_FLOAT_EQ(x2, 200);
265+
EXPECT_FLOAT_EQ(y2, 630);
266+
}
267+
268+
// Guarantees x1<=x2 and y1<=y2 after transform.
269+
TEST(InverseRotateBbox, OutputOrdered) {
270+
float x1 = 50, y1 = 50, x2 = 150, y2 = 250;
271+
inverseRotateBbox(x1, y1, x2, y2, makeOrient("up", false), 640, 480);
272+
EXPECT_LE(x1, x2);
273+
EXPECT_LE(y1, y2);
274+
}
275+
276+
// ============================================================================
277+
// inverseRotatePoints — inverse of rotateFrameForModel for 4-point bboxes.
278+
//
279+
// Same formulas as inverseRotateBbox but applied per-point (no reordering).
280+
// On Android (no __APPLE__), isMirrored is ignored.
281+
// ============================================================================
282+
283+
struct Pt {
284+
float x;
285+
float y;
286+
};
287+
288+
// "left" → no-op. Points unchanged.
289+
TEST(InverseRotatePoints, Left_NoOp) {
290+
std::array<Pt, 4> pts = {{{10, 20}, {30, 40}, {50, 60}, {70, 80}}};
291+
inverseRotatePoints(pts, makeOrient("left", false), 640, 480);
292+
EXPECT_FLOAT_EQ(pts[0].x, 10);
293+
EXPECT_FLOAT_EQ(pts[0].y, 20);
294+
EXPECT_FLOAT_EQ(pts[1].x, 30);
295+
EXPECT_FLOAT_EQ(pts[1].y, 40);
296+
EXPECT_FLOAT_EQ(pts[2].x, 50);
297+
EXPECT_FLOAT_EQ(pts[2].y, 60);
298+
EXPECT_FLOAT_EQ(pts[3].x, 70);
299+
EXPECT_FLOAT_EQ(pts[3].y, 80);
300+
}
301+
302+
// "up" → CW per point. rW=640, rH=480. pt(10,20): nx=480-20=460, ny=10.
303+
TEST(InverseRotatePoints, Up_CW) {
304+
std::array<Pt, 4> pts = {{{10, 20}, {30, 40}, {50, 60}, {70, 80}}};
305+
inverseRotatePoints(pts, makeOrient("up", false), 640, 480);
306+
EXPECT_FLOAT_EQ(pts[0].x, 460);
307+
EXPECT_FLOAT_EQ(pts[0].y, 10);
308+
EXPECT_FLOAT_EQ(pts[1].x, 440);
309+
EXPECT_FLOAT_EQ(pts[1].y, 30);
310+
EXPECT_FLOAT_EQ(pts[2].x, 420);
311+
EXPECT_FLOAT_EQ(pts[2].y, 50);
312+
EXPECT_FLOAT_EQ(pts[3].x, 400);
313+
EXPECT_FLOAT_EQ(pts[3].y, 70);
314+
}
315+
316+
// "right" → 180° per point. rW=480, rH=640. pt(10,20): nx=480-10=470, ny=640-20=620.
317+
TEST(InverseRotatePoints, Right_180) {
318+
std::array<Pt, 4> pts = {{{10, 20}, {30, 40}, {50, 60}, {70, 80}}};
319+
inverseRotatePoints(pts, makeOrient("right", false), 480, 640);
320+
EXPECT_FLOAT_EQ(pts[0].x, 470);
321+
EXPECT_FLOAT_EQ(pts[0].y, 620);
322+
EXPECT_FLOAT_EQ(pts[1].x, 450);
323+
EXPECT_FLOAT_EQ(pts[1].y, 600);
324+
EXPECT_FLOAT_EQ(pts[2].x, 430);
325+
EXPECT_FLOAT_EQ(pts[2].y, 580);
326+
EXPECT_FLOAT_EQ(pts[3].x, 410);
327+
EXPECT_FLOAT_EQ(pts[3].y, 560);
328+
}
329+
330+
// "down" → CCW per point. rW=640, rH=480. pt(10,20): nx=20, ny=640-10=630.
331+
TEST(InverseRotatePoints, Down_CCW) {
332+
std::array<Pt, 4> pts = {{{10, 20}, {30, 40}, {50, 60}, {70, 80}}};
333+
inverseRotatePoints(pts, makeOrient("down", false), 640, 480);
334+
EXPECT_FLOAT_EQ(pts[0].x, 20);
335+
EXPECT_FLOAT_EQ(pts[0].y, 630);
336+
EXPECT_FLOAT_EQ(pts[1].x, 40);
337+
EXPECT_FLOAT_EQ(pts[1].y, 610);
338+
EXPECT_FLOAT_EQ(pts[2].x, 60);
339+
EXPECT_FLOAT_EQ(pts[2].y, 590);
340+
EXPECT_FLOAT_EQ(pts[3].x, 80);
341+
EXPECT_FLOAT_EQ(pts[3].y, 570);
342+
}

0 commit comments

Comments
 (0)