You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

648 lines
21KB

  1. /*
  2. * This file is part of FFmpeg.
  3. *
  4. * FFmpeg is free software; you can redistribute it and/or
  5. * modify it under the terms of the GNU Lesser General Public
  6. * License as published by the Free Software Foundation; either
  7. * version 2.1 of the License, or (at your option) any later version.
  8. *
  9. * FFmpeg is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. * Lesser General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Lesser General Public
  15. * License along with FFmpeg; if not, write to the Free Software
  16. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  17. *
  18. * Copyright (C) 2000, Intel Corporation, all rights reserved.
  19. * Copyright (C) 2013, OpenCV Foundation, all rights reserved.
  20. * Third party copyrights are property of their respective owners.
  21. *
  22. * Redistribution and use in source and binary forms, with or without modification,
  23. * are permitted provided that the following conditions are met:
  24. *
  25. * * Redistribution's of source code must retain the above copyright notice,
  26. * this list of conditions and the following disclaimer.
  27. *
  28. * * Redistribution's in binary form must reproduce the above copyright notice,
  29. * this list of conditions and the following disclaimer in the documentation
  30. * and/or other materials provided with the distribution.
  31. *
  32. * * The name of the copyright holders may not be used to endorse or promote products
  33. * derived from this software without specific prior written permission.
  34. *
  35. * This software is provided by the copyright holders and contributors "as is" and
  36. * any express or implied warranties, including, but not limited to, the implied
  37. * warranties of merchantability and fitness for a particular purpose are disclaimed.
  38. * In no event shall the Intel Corporation or contributors be liable for any direct,
  39. * indirect, incidental, special, exemplary, or consequential damages
  40. * (including, but not limited to, procurement of substitute goods or services;
  41. * loss of use, data, or profits; or business interruption) however caused
  42. * and on any theory of liability, whether in contract, strict liability,
  43. * or tort (including negligence or otherwise) arising in any way out of
  44. * the use of this software, even if advised of the possibility of such damage.
  45. */
  46. #define HARRIS_THRESHOLD 3.0f
  47. // Block size over which to compute harris response
  48. //
  49. // Note that changing this will require fiddling with the local array sizes in
  50. // harris_response
  51. #define HARRIS_RADIUS 2
  52. #define DISTANCE_THRESHOLD 80
  53. // Sub-pixel refinement window for feature points
  54. #define REFINE_WIN_HALF_W 5
  55. #define REFINE_WIN_HALF_H 5
  56. #define REFINE_WIN_W 11 // REFINE_WIN_HALF_W * 2 + 1
  57. #define REFINE_WIN_H 11
  58. // Non-maximum suppression window size
  59. #define NONMAX_WIN 30
  60. #define NONMAX_WIN_HALF 15 // NONMAX_WIN / 2
  61. typedef struct PointPair {
  62. // Previous frame
  63. float2 p1;
  64. // Current frame
  65. float2 p2;
  66. } PointPair;
  67. typedef struct SmoothedPointPair {
  68. // Non-smoothed point in current frame
  69. int2 p1;
  70. // Smoothed point in current frame
  71. float2 p2;
  72. } SmoothedPointPair;
  73. typedef struct MotionVector {
  74. PointPair p;
  75. // Used to mark vectors as potential outliers
  76. int should_consider;
  77. } MotionVector;
  78. const sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE |
  79. CLK_ADDRESS_CLAMP_TO_EDGE |
  80. CLK_FILTER_NEAREST;
  81. const sampler_t sampler_linear = CLK_NORMALIZED_COORDS_FALSE |
  82. CLK_ADDRESS_CLAMP_TO_EDGE |
  83. CLK_FILTER_LINEAR;
  84. const sampler_t sampler_linear_mirror = CLK_NORMALIZED_COORDS_TRUE |
  85. CLK_ADDRESS_MIRRORED_REPEAT |
  86. CLK_FILTER_LINEAR;
  87. // Writes to a 1D array at loc, treating it as a 2D array with the same
  88. // dimensions as the global work size.
  89. static void write_to_1d_arrf(__global float *buf, int2 loc, float val) {
  90. buf[loc.x + loc.y * get_global_size(0)] = val;
  91. }
  92. static void write_to_1d_arrul8(__global ulong8 *buf, int2 loc, ulong8 val) {
  93. buf[loc.x + loc.y * get_global_size(0)] = val;
  94. }
  95. static void write_to_1d_arrvec(__global MotionVector *buf, int2 loc, MotionVector val) {
  96. buf[loc.x + loc.y * get_global_size(0)] = val;
  97. }
  98. static void write_to_1d_arrf2(__global float2 *buf, int2 loc, float2 val) {
  99. buf[loc.x + loc.y * get_global_size(0)] = val;
  100. }
  101. static ulong8 read_from_1d_arrul8(__global const ulong8 *buf, int2 loc) {
  102. return buf[loc.x + loc.y * get_global_size(0)];
  103. }
  104. static float2 read_from_1d_arrf2(__global const float2 *buf, int2 loc) {
  105. return buf[loc.x + loc.y * get_global_size(0)];
  106. }
  107. // Returns the grayscale value at the given point.
  108. static float pixel_grayscale(__read_only image2d_t src, int2 loc) {
  109. float4 pixel = read_imagef(src, sampler, loc);
  110. return (pixel.x + pixel.y + pixel.z) / 3.0f;
  111. }
  112. static float convolve(
  113. __local const float *grayscale,
  114. int local_idx_x,
  115. int local_idx_y,
  116. float mask[3][3]
  117. ) {
  118. float ret = 0;
  119. // These loops touch each pixel surrounding loc as well as loc itself
  120. for (int i = 1, i2 = 0; i >= -1; --i, ++i2) {
  121. for (int j = -1, j2 = 0; j <= 1; ++j, ++j2) {
  122. ret += mask[i2][j2] * grayscale[(local_idx_x + 3 + j) + (local_idx_y + 3 + i) * 14];
  123. }
  124. }
  125. return ret;
  126. }
  127. // Sums dx * dy for all pixels within radius of loc
  128. static float sum_deriv_prod(
  129. __local const float *grayscale,
  130. float mask_x[3][3],
  131. float mask_y[3][3]
  132. ) {
  133. float ret = 0;
  134. for (int i = HARRIS_RADIUS; i >= -HARRIS_RADIUS; --i) {
  135. for (int j = -HARRIS_RADIUS; j <= HARRIS_RADIUS; ++j) {
  136. ret += convolve(grayscale, get_local_id(0) + j, get_local_id(1) + i, mask_x) *
  137. convolve(grayscale, get_local_id(0) + j, get_local_id(1) + i, mask_y);
  138. }
  139. }
  140. return ret;
  141. }
  142. // Sums d<>^2 (determined by mask) for all pixels within radius of loc
  143. static float sum_deriv_pow(__local const float *grayscale, float mask[3][3])
  144. {
  145. float ret = 0;
  146. for (int i = HARRIS_RADIUS; i >= -HARRIS_RADIUS; --i) {
  147. for (int j = -HARRIS_RADIUS; j <= HARRIS_RADIUS; ++j) {
  148. float deriv = convolve(grayscale, get_local_id(0) + j, get_local_id(1) + i, mask);
  149. ret += deriv * deriv;
  150. }
  151. }
  152. return ret;
  153. }
  154. // Fills a box with the given radius and pixel around loc
  155. static void draw_box(__write_only image2d_t dst, int2 loc, float4 pixel, int radius)
  156. {
  157. for (int i = -radius; i <= radius; ++i) {
  158. for (int j = -radius; j <= radius; ++j) {
  159. write_imagef(
  160. dst,
  161. (int2)(
  162. // Clamp to avoid writing outside image bounds
  163. clamp(loc.x + i, 0, get_image_dim(dst).x - 1),
  164. clamp(loc.y + j, 0, get_image_dim(dst).y - 1)
  165. ),
  166. pixel
  167. );
  168. }
  169. }
  170. }
  171. // Converts the src image to grayscale
  172. __kernel void grayscale(
  173. __read_only image2d_t src,
  174. __write_only image2d_t grayscale
  175. ) {
  176. int2 loc = (int2)(get_global_id(0), get_global_id(1));
  177. write_imagef(grayscale, loc, (float4)(pixel_grayscale(src, loc), 0.0f, 0.0f, 1.0f));
  178. }
  179. // This kernel computes the harris response for the given grayscale src image
  180. // within the given radius and writes it to harris_buf
  181. __kernel void harris_response(
  182. __read_only image2d_t grayscale,
  183. __global float *harris_buf
  184. ) {
  185. int2 loc = (int2)(get_global_id(0), get_global_id(1));
  186. if (loc.x > get_image_width(grayscale) - 1 || loc.y > get_image_height(grayscale) - 1) {
  187. write_to_1d_arrf(harris_buf, loc, 0);
  188. return;
  189. }
  190. float scale = 1.0f / ((1 << 2) * HARRIS_RADIUS * 255.0f);
  191. float sobel_mask_x[3][3] = {
  192. {-1, 0, 1},
  193. {-2, 0, 2},
  194. {-1, 0, 1}
  195. };
  196. float sobel_mask_y[3][3] = {
  197. { 1, 2, 1},
  198. { 0, 0, 0},
  199. {-1, -2, -1}
  200. };
  201. // 8 x 8 local work + 3 pixels around each side (needed to accomodate for the
  202. // block size radius of 2)
  203. __local float grayscale_data[196];
  204. int idx = get_group_id(0) * get_local_size(0);
  205. int idy = get_group_id(1) * get_local_size(1);
  206. for (int i = idy - 3, it = 0; i < idy + (int)get_local_size(1) + 3; i++, it++) {
  207. for (int j = idx - 3, jt = 0; j < idx + (int)get_local_size(0) + 3; j++, jt++) {
  208. grayscale_data[jt + it * 14] = read_imagef(grayscale, sampler, (int2)(j, i)).x;
  209. }
  210. }
  211. barrier(CLK_LOCAL_MEM_FENCE);
  212. float sumdxdy = sum_deriv_prod(grayscale_data, sobel_mask_x, sobel_mask_y);
  213. float sumdx2 = sum_deriv_pow(grayscale_data, sobel_mask_x);
  214. float sumdy2 = sum_deriv_pow(grayscale_data, sobel_mask_y);
  215. float trace = sumdx2 + sumdy2;
  216. // r = det(M) - k(trace(M))^2
  217. // k usually between 0.04 to 0.06
  218. float r = (sumdx2 * sumdy2 - sumdxdy * sumdxdy) - 0.04f * (trace * trace) * pown(scale, 4);
  219. // Threshold the r value
  220. harris_buf[loc.x + loc.y * get_image_width(grayscale)] = r * step(HARRIS_THRESHOLD, r);
  221. }
  222. // Gets a patch centered around a float coordinate from a grayscale image using
  223. // bilinear interpolation
  224. static void get_rect_sub_pix(
  225. __read_only image2d_t grayscale,
  226. float *buffer,
  227. int size_x,
  228. int size_y,
  229. float2 center
  230. ) {
  231. float2 offset = ((float2)(size_x, size_y) - 1.0f) * 0.5f;
  232. for (int i = 0; i < size_y; i++) {
  233. for (int j = 0; j < size_x; j++) {
  234. buffer[i * size_x + j] = read_imagef(
  235. grayscale,
  236. sampler_linear,
  237. (float2)(j, i) + center - offset
  238. ).x * 255.0f;
  239. }
  240. }
  241. }
  242. // Refines detected features at a sub-pixel level
  243. //
  244. // This function is ported from OpenCV
  245. static float2 corner_sub_pix(
  246. __read_only image2d_t grayscale,
  247. float2 feature,
  248. float *mask
  249. ) {
  250. float2 init = feature;
  251. int src_width = get_global_size(0);
  252. int src_height = get_global_size(1);
  253. const int max_iters = 40;
  254. const float eps = 0.001f * 0.001f;
  255. int i, j, k;
  256. int iter = 0;
  257. float err = 0;
  258. float subpix[(REFINE_WIN_W + 2) * (REFINE_WIN_H + 2)];
  259. const float flt_epsilon = 0x1.0p-23f;
  260. do {
  261. float2 feature_tmp;
  262. float a = 0, b = 0, c = 0, bb1 = 0, bb2 = 0;
  263. get_rect_sub_pix(grayscale, subpix, REFINE_WIN_W + 2, REFINE_WIN_H + 2, feature);
  264. float *subpix_ptr = subpix;
  265. subpix_ptr += REFINE_WIN_W + 2 + 1;
  266. // process gradient
  267. for (i = 0, k = 0; i < REFINE_WIN_H; i++, subpix_ptr += REFINE_WIN_W + 2) {
  268. float py = i - REFINE_WIN_HALF_H;
  269. for (j = 0; j < REFINE_WIN_W; j++, k++) {
  270. float m = mask[k];
  271. float tgx = subpix_ptr[j + 1] - subpix_ptr[j - 1];
  272. float tgy = subpix_ptr[j + REFINE_WIN_W + 2] - subpix_ptr[j - REFINE_WIN_W - 2];
  273. float gxx = tgx * tgx * m;
  274. float gxy = tgx * tgy * m;
  275. float gyy = tgy * tgy * m;
  276. float px = j - REFINE_WIN_HALF_W;
  277. a += gxx;
  278. b += gxy;
  279. c += gyy;
  280. bb1 += gxx * px + gxy * py;
  281. bb2 += gxy * px + gyy * py;
  282. }
  283. }
  284. float det = a * c - b * b;
  285. if (fabs(det) <= flt_epsilon * flt_epsilon) {
  286. break;
  287. }
  288. // 2x2 matrix inversion
  289. float scale = 1.0f / det;
  290. feature_tmp.x = (float)(feature.x + (c * scale * bb1) - (b * scale * bb2));
  291. feature_tmp.y = (float)(feature.y - (b * scale * bb1) + (a * scale * bb2));
  292. err = dot(feature_tmp - feature, feature_tmp - feature);
  293. feature = feature_tmp;
  294. if (feature.x < 0 || feature.x >= src_width || feature.y < 0 || feature.y >= src_height) {
  295. break;
  296. }
  297. } while (++iter < max_iters && err > eps);
  298. // Make sure new point isn't too far from the initial point (indicates poor convergence)
  299. if (fabs(feature.x - init.x) > REFINE_WIN_HALF_W || fabs(feature.y - init.y) > REFINE_WIN_HALF_H) {
  300. feature = init;
  301. }
  302. return feature;
  303. }
  304. // Performs non-maximum suppression on the harris response and writes the resulting
  305. // feature locations to refined_features.
  306. //
  307. // Assumes that refined_features and the global work sizes are set up such that the image
  308. // is split up into a grid of 32x32 blocks where each block has a single slot in the
  309. // refined_features buffer. This kernel finds the best corner in each block (if the
  310. // block has any) and writes it to the corresponding slot in the buffer.
  311. //
  312. // If subpixel_refine is true, the features are additionally refined at a sub-pixel
  313. // level for increased precision.
  314. __kernel void refine_features(
  315. __read_only image2d_t grayscale,
  316. __global const float *harris_buf,
  317. __global float2 *refined_features,
  318. int subpixel_refine
  319. ) {
  320. int2 loc = (int2)(get_global_id(0), get_global_id(1));
  321. // The location in the grayscale buffer rather than the compacted grid
  322. int2 loc_i = (int2)(loc.x * 32, loc.y * 32);
  323. float new_val;
  324. float max_val = 0;
  325. float2 loc_max = (float2)(-1, -1);
  326. int end_x = min(loc_i.x + 32, (int)get_image_dim(grayscale).x - 1);
  327. int end_y = min(loc_i.y + 32, (int)get_image_dim(grayscale).y - 1);
  328. for (int i = loc_i.x; i < end_x; ++i) {
  329. for (int j = loc_i.y; j < end_y; ++j) {
  330. new_val = harris_buf[i + j * get_image_dim(grayscale).x];
  331. if (new_val > max_val) {
  332. max_val = new_val;
  333. loc_max = (float2)(i, j);
  334. }
  335. }
  336. }
  337. if (max_val == 0) {
  338. // There are no features in this part of the frame
  339. write_to_1d_arrf2(refined_features, loc, loc_max);
  340. return;
  341. }
  342. if (subpixel_refine) {
  343. float mask[REFINE_WIN_H * REFINE_WIN_W];
  344. for (int i = 0; i < REFINE_WIN_H; i++) {
  345. float y = (float)(i - REFINE_WIN_HALF_H) / REFINE_WIN_HALF_H;
  346. float vy = exp(-y * y);
  347. for (int j = 0; j < REFINE_WIN_W; j++) {
  348. float x = (float)(j - REFINE_WIN_HALF_W) / REFINE_WIN_HALF_W;
  349. mask[i * REFINE_WIN_W + j] = (float)(vy * exp(-x * x));
  350. }
  351. }
  352. loc_max = corner_sub_pix(grayscale, loc_max, mask);
  353. }
  354. write_to_1d_arrf2(refined_features, loc, loc_max);
  355. }
  356. // Extracts BRIEF descriptors from the grayscale src image for the given features
  357. // using the provided sampler.
  358. __kernel void brief_descriptors(
  359. __read_only image2d_t grayscale,
  360. __global const float2 *refined_features,
  361. // for 512 bit descriptors
  362. __global ulong8 *desc_buf,
  363. __global const PointPair *brief_pattern
  364. ) {
  365. int2 loc = (int2)(get_global_id(0), get_global_id(1));
  366. float2 feature = read_from_1d_arrf2(refined_features, loc);
  367. // There was no feature in this part of the frame
  368. if (feature.x == -1) {
  369. write_to_1d_arrul8(desc_buf, loc, (ulong8)(0));
  370. return;
  371. }
  372. ulong8 desc = 0;
  373. ulong *p = &desc;
  374. for (int i = 0; i < 8; ++i) {
  375. for (int j = 0; j < 64; ++j) {
  376. PointPair pair = brief_pattern[j * (i + 1)];
  377. float l1 = read_imagef(grayscale, sampler_linear, feature + pair.p1).x;
  378. float l2 = read_imagef(grayscale, sampler_linear, feature + pair.p2).x;
  379. if (l1 < l2) {
  380. p[i] |= 1UL << j;
  381. }
  382. }
  383. }
  384. write_to_1d_arrul8(desc_buf, loc, desc);
  385. }
  386. // Given buffers with descriptors for the current and previous frame, determines
  387. // which ones match, writing correspondences to matches_buf.
  388. //
  389. // Feature and descriptor buffers are assumed to be compacted (each element sourced
  390. // from a 32x32 block in the frame being processed).
  391. __kernel void match_descriptors(
  392. __global const float2 *prev_refined_features,
  393. __global const float2 *refined_features,
  394. __global const ulong8 *desc_buf,
  395. __global const ulong8 *prev_desc_buf,
  396. __global MotionVector *matches_buf
  397. ) {
  398. int2 loc = (int2)(get_global_id(0), get_global_id(1));
  399. ulong8 desc = read_from_1d_arrul8(desc_buf, loc);
  400. const int search_radius = 3;
  401. MotionVector invalid_vector = (MotionVector) {
  402. (PointPair) {
  403. (float2)(-1, -1),
  404. (float2)(-1, -1)
  405. },
  406. 0
  407. };
  408. if (desc.s0 == 0 && desc.s1 == 0) {
  409. // There was no feature in this part of the frame
  410. write_to_1d_arrvec(
  411. matches_buf,
  412. loc,
  413. invalid_vector
  414. );
  415. return;
  416. }
  417. int2 start = max(loc - search_radius, 0);
  418. int2 end = min(loc + search_radius, (int2)(get_global_size(0) - 1, get_global_size(1) - 1));
  419. for (int i = start.x; i < end.x; ++i) {
  420. for (int j = start.y; j < end.y; ++j) {
  421. int2 prev_point = (int2)(i, j);
  422. int total_dist = 0;
  423. ulong8 prev_desc = read_from_1d_arrul8(prev_desc_buf, prev_point);
  424. if (prev_desc.s0 == 0 && prev_desc.s1 == 0) {
  425. continue;
  426. }
  427. ulong *prev_desc_p = &prev_desc;
  428. ulong *desc_p = &desc;
  429. for (int i = 0; i < 8; i++) {
  430. total_dist += popcount(desc_p[i] ^ prev_desc_p[i]);
  431. }
  432. if (total_dist < DISTANCE_THRESHOLD) {
  433. write_to_1d_arrvec(
  434. matches_buf,
  435. loc,
  436. (MotionVector) {
  437. (PointPair) {
  438. read_from_1d_arrf2(prev_refined_features, prev_point),
  439. read_from_1d_arrf2(refined_features, loc)
  440. },
  441. 1
  442. }
  443. );
  444. return;
  445. }
  446. }
  447. }
  448. // There is no found match for this point
  449. write_to_1d_arrvec(
  450. matches_buf,
  451. loc,
  452. invalid_vector
  453. );
  454. }
  455. // Returns the position of the given point after the transform is applied
  456. static float2 transformed_point(float2 p, __global const float *transform) {
  457. float2 ret;
  458. ret.x = p.x * transform[0] + p.y * transform[1] + transform[2];
  459. ret.y = p.x * transform[3] + p.y * transform[4] + transform[5];
  460. return ret;
  461. }
  462. // Performs the given transform on the src image
  463. __kernel void transform(
  464. __read_only image2d_t src,
  465. __write_only image2d_t dst,
  466. __global const float *transform
  467. ) {
  468. int2 loc = (int2)(get_global_id(0), get_global_id(1));
  469. float2 norm = convert_float2(get_image_dim(src));
  470. write_imagef(
  471. dst,
  472. loc,
  473. read_imagef(
  474. src,
  475. sampler_linear_mirror,
  476. transformed_point((float2)(loc.x, loc.y), transform) / norm
  477. )
  478. );
  479. }
  480. // Returns the new location of the given point using the given crop bounding box
  481. // and the width and height of the original frame.
  482. static float2 cropped_point(
  483. float2 p,
  484. float2 top_left,
  485. float2 bottom_right,
  486. int2 orig_dim
  487. ) {
  488. float2 ret;
  489. float crop_width = bottom_right.x - top_left.x;
  490. float crop_height = bottom_right.y - top_left.y;
  491. float width_norm = p.x / (float)orig_dim.x;
  492. float height_norm = p.y / (float)orig_dim.y;
  493. ret.x = (width_norm * crop_width) + top_left.x;
  494. ret.y = (height_norm * crop_height) + ((float)orig_dim.y - bottom_right.y);
  495. return ret;
  496. }
  497. // Upscales the given cropped region to the size of the original frame
  498. __kernel void crop_upscale(
  499. __read_only image2d_t src,
  500. __write_only image2d_t dst,
  501. float2 top_left,
  502. float2 bottom_right
  503. ) {
  504. int2 loc = (int2)(get_global_id(0), get_global_id(1));
  505. write_imagef(
  506. dst,
  507. loc,
  508. read_imagef(
  509. src,
  510. sampler_linear,
  511. cropped_point((float2)(loc.x, loc.y), top_left, bottom_right, get_image_dim(dst))
  512. )
  513. );
  514. }
  515. // Draws boxes to represent the given point matches and uses the given transform
  516. // and crop info to make sure their positions are accurate on the transformed frame.
  517. //
  518. // model_matches is an array of three points that were used by the RANSAC process
  519. // to generate the given transform
  520. __kernel void draw_debug_info(
  521. __write_only image2d_t dst,
  522. __global const MotionVector *matches,
  523. __global const MotionVector *model_matches,
  524. int num_model_matches,
  525. __global const float *transform
  526. ) {
  527. int loc = get_global_id(0);
  528. MotionVector vec = matches[loc];
  529. // Black box: matched point that RANSAC considered an outlier
  530. float4 big_rect_color = (float4)(0.1f, 0.1f, 0.1f, 1.0f);
  531. if (vec.should_consider) {
  532. // Green box: matched point that RANSAC considered an inlier
  533. big_rect_color = (float4)(0.0f, 1.0f, 0.0f, 1.0f);
  534. }
  535. for (int i = 0; i < num_model_matches; i++) {
  536. if (vec.p.p2.x == model_matches[i].p.p2.x && vec.p.p2.y == model_matches[i].p.p2.y) {
  537. // Orange box: point used to calculate model
  538. big_rect_color = (float4)(1.0f, 0.5f, 0.0f, 1.0f);
  539. }
  540. }
  541. float2 transformed_p1 = transformed_point(vec.p.p1, transform);
  542. float2 transformed_p2 = transformed_point(vec.p.p2, transform);
  543. draw_box(dst, (int2)(transformed_p2.x, transformed_p2.y), big_rect_color, 5);
  544. // Small light blue box: the point in the previous frame
  545. draw_box(dst, (int2)(transformed_p1.x, transformed_p1.y), (float4)(0.0f, 0.3f, 0.7f, 1.0f), 3);
  546. }