|
| 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