scale.cpp
1 /* ************************************************************************** */ 2 /* */ 3 /* ::: :::::::: */ 4 /* scale.cpp :+: :+: :+: */ 5 /* +:+ +:+ +:+ */ 6 /* By: lfiorell <lfiorell@student.42.fr> +#+ +:+ +#+ */ 7 /* +#+#+#+#+#+ +#+ */ 8 /* Created: 2025/02/13 21:13:24 by lfiorell #+# #+# */ 9 /* Updated: 2025/02/14 12:23:47 by lfiorell ### ########.fr */ 10 /* */ 11 /* ************************************************************************** */ 12 13 #include "CUnit/Basic.h" 14 #include <unistd.h> // Added for usleep 15 16 extern "C" 17 { 18 #include "img/scale.h" 19 #include "mlx.h" 20 #include <time.h> 21 } 22 23 #define CU_ASSERT_RGB_EQUAL(actual, expected) \ 24 { \ 25 CU_ASSERT_EQUAL(actual.r, expected.r); \ 26 CU_ASSERT_EQUAL(actual.g, expected.g); \ 27 CU_ASSERT_EQUAL(actual.b, expected.b); \ 28 } 29 30 #define TOLERANCE 100 // Allowable difference in each color channel 31 #define SHOW_DIFFS 0 // Set to 1 to print out differences and show windows 32 #define MAX_EXECUTION_TIME_SEC (1.0f / 30.0f) // Maximum allowed execution time in seconds 33 34 #if SHOW_DIFFS 35 // Helper function to display images in two windows and wait for user input in the console 36 static void display_images(void *mlx, t_img *src, t_img *dst) 37 { 38 // Determine which image is bigger 39 t_img *first = (src->width * src->height > dst->width * dst->height) ? src : dst; 40 t_img *second = (first == src) ? dst : src; 41 42 const char *name_first = (first == src) ? "src" : "dst"; 43 const char *name_second = (second == src) ? "src" : "dst"; 44 45 void *win_first = mlx_new_window(mlx, first->width, first->height, (char *)name_first); 46 void *win_second = mlx_new_window(mlx, second->width, second->height, (char *)name_second); 47 48 mlx_put_image_to_window(mlx, win_first, first->img_ptr, 0, 0); 49 mlx_put_image_to_window(mlx, win_second, second->img_ptr, 0, 0); 50 51 printf("\n Enter Y for CU_PASS or N for CU_FAIL: "); 52 int ch = getchar(); 53 while (getchar() != '\n') 54 { 55 } // Clear the input buffer 56 57 if (ch == 'Y' || ch == 'y') 58 printf("CU_PASS\n"); 59 else if (ch == 'N' || ch == 'n') 60 printf("CU_FAIL\n"); 61 else 62 printf("Invalid input. Defaulting to CU_FAIL\n"); 63 64 mlx_destroy_window(mlx, win_first); 65 mlx_destroy_window(mlx, win_second); 66 } 67 #endif 68 69 // Helper function: compare two channel values with tolerance 70 static int channels_equal(unsigned char a, unsigned char b) 71 { 72 return (abs((int)a - (int)b) <= TOLERANCE); 73 } 74 75 static void assert_rgba_equal(t_rgba expected, t_rgba actual) 76 { 77 if (!channels_equal(expected.r, actual.r) || 78 !channels_equal(expected.g, actual.g) || 79 !channels_equal(expected.b, actual.b) || 80 !channels_equal(expected.a, actual.a)) 81 { 82 printf("\nExpected: R=%d G=%d B=%d A=%d\n", expected.r, expected.g, expected.b, expected.a); 83 printf("Actual : R=%d G=%d B=%d A=%d\n", actual.r, actual.g, actual.b, actual.a); 84 printf("Diffs : R=%d G=%d B=%d A=%d\n", 85 abs((int)expected.r - (int)actual.r), 86 abs((int)expected.g - (int)actual.g), 87 abs((int)expected.b - (int)actual.b), 88 abs((int)expected.a - (int)actual.a)); 89 } 90 CU_ASSERT_TRUE_FATAL(channels_equal(expected.r, actual.r)); 91 CU_ASSERT_TRUE_FATAL(channels_equal(expected.g, actual.g)); 92 CU_ASSERT_TRUE_FATAL(channels_equal(expected.b, actual.b)); 93 CU_ASSERT_TRUE_FATAL(channels_equal(expected.a, actual.a)); 94 } 95 96 static void assert_execution_time(clock_t start, clock_t end) 97 { 98 double cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC; 99 if (cpu_time_used > MAX_EXECUTION_TIME_SEC) 100 { 101 printf("\nWARNING: Execution time: %.6f seconds (exceeded %.6f seconds limit of %.6f seconds)\n", 102 cpu_time_used, MAX_EXECUTION_TIME_SEC, cpu_time_used - MAX_EXECUTION_TIME_SEC); 103 CU_ASSERT_TRUE(1); // Still pass the test, but issue a warning 104 } 105 else 106 { 107 CU_ASSERT_TRUE(cpu_time_used <= MAX_EXECUTION_TIME_SEC); 108 } 109 } 110 111 #pragma region Nearest Neighbor 112 void test_img_scale_nearest_up_2x(void) 113 { 114 char *path = (char *)"data/image.xpm"; 115 void *mlx = mlx_init(); 116 117 t_img *img = crust_img_from_xpm(mlx, path); 118 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 119 120 t_2d new_size = {img->width * 2, img->height * 2}; 121 122 // Start timing before scale operation 123 clock_t start = clock(); 124 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST); 125 clock_t end = clock(); 126 127 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 128 assert_execution_time(start, end); 129 130 CU_ASSERT_EQUAL(dst->width, img->width * 2); 131 CU_ASSERT_EQUAL(dst->height, img->height * 2); 132 133 for (int y = 0; y < img->height; ++y) 134 { 135 for (int x = 0; x < img->width; ++x) 136 { 137 t_2d pos = {x * 2, y * 2}; 138 t_2d src_pos = {x, y}; 139 t_rgba src_pixel = crust_img_get_pixel(img, src_pos); 140 t_rgba dst_pixel = crust_img_get_pixel(dst, pos); 141 142 CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel); 143 } 144 } 145 146 #if SHOW_DIFFS 147 display_images(mlx, img, dst); 148 #endif 149 150 crust_img_drop(img); 151 crust_img_drop(dst); 152 } 153 154 void test_img_scale_nearest_down_2x(void) 155 { 156 char *path = (char *)"data/image.xpm"; 157 void *mlx = mlx_init(); 158 159 t_img *img = crust_img_from_xpm(mlx, path); 160 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 161 162 t_2d new_size = {img->width / 2, img->height / 2}; 163 164 // Start timing before scale operation 165 clock_t start = clock(); 166 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST); 167 clock_t end = clock(); 168 169 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 170 assert_execution_time(start, end); 171 172 CU_ASSERT_EQUAL(dst->width, img->width / 2); 173 CU_ASSERT_EQUAL(dst->height, img->height / 2); 174 175 for (int y = 0; y < dst->height; ++y) 176 { 177 for (int x = 0; x < dst->width; ++x) 178 { 179 t_2d pos = {x, y}; 180 t_2d src_pos = {x * 2, y * 2}; 181 t_rgba src_pixel = crust_img_get_pixel(img, src_pos); 182 t_rgba dst_pixel = crust_img_get_pixel(dst, pos); 183 184 CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel); 185 } 186 } 187 188 #if SHOW_DIFFS 189 display_images(mlx, img, dst); 190 #endif 191 192 crust_img_drop(img); 193 crust_img_drop(dst); 194 } 195 196 void test_img_scale_nearest_up_1_5x(void) 197 { 198 char *path = (char *)"data/image.xpm"; 199 void *mlx = mlx_init(); 200 201 double gscale = 1.5; 202 float fgscale = gscale; 203 204 t_img *img = crust_img_from_xpm(mlx, path); 205 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 206 207 t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)}; 208 209 // Start timing before scale operation 210 clock_t start = clock(); 211 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST); 212 clock_t end = clock(); 213 214 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 215 assert_execution_time(start, end); 216 217 CU_ASSERT_EQUAL(dst->width, new_size.x); 218 CU_ASSERT_EQUAL(dst->height, new_size.y); 219 220 const float scale = fgscale; 221 const float inv_scale = 1.0f / scale; 222 223 /* Instead of iterating over the source image and mapping forward, 224 iterate over every pixel in the destination and compute the corresponding 225 source pixel (using nearest-neighbor, which is simply a cast after scaling). */ 226 for (int dst_y = 0; dst_y < dst->height; ++dst_y) 227 { 228 int src_y = (int)(dst_y * inv_scale); 229 if (src_y >= img->height) 230 src_y = img->height - 1; 231 232 for (int dst_x = 0; dst_x < dst->width; ++dst_x) 233 { 234 int src_x = (int)(dst_x * inv_scale); 235 if (src_x >= img->width) 236 src_x = img->width - 1; 237 238 t_2d src_pos = {src_x, src_y}; 239 t_2d dst_pos = {dst_x, dst_y}; 240 241 t_rgba src_pixel = crust_img_get_pixel(img, src_pos); 242 t_rgba dst_pixel = crust_img_get_pixel(dst, dst_pos); 243 244 CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel); 245 } 246 } 247 248 #if SHOW_DIFFS 249 display_images(mlx, img, dst); 250 #endif 251 252 crust_img_drop(img); 253 crust_img_drop(dst); 254 } 255 256 void test_img_scale_nearest_down_0_5x(void) 257 { 258 char *path = (char *)"data/image.xpm"; 259 void *mlx = mlx_init(); 260 261 double gscale = 0.5; 262 float fgscale = gscale; 263 264 t_img *img = crust_img_from_xpm(mlx, path); 265 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 266 267 // Calculate new (scaled down) size 268 t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)}; 269 270 // Start timing before scale operation 271 clock_t start = clock(); 272 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST); 273 clock_t end = clock(); 274 275 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 276 assert_execution_time(start, end); 277 278 CU_ASSERT_EQUAL(dst->width, new_size.x); 279 CU_ASSERT_EQUAL(dst->height, new_size.y); 280 281 const float scale = fgscale; 282 const float inv_scale = 1.0f / scale; // inv_scale = 2.0 283 284 // Iterate over every pixel in the destination image. 285 // For each destination pixel, compute its corresponding source pixel. 286 for (int dst_y = 0; dst_y < dst->height; ++dst_y) 287 { 288 // Calculate the corresponding source y position via the inverse scale factor. 289 int src_y = (int)(dst_y * inv_scale); 290 if (src_y >= img->height) 291 src_y = img->height - 1; 292 293 for (int dst_x = 0; dst_x < dst->width; ++dst_x) 294 { 295 // Calculate the corresponding source x position. 296 int src_x = (int)(dst_x * inv_scale); 297 if (src_x >= img->width) 298 src_x = img->width - 1; 299 300 t_2d src_pos = {src_x, src_y}; 301 t_2d dst_pos = {dst_x, dst_y}; 302 303 t_rgba src_pixel = crust_img_get_pixel(img, src_pos); 304 t_rgba dst_pixel = crust_img_get_pixel(dst, dst_pos); 305 306 CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel); 307 } 308 } 309 310 #if SHOW_DIFFS 311 display_images(mlx, img, dst); 312 #endif 313 314 crust_img_drop(img); 315 crust_img_drop(dst); 316 } 317 #pragma endregion 318 319 #pragma region Bilenaer 320 321 // Helper function to clamp coordinates within the image boundaries 322 static inline int clamp(int x, int lower, int upper) 323 { 324 if (x < lower) 325 return lower; 326 else if (x > upper) 327 return upper; 328 else 329 return x; 330 } 331 332 // Helper function: bilinear interpolation of four channel values given fractional weights 333 static unsigned char bilinear_interp_channel(unsigned char c00, unsigned char c10, 334 unsigned char c01, unsigned char c11, 335 float frac_x, float frac_y) 336 { 337 float top = c00 * (1.0f - frac_x) + c10 * frac_x; 338 float bottom = c01 * (1.0f - frac_x) + c11 * frac_x; 339 float value = top * (1.0f - frac_y) + bottom * frac_y; 340 return (unsigned char)(value + 0.5f); 341 } 342 343 // Helper function: compute expected bilinear-interpolated pixel from the source image 344 static t_rgba compute_expected_bilinear(t_img *img, float src_x, float src_y) 345 { 346 // The four neighboring pixels: 347 int x0 = clamp((int)src_x, 0, img->width - 1); 348 int x1 = clamp(x0 + 1, 0, img->width - 1); 349 int y0 = clamp((int)src_y, 0, img->height - 1); 350 int y1 = clamp(y0 + 1, 0, img->height - 1); 351 352 // Fractional parts within the pixel 353 float frac_x = src_x - (float)x0; 354 float frac_y = src_y - (float)y0; 355 356 t_2d p00 = {x0, y0}; 357 t_2d p10 = {x1, y0}; 358 t_2d p01 = {x0, y1}; 359 t_2d p11 = {x1, y1}; 360 361 t_rgba c00 = crust_img_get_pixel(img, p00); 362 t_rgba c10 = crust_img_get_pixel(img, p10); 363 t_rgba c01 = crust_img_get_pixel(img, p01); 364 t_rgba c11 = crust_img_get_pixel(img, p11); 365 366 t_rgba result; 367 result.r = bilinear_interp_channel(c00.r, c10.r, c01.r, c11.r, frac_x, frac_y); 368 result.g = bilinear_interp_channel(c00.g, c10.g, c01.g, c11.g, frac_x, frac_y); 369 result.b = bilinear_interp_channel(c00.b, c10.b, c01.b, c11.b, frac_x, frac_y); 370 result.a = bilinear_interp_channel(c00.a, c10.a, c01.a, c11.a, frac_x, frac_y); 371 372 return result; 373 } 374 375 // Test function for bilinear upscaling by 2x 376 void test_img_scale_bilinear_up_2x(void) 377 { 378 char *path = (char *)"data/image.xpm"; 379 void *mlx = mlx_init(); 380 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 381 382 t_img *img = crust_img_from_xpm(mlx, path); 383 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 384 385 // Target size: double both dimensions 386 t_2d new_size = {img->width * 2, img->height * 2}; 387 388 // Start timing before scale operation 389 clock_t start = clock(); 390 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR); 391 clock_t end = clock(); 392 393 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 394 assert_execution_time(start, end); 395 396 CU_ASSERT_EQUAL(dst->width, new_size.x); 397 CU_ASSERT_EQUAL(dst->height, new_size.y); 398 399 // For each pixel in the destination image, 400 // compute the corresponding source coordinate. 401 // Note: When scaling up 2x, the mapping is: 402 // src_x = dst_x / 2.0 and src_y = dst_y / 2.0 403 for (int dst_y = 0; dst_y < dst->height; ++dst_y) 404 { 405 for (int dst_x = 0; dst_x < dst->width; ++dst_x) 406 { 407 float src_x = dst_x / 2.0f; 408 float src_y = dst_y / 2.0f; 409 410 // Compute what the pixel should be from a bilinear interpolation of the source 411 t_2d dst_pos = {dst_x, dst_y}; 412 t_rgba expected = compute_expected_bilinear(img, src_x, src_y); 413 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 414 415 // Compare each channel with tolerance (if needed) 416 assert_rgba_equal(expected, actual); 417 } 418 } 419 420 #if SHOW_DIFFS 421 display_images(mlx, img, dst); 422 #endif 423 424 crust_img_drop(img); 425 crust_img_drop(dst); 426 } 427 428 // Test function for bilinear downscaling by 2x 429 void test_img_scale_bilinear_down_2x(void) 430 { 431 char *path = (char *)"data/image.xpm"; 432 void *mlx = mlx_init(); 433 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 434 435 t_img *img = crust_img_from_xpm(mlx, path); 436 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 437 438 // Target size: half both dimensions 439 t_2d new_size = {img->width / 2, img->height / 2}; 440 441 // Start timing before scale operation 442 clock_t start = clock(); 443 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR); 444 clock_t end = clock(); 445 446 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 447 assert_execution_time(start, end); 448 449 CU_ASSERT_EQUAL(dst->width, new_size.x); 450 CU_ASSERT_EQUAL(dst->height, new_size.y); 451 452 // For each pixel in the destination image, 453 // compute the corresponding source coordinate. 454 // Note: When scaling down 2x, the mapping is: 455 // src_x = dst_x * 2.0 and src_y = dst_y * 2.0 456 for (int dst_y = 0; dst_y < dst->height; ++dst_y) 457 { 458 for (int dst_x = 0; dst_x < dst->width; ++dst_x) 459 { 460 float src_x = dst_x * 2.0f; 461 float src_y = dst_y * 2.0f; 462 463 // Compute what the pixel should be from a bilinear interpolation of the source 464 t_2d dst_pos = {dst_x, dst_y}; 465 t_rgba expected = compute_expected_bilinear(img, src_x, src_y); 466 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 467 468 // Compare each channel with tolerance (if needed) 469 assert_rgba_equal(expected, actual); 470 } 471 } 472 473 #if SHOW_DIFFS 474 display_images(mlx, img, dst); 475 #endif 476 477 crust_img_drop(img); 478 crust_img_drop(dst); 479 } 480 481 // Test function for bilinear upscaling by 1.5x 482 void test_img_scale_bilinear_up_1_5x(void) 483 { 484 char *path = (char *)"data/image.xpm"; 485 void *mlx = mlx_init(); 486 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 487 488 double gscale = 1.5; 489 float fgscale = gscale; 490 491 t_img *img = crust_img_from_xpm(mlx, path); 492 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 493 494 // Target size: 1.5x both dimensions 495 t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)}; 496 497 // Start timing before scale operation 498 clock_t start = clock(); 499 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR); 500 clock_t end = clock(); 501 502 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 503 assert_execution_time(start, end); 504 505 CU_ASSERT_EQUAL(dst->width, new_size.x); 506 CU_ASSERT_EQUAL(dst->height, new_size.y); 507 508 const float scale = fgscale; 509 const float inv_scale = 1.0f / scale; 510 511 // For each pixel in the destination image, 512 // compute the corresponding source coordinate. 513 for (int dst_y = 0; dst_y < dst->height; ++dst_y) 514 { 515 for (int dst_x = 0; dst_x < dst->width; ++dst_x) 516 { 517 float src_x = dst_x * inv_scale; 518 float src_y = dst_y * inv_scale; 519 520 // Compute what the pixel should be from a bilinear interpolation of the source 521 t_2d dst_pos = {dst_x, dst_y}; 522 t_rgba expected = compute_expected_bilinear(img, src_x, src_y); 523 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 524 525 // Compare each channel with tolerance (if needed) 526 assert_rgba_equal(expected, actual); 527 } 528 } 529 530 #if SHOW_DIFFS 531 display_images(mlx, img, dst); 532 #endif 533 534 crust_img_drop(img); 535 crust_img_drop(dst); 536 } 537 538 // Test function for bilinear downscaling by 0.5x 539 void test_img_scale_bilinear_down_0_5x(void) 540 { 541 char *path = (char *)"data/image.xpm"; 542 void *mlx = mlx_init(); 543 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 544 545 double gscale = 0.5; 546 float fgscale = gscale; 547 548 t_img *img = crust_img_from_xpm(mlx, path); 549 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 550 551 // Target size: half both dimensions 552 t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)}; 553 554 // Start timing before scale operation 555 clock_t start = clock(); 556 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR); 557 clock_t end = clock(); 558 559 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 560 assert_execution_time(start, end); 561 562 CU_ASSERT_EQUAL(dst->width, new_size.x); 563 CU_ASSERT_EQUAL(dst->height, new_size.y); 564 565 const float scale = fgscale; 566 const float inv_scale = 1.0f / scale; 567 568 // For each pixel in the destination image, 569 // compute the corresponding source coordinate. 570 for (int dst_y = 0; dst_y < dst->height; ++dst_y) 571 { 572 for (int dst_x = 0; dst_x < dst->width; ++dst_x) 573 { 574 float src_x = dst_x * inv_scale; 575 float src_y = dst_y * inv_scale; 576 577 // Compute what the pixel should be from a bilinear interpolation of the source 578 t_2d dst_pos = {dst_x, dst_y}; 579 t_rgba expected = compute_expected_bilinear(img, src_x, src_y); 580 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 581 582 // Compare each channel with tolerance (if needed) 583 assert_rgba_equal(expected, actual); 584 } 585 } 586 587 #if SHOW_DIFFS 588 display_images(mlx, img, dst); 589 #endif 590 591 crust_img_drop(img); 592 crust_img_drop(dst); 593 } 594 #pragma endregion 595 596 #pragma region Lanczos 597 // Lanczos kernel with a=2 (using 4 samples total) 598 static float lanczos2(float x) 599 { 600 if (x == 0.0f) 601 return 1.0f; 602 if (x < -2.0f || x > 2.0f) 603 return 0.0f; 604 605 x *= M_PI; 606 return (sin(x) * sin(x / 2.0f)) / (x * x / 2.0f); 607 } 608 609 // Helper function: compute expected Lanczos-interpolated pixel from the source image 610 static t_rgba compute_expected_lanczos(t_img *img, float src_x, float src_y) 611 { 612 float r = 0.0f, g = 0.0f, b = 0.0f, a = 0.0f; 613 float weight_sum = 0.0f; 614 615 // Sample a 4x4 neighborhood centered around the source coordinate 616 int start_x = (int)src_x - 2; 617 int start_y = (int)src_y - 2; 618 619 for (int y = 0; y < 4; y++) 620 { 621 int sample_y = clamp(start_y + y, 0, img->height - 1); 622 float dy = src_y - (start_y + y); 623 624 for (int x = 0; x < 4; x++) 625 { 626 int sample_x = clamp(start_x + x, 0, img->width - 1); 627 float dx = src_x - (start_x + x); 628 629 // Calculate the 2D Lanczos weight 630 float weight = lanczos2(dx) * lanczos2(dy); 631 t_2d pos = {sample_x, sample_y}; 632 t_rgba pixel = crust_img_get_pixel(img, pos); 633 634 r += pixel.r * weight; 635 g += pixel.g * weight; 636 b += pixel.b * weight; 637 a += pixel.a * weight; 638 weight_sum += weight; 639 } 640 } 641 642 // Normalize by weight sum to account for edge cases 643 float inv_weight = 1.0f / weight_sum; 644 t_rgba result; 645 result.r = (unsigned char)(clamp((int)(r * inv_weight + 0.5f), 0, 255)); 646 result.g = (unsigned char)(clamp((int)(g * inv_weight + 0.5f), 0, 255)); 647 result.b = (unsigned char)(clamp((int)(b * inv_weight + 0.5f), 0, 255)); 648 result.a = (unsigned char)(clamp((int)(a * inv_weight + 0.5f), 0, 255)); 649 650 return result; 651 } 652 653 // Test function for Lanczos upscaling by 2x 654 void test_img_scale_lanczos_up_2x(void) 655 { 656 char *path = (char *)"data/image.xpm"; 657 void *mlx = mlx_init(); 658 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 659 660 t_img *img = crust_img_from_xpm(mlx, path); 661 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 662 663 // Ensure source image has valid dimensions 664 CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0); 665 666 // Target size: double both dimensions 667 t_2d new_size = {img->width * 2, img->height * 2}; 668 669 // Start timing before scale operation 670 clock_t start = clock(); 671 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS); 672 clock_t end = clock(); 673 674 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 675 assert_execution_time(start, end); 676 677 CU_ASSERT_EQUAL(dst->width, new_size.x); 678 CU_ASSERT_EQUAL(dst->height, new_size.y); 679 680 const float scale_factor = 2.0f; 681 const float inv_scale = 1.0f / scale_factor; 682 683 // Only test a subset of pixels to avoid long test times 684 const int step = 4; // Test every 4th pixel 685 for (int dst_y = 0; dst_y < dst->height; dst_y += step) 686 { 687 for (int dst_x = 0; dst_x < dst->width; dst_x += step) 688 { 689 float src_x = dst_x * inv_scale; 690 float src_y = dst_y * inv_scale; 691 692 // Ensure source coordinates are within bounds 693 if (src_x >= img->width - 2 || src_y >= img->height - 2) 694 continue; 695 696 t_2d dst_pos = {dst_x, dst_y}; 697 t_rgba expected = compute_expected_lanczos(img, src_x, src_y); 698 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 699 700 // Compare each channel with tolerance 701 assert_rgba_equal(expected, actual); 702 } 703 } 704 705 #if SHOW_DIFFS 706 display_images(mlx, img, dst); 707 #endif 708 709 crust_img_drop(img); 710 crust_img_drop(dst); 711 } 712 713 // Test function for Lanczos downscaling by 2x 714 void test_img_scale_lanczos_down_2x(void) 715 { 716 char *path = (char *)"data/image.xpm"; 717 void *mlx = mlx_init(); 718 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 719 720 t_img *img = crust_img_from_xpm(mlx, path); 721 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 722 723 // Ensure source image has valid dimensions 724 CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0); 725 726 // Target size: half both dimensions 727 t_2d new_size = {img->width / 2, img->height / 2}; 728 729 // Start timing before scale operation 730 clock_t start = clock(); 731 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS); 732 clock_t end = clock(); 733 734 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 735 assert_execution_time(start, end); 736 737 CU_ASSERT_EQUAL(dst->width, new_size.x); 738 CU_ASSERT_EQUAL(dst->height, new_size.y); 739 740 const float scale_factor = 0.5f; 741 const float inv_scale = 1.0f / scale_factor; 742 743 // Only test a subset of pixels to avoid long test times 744 const int step = 4; // Test every 4th pixel 745 for (int dst_y = 0; dst_y < dst->height; dst_y += step) 746 { 747 for (int dst_x = 0; dst_x < dst->width; dst_x += step) 748 { 749 float src_x = dst_x * inv_scale; 750 float src_y = dst_y * inv_scale; 751 752 // Ensure source coordinates are within bounds 753 if (src_x >= img->width - 2 || src_y >= img->height - 2) 754 continue; 755 756 t_2d dst_pos = {dst_x, dst_y}; 757 t_rgba expected = compute_expected_lanczos(img, src_x, src_y); 758 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 759 760 // Compare each channel with tolerance 761 assert_rgba_equal(expected, actual); 762 } 763 } 764 765 #if SHOW_DIFFS 766 display_images(mlx, img, dst); 767 #endif 768 769 crust_img_drop(img); 770 crust_img_drop(dst); 771 } 772 773 // Test function for Lanczos upscaling by 1.5x 774 void test_img_scale_lanczos_up_1_5x(void) 775 { 776 char *path = (char *)"data/image.xpm"; 777 void *mlx = mlx_init(); 778 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 779 780 double gscale = 1.5; 781 float fgscale = gscale; 782 783 t_img *img = crust_img_from_xpm(mlx, path); 784 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 785 786 // Ensure source image has valid dimensions 787 CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0); 788 789 // Target size: 1.5x both dimensions 790 t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)}; 791 792 // Start timing before scale operation 793 clock_t start = clock(); 794 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS); 795 clock_t end = clock(); 796 797 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 798 assert_execution_time(start, end); 799 800 CU_ASSERT_EQUAL(dst->width, new_size.x); 801 CU_ASSERT_EQUAL(dst->height, new_size.y); 802 803 const float scale = fgscale; 804 const float inv_scale = 1.0f / scale; 805 806 // Only test a subset of pixels to avoid long test times 807 const int step = 4; // Test every 4th pixel 808 for (int dst_y = 0; dst_y < dst->height; dst_y += step) 809 { 810 for (int dst_x = 0; dst_x < dst->width; dst_x += step) 811 { 812 float src_x = dst_x * inv_scale; 813 float src_y = dst_y * inv_scale; 814 815 // Ensure source coordinates are within bounds 816 if (src_x >= img->width - 2 || src_y >= img->height - 2) 817 continue; 818 819 t_2d dst_pos = {dst_x, dst_y}; 820 t_rgba expected = compute_expected_lanczos(img, src_x, src_y); 821 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 822 823 // Compare each channel with tolerance 824 assert_rgba_equal(expected, actual); 825 } 826 } 827 828 #if SHOW_DIFFS 829 display_images(mlx, img, dst); 830 #endif 831 832 crust_img_drop(img); 833 crust_img_drop(dst); 834 } 835 836 // Test function for Lanczos downscaling by 0.5x 837 void test_img_scale_lanczos_down_0_5x(void) 838 { 839 char *path = (char *)"data/image.xpm"; 840 void *mlx = mlx_init(); 841 CU_ASSERT_PTR_NOT_NULL_FATAL(mlx); 842 843 double gscale = 0.5; 844 float fgscale = gscale; 845 846 t_img *img = crust_img_from_xpm(mlx, path); 847 CU_ASSERT_PTR_NOT_NULL_FATAL(img); 848 849 // Ensure source image has valid dimensions 850 CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0); 851 852 // Target size: half both dimensions 853 t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)}; 854 855 // Start timing before scale operation 856 clock_t start = clock(); 857 t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS); 858 clock_t end = clock(); 859 860 CU_ASSERT_PTR_NOT_NULL_FATAL(dst); 861 assert_execution_time(start, end); 862 863 CU_ASSERT_EQUAL(dst->width, new_size.x); 864 CU_ASSERT_EQUAL(dst->height, new_size.y); 865 866 const float scale = fgscale; 867 const float inv_scale = 1.0f / scale; 868 869 // Only test a subset of pixels to avoid long test times 870 const int step = 4; // Test every 4th pixel 871 for (int dst_y = 0; dst_y < dst->height; dst_y += step) 872 { 873 for (int dst_x = 0; dst_x < dst->width; dst_x += step) 874 { 875 float src_x = dst_x * inv_scale; 876 float src_y = dst_y * inv_scale; 877 878 // Ensure source coordinates are within bounds 879 if (src_x >= img->width - 2 || src_y >= img->height - 2) 880 continue; 881 882 t_2d dst_pos = {dst_x, dst_y}; 883 t_rgba expected = compute_expected_lanczos(img, src_x, src_y); 884 t_rgba actual = crust_img_get_pixel(dst, dst_pos); 885 886 // Compare each channel with tolerance 887 assert_rgba_equal(expected, actual); 888 } 889 } 890 891 #if SHOW_DIFFS 892 display_images(mlx, img, dst); 893 #endif 894 895 crust_img_drop(img); 896 crust_img_drop(dst); 897 } 898 #pragma endregion 899 900 void run_scale_tests(void) 901 { 902 CU_pSuite suite = CU_add_suite("Scale", NULL, NULL); 903 CU_add_test(suite, "test_img_scale_nearest_up_2x", test_img_scale_nearest_up_2x); 904 CU_add_test(suite, "test_img_scale_nearest_down_2x", test_img_scale_nearest_down_2x); 905 CU_add_test(suite, "test_img_scale_nearest_up_1_5x", test_img_scale_nearest_up_1_5x); 906 CU_add_test(suite, "test_img_scale_nearest_down_0_5x", test_img_scale_nearest_down_0_5x); 907 908 CU_add_test(suite, "test_img_scale_bilinear_up_2x", test_img_scale_bilinear_up_2x); 909 CU_add_test(suite, "test_img_scale_bilinear_down_2x", test_img_scale_bilinear_down_2x); 910 CU_add_test(suite, "test_img_scale_bilinear_up_1_5x", test_img_scale_bilinear_up_1_5x); 911 CU_add_test(suite, "test_img_scale_bilinear_down_0_5x", test_img_scale_bilinear_down_0_5x); 912 913 CU_add_test(suite, "test_img_scale_lanczos_up_2x", test_img_scale_lanczos_up_2x); 914 CU_add_test(suite, "test_img_scale_lanczos_down_2x", test_img_scale_lanczos_down_2x); 915 CU_add_test(suite, "test_img_scale_lanczos_up_1_5x", test_img_scale_lanczos_up_1_5x); 916 CU_add_test(suite, "test_img_scale_lanczos_down_0_5x", test_img_scale_lanczos_down_0_5x); 917 }