├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets └── 2spooky.png ├── include └── third_party │ ├── halide_image_io.h │ └── halide_target_check.h ├── spook.cpp ├── src └── pipeline.cpp └── test ├── images └── horse.png └── output ├── skel.png └── spook_out.png /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cmake-build-debug/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | project(skeletonide) 3 | include_directories(include/third_party) 4 | add_executable(skelgen src/pipeline.cpp) 5 | target_link_libraries(skelgen Halide pthread dl png16 jpeg) 6 | add_custom_command( 7 | TARGET skelgen 8 | COMMAND skelgen 9 | ) 10 | include_directories(${CMAKE_CURRENT_BINARY_DIR}) 11 | add_executable(spook spook.cpp) 12 | add_dependencies(spook skelgen) 13 | link_directories(${CMAKE_CURRENT_BINARY_DIR}) 14 | target_link_libraries(spook ${CMAKE_CURRENT_BINARY_DIR}/skeletonide.a Halide pthread dl png16 jpeg ) 15 | add_custom_command( 16 | TARGET spook 17 | COMMAND mv spook ${CMAKE_HOME_DIRECTORY} 18 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Srimukh Sripada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skeletonide 2 | 3 | 4 | Skeletonide is a parallel implementaion of Zhang-Suen morphological 5 | thinning algorithm written in Halide-lang. It can be used for fast 6 | skeletonization of binary masks. Can also be run on the GPU. 7 | 8 | When you build the project, it generates an ahead-of-time 9 | compiled static library from the halide pipeline. It is then 10 | linked with the caller code to generate a single binary. 11 | 12 | Note: The halide pipeline represents a single pass of the 13 | Zhang-Suen method. The iterations have to be handled by the 14 | caller code - see `spook.cpp` for an example. The number of 15 | iterations is hardcoded right now. It should depend on the 16 | completion flags returned by the halide pipeline. 17 | 18 | 19 | ## Usage 20 | 21 | See `spook.cpp` for an example. The example benchmarks the time 22 | taken to skeletonize a large image on the GPU. Pipeline code is in 23 | `src/pipeline.cpp`. 24 | 25 | 26 | ## Benchmarks 27 | 28 | We get the best performance when it is run on a GPU. The tests are 29 | run on the scikit-image's horse mask, but tiled 10x10 to create a large 30 | (3280, 4000) shaped test image. The time is averaged over 100 runs. 31 | 32 | | Implementation | CPU (i7-7700HQ) | GPU (GTX 1050m) | 33 | | ------------------------------------- | --------------- | --------------- | 34 | | Scikit-image `morphology.skeletonize` | **2073 ms** | NA | 35 | | Skeletonide | 3786 ms | **210 ms** | 36 | 37 | The scheduling of the Halide pipeline can be further tweaked through 38 | trial-and-error to achieve better CPU times. The slow performance on the CPU 39 | is partly explained by the fact that the pipeline only represents a single pass 40 | of the thinning algorithm. There is significant time penalty in handling the 41 | iterations from the outside as illustrated in `spook.cpp`. 42 | However, on the GPU, Skeletonide performs roughly 10x faster than the scikit-image's 43 | CPU implemetation without major modifications. Not a level playing field, obviously. 44 | 45 | The output on the scikit-image's horse mask: 46 | 47 | Mask: 48 | ![mask](test/images/horse.png) 49 | 50 | Skeleton: 51 | ![skel](test/output/spook_out.png) 52 | 53 | ## Build 54 | 55 | ```sh 56 | mkdir build 57 | cd build 58 | cmake .. 59 | cmake --build . 60 | # this will build the static library and its 61 | # header in `build/` and a single test binary 62 | # called `spook` in `skeletonide` - which can 63 | # be run `./spook` to see it in action. 64 | ``` 65 | 66 | ## License 67 | MIT License -------------------------------------------------------------------------------- /assets/2spooky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmalloc/skeletonide/05f76ef65e7d29efb19ebd27bd88de8f5e5db46b/assets/2spooky.png -------------------------------------------------------------------------------- /include/third_party/halide_image_io.h: -------------------------------------------------------------------------------- 1 | // This simple IO library works the Halide::Buffer type or any 2 | // other image type with the same API. 3 | 4 | #ifndef HALIDE_IMAGE_IO_H 5 | #define HALIDE_IMAGE_IO_H 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #ifndef HALIDE_NO_PNG 20 | #include "png.h" 21 | #endif 22 | 23 | #ifndef HALIDE_NO_JPEG 24 | #ifdef _WIN32 25 | #ifndef NOMINMAX 26 | #define NOMINMAX 27 | #endif 28 | #include 29 | #endif 30 | #include "jpeglib.h" 31 | #endif 32 | 33 | #include "HalideRuntime.h" // for halide_type_t 34 | 35 | namespace Halide { 36 | namespace Tools { 37 | 38 | struct FormatInfo { 39 | halide_type_t type; 40 | int dimensions; 41 | 42 | bool operator<(const FormatInfo &other) const { 43 | if (type.code < other.type.code) { 44 | return true; 45 | } else if (type.code > other.type.code) { 46 | return false; 47 | } 48 | if (type.bits < other.type.bits) { 49 | return true; 50 | } else if (type.bits > other.type.bits) { 51 | return false; 52 | } 53 | if (type.lanes < other.type.lanes) { 54 | return true; 55 | } else if (type.lanes > other.type.lanes) { 56 | return false; 57 | } 58 | return (dimensions < other.dimensions); 59 | } 60 | }; 61 | 62 | namespace Internal { 63 | 64 | // Must be constexpr to allow use in case clauses. 65 | inline constexpr int halide_type_code(halide_type_code_t code, int bits) { 66 | return (((int)code) << 8) | bits; 67 | } 68 | 69 | typedef bool (*CheckFunc)(bool condition, const char *msg); 70 | 71 | inline bool CheckFail(bool condition, const char *msg) { 72 | if (!condition) { 73 | fprintf(stderr, "%s\n", msg); 74 | abort(); 75 | } 76 | return condition; 77 | } 78 | 79 | inline bool CheckReturn(bool condition, const char *msg) { 80 | return condition; 81 | } 82 | 83 | template 84 | To convert(const From &from); 85 | 86 | // Convert to bool 87 | template<> 88 | inline bool convert(const bool &in) { 89 | return in; 90 | } 91 | template<> 92 | inline bool convert(const uint8_t &in) { 93 | return in != 0; 94 | } 95 | template<> 96 | inline bool convert(const uint16_t &in) { 97 | return in != 0; 98 | } 99 | template<> 100 | inline bool convert(const uint32_t &in) { 101 | return in != 0; 102 | } 103 | template<> 104 | inline bool convert(const uint64_t &in) { 105 | return in != 0; 106 | } 107 | template<> 108 | inline bool convert(const int8_t &in) { 109 | return in != 0; 110 | } 111 | template<> 112 | inline bool convert(const int16_t &in) { 113 | return in != 0; 114 | } 115 | template<> 116 | inline bool convert(const int32_t &in) { 117 | return in != 0; 118 | } 119 | template<> 120 | inline bool convert(const int64_t &in) { 121 | return in != 0; 122 | } 123 | template<> 124 | inline bool convert(const float &in) { 125 | return in != 0; 126 | } 127 | template<> 128 | inline bool convert(const double &in) { 129 | return in != 0; 130 | } 131 | 132 | // Convert to u8 133 | template<> 134 | inline uint8_t convert(const bool &in) { 135 | return in; 136 | } 137 | template<> 138 | inline uint8_t convert(const uint8_t &in) { 139 | return in; 140 | } 141 | template<> 142 | inline uint8_t convert(const uint16_t &in) { 143 | uint32_t tmp = (uint32_t)(in) + 0x80; 144 | // Fast approximation of div-by-257: see http://research.swtch.com/divmult 145 | return ((tmp * 255 + 255) >> 16); 146 | } 147 | template<> 148 | inline uint8_t convert(const uint32_t &in) { 149 | return (uint8_t)((((uint64_t)in) + 0x00808080) / 0x01010101); 150 | } 151 | // uint64 -> 8 just discards the lower 32 bits: if you were expecting more precision, well, sorry 152 | template<> 153 | inline uint8_t convert(const uint64_t &in) { 154 | return convert(uint32_t(in >> 32)); 155 | } 156 | template<> 157 | inline uint8_t convert(const int8_t &in) { 158 | return convert(in); 159 | } 160 | template<> 161 | inline uint8_t convert(const int16_t &in) { 162 | return convert(in); 163 | } 164 | template<> 165 | inline uint8_t convert(const int32_t &in) { 166 | return convert(in); 167 | } 168 | template<> 169 | inline uint8_t convert(const int64_t &in) { 170 | return convert(in); 171 | } 172 | template<> 173 | inline uint8_t convert(const float &in) { 174 | return (uint8_t)(in * 255.0f + 0.5f); 175 | } 176 | template<> 177 | inline uint8_t convert(const double &in) { 178 | return (uint8_t)(in * 255.0 + 0.5); 179 | } 180 | 181 | // Convert to u16 182 | template<> 183 | inline uint16_t convert(const bool &in) { 184 | return in; 185 | } 186 | template<> 187 | inline uint16_t convert(const uint8_t &in) { 188 | return uint16_t(in) * 0x0101; 189 | } 190 | template<> 191 | inline uint16_t convert(const uint16_t &in) { 192 | return in; 193 | } 194 | template<> 195 | inline uint16_t convert(const uint32_t &in) { 196 | return in >> 16; 197 | } 198 | template<> 199 | inline uint16_t convert(const uint64_t &in) { 200 | return in >> 48; 201 | } 202 | template<> 203 | inline uint16_t convert(const int8_t &in) { 204 | return convert(in); 205 | } 206 | template<> 207 | inline uint16_t convert(const int16_t &in) { 208 | return convert(in); 209 | } 210 | template<> 211 | inline uint16_t convert(const int32_t &in) { 212 | return convert(in); 213 | } 214 | template<> 215 | inline uint16_t convert(const int64_t &in) { 216 | return convert(in); 217 | } 218 | template<> 219 | inline uint16_t convert(const float &in) { 220 | return (uint16_t)(in * 65535.0f + 0.5f); 221 | } 222 | template<> 223 | inline uint16_t convert(const double &in) { 224 | return (uint16_t)(in * 65535.0 + 0.5); 225 | } 226 | 227 | // Convert to u32 228 | template<> 229 | inline uint32_t convert(const bool &in) { 230 | return in; 231 | } 232 | template<> 233 | inline uint32_t convert(const uint8_t &in) { 234 | return uint32_t(in) * 0x01010101; 235 | } 236 | template<> 237 | inline uint32_t convert(const uint16_t &in) { 238 | return uint32_t(in) * 0x00010001; 239 | } 240 | template<> 241 | inline uint32_t convert(const uint32_t &in) { 242 | return in; 243 | } 244 | template<> 245 | inline uint32_t convert(const uint64_t &in) { 246 | return (uint32_t)(in >> 32); 247 | } 248 | template<> 249 | inline uint32_t convert(const int8_t &in) { 250 | return convert(in); 251 | } 252 | template<> 253 | inline uint32_t convert(const int16_t &in) { 254 | return convert(in); 255 | } 256 | template<> 257 | inline uint32_t convert(const int32_t &in) { 258 | return convert(in); 259 | } 260 | template<> 261 | inline uint32_t convert(const int64_t &in) { 262 | return convert(in); 263 | } 264 | template<> 265 | inline uint32_t convert(const float &in) { 266 | return (uint32_t)(in * 4294967295.0 + 0.5); 267 | } 268 | template<> 269 | inline uint32_t convert(const double &in) { 270 | return (uint32_t)(in * 4294967295.0 + 0.5f); 271 | } 272 | 273 | // Convert to u64 274 | template<> 275 | inline uint64_t convert(const bool &in) { 276 | return in; 277 | } 278 | template<> 279 | inline uint64_t convert(const uint8_t &in) { 280 | return uint64_t(in) * 0x0101010101010101LL; 281 | } 282 | template<> 283 | inline uint64_t convert(const uint16_t &in) { 284 | return uint64_t(in) * 0x0001000100010001LL; 285 | } 286 | template<> 287 | inline uint64_t convert(const uint32_t &in) { 288 | return uint64_t(in) * 0x0000000100000001LL; 289 | } 290 | template<> 291 | inline uint64_t convert(const uint64_t &in) { 292 | return in; 293 | } 294 | template<> 295 | inline uint64_t convert(const int8_t &in) { 296 | return convert(in); 297 | } 298 | template<> 299 | inline uint64_t convert(const int16_t &in) { 300 | return convert(in); 301 | } 302 | template<> 303 | inline uint64_t convert(const int32_t &in) { 304 | return convert(in); 305 | } 306 | template<> 307 | inline uint64_t convert(const int64_t &in) { 308 | return convert(in); 309 | } 310 | template<> 311 | inline uint64_t convert(const float &in) { 312 | return convert((uint32_t)(in * 4294967295.0 + 0.5)); 313 | } 314 | template<> 315 | inline uint64_t convert(const double &in) { 316 | return convert((uint32_t)(in * 4294967295.0 + 0.5)); 317 | } 318 | 319 | // Convert to i8 320 | template<> 321 | inline int8_t convert(const bool &in) { 322 | return in; 323 | } 324 | template<> 325 | inline int8_t convert(const uint8_t &in) { 326 | return convert(in); 327 | } 328 | template<> 329 | inline int8_t convert(const uint16_t &in) { 330 | return convert(in); 331 | } 332 | template<> 333 | inline int8_t convert(const uint32_t &in) { 334 | return convert(in); 335 | } 336 | template<> 337 | inline int8_t convert(const uint64_t &in) { 338 | return convert(in); 339 | } 340 | template<> 341 | inline int8_t convert(const int8_t &in) { 342 | return convert(in); 343 | } 344 | template<> 345 | inline int8_t convert(const int16_t &in) { 346 | return convert(in); 347 | } 348 | template<> 349 | inline int8_t convert(const int32_t &in) { 350 | return convert(in); 351 | } 352 | template<> 353 | inline int8_t convert(const int64_t &in) { 354 | return convert(in); 355 | } 356 | template<> 357 | inline int8_t convert(const float &in) { 358 | return convert(in); 359 | } 360 | template<> 361 | inline int8_t convert(const double &in) { 362 | return convert(in); 363 | } 364 | 365 | // Convert to i16 366 | template<> 367 | inline int16_t convert(const bool &in) { 368 | return in; 369 | } 370 | template<> 371 | inline int16_t convert(const uint8_t &in) { 372 | return convert(in); 373 | } 374 | template<> 375 | inline int16_t convert(const uint16_t &in) { 376 | return convert(in); 377 | } 378 | template<> 379 | inline int16_t convert(const uint32_t &in) { 380 | return convert(in); 381 | } 382 | template<> 383 | inline int16_t convert(const uint64_t &in) { 384 | return convert(in); 385 | } 386 | template<> 387 | inline int16_t convert(const int8_t &in) { 388 | return convert(in); 389 | } 390 | template<> 391 | inline int16_t convert(const int16_t &in) { 392 | return convert(in); 393 | } 394 | template<> 395 | inline int16_t convert(const int32_t &in) { 396 | return convert(in); 397 | } 398 | template<> 399 | inline int16_t convert(const int64_t &in) { 400 | return convert(in); 401 | } 402 | template<> 403 | inline int16_t convert(const float &in) { 404 | return convert(in); 405 | } 406 | template<> 407 | inline int16_t convert(const double &in) { 408 | return convert(in); 409 | } 410 | 411 | // Convert to i32 412 | template<> 413 | inline int32_t convert(const bool &in) { 414 | return in; 415 | } 416 | template<> 417 | inline int32_t convert(const uint8_t &in) { 418 | return convert(in); 419 | } 420 | template<> 421 | inline int32_t convert(const uint16_t &in) { 422 | return convert(in); 423 | } 424 | template<> 425 | inline int32_t convert(const uint32_t &in) { 426 | return convert(in); 427 | } 428 | template<> 429 | inline int32_t convert(const uint64_t &in) { 430 | return convert(in); 431 | } 432 | template<> 433 | inline int32_t convert(const int8_t &in) { 434 | return convert(in); 435 | } 436 | template<> 437 | inline int32_t convert(const int16_t &in) { 438 | return convert(in); 439 | } 440 | template<> 441 | inline int32_t convert(const int32_t &in) { 442 | return convert(in); 443 | } 444 | template<> 445 | inline int32_t convert(const int64_t &in) { 446 | return convert(in); 447 | } 448 | template<> 449 | inline int32_t convert(const float &in) { 450 | return convert(in); 451 | } 452 | template<> 453 | inline int32_t convert(const double &in) { 454 | return convert(in); 455 | } 456 | 457 | // Convert to i64 458 | template<> 459 | inline int64_t convert(const bool &in) { 460 | return in; 461 | } 462 | template<> 463 | inline int64_t convert(const uint8_t &in) { 464 | return convert(in); 465 | } 466 | template<> 467 | inline int64_t convert(const uint16_t &in) { 468 | return convert(in); 469 | } 470 | template<> 471 | inline int64_t convert(const uint32_t &in) { 472 | return convert(in); 473 | } 474 | template<> 475 | inline int64_t convert(const uint64_t &in) { 476 | return convert(in); 477 | } 478 | template<> 479 | inline int64_t convert(const int8_t &in) { 480 | return convert(in); 481 | } 482 | template<> 483 | inline int64_t convert(const int16_t &in) { 484 | return convert(in); 485 | } 486 | template<> 487 | inline int64_t convert(const int32_t &in) { 488 | return convert(in); 489 | } 490 | template<> 491 | inline int64_t convert(const int64_t &in) { 492 | return convert(in); 493 | } 494 | template<> 495 | inline int64_t convert(const float &in) { 496 | return convert(in); 497 | } 498 | template<> 499 | inline int64_t convert(const double &in) { 500 | return convert(in); 501 | } 502 | 503 | // Convert to f32 504 | template<> 505 | inline float convert(const bool &in) { 506 | return in; 507 | } 508 | template<> 509 | inline float convert(const uint8_t &in) { 510 | return in / 255.0f; 511 | } 512 | template<> 513 | inline float convert(const uint16_t &in) { 514 | return in / 65535.0f; 515 | } 516 | template<> 517 | inline float convert(const uint32_t &in) { 518 | return (float)(in / 4294967295.0); 519 | } 520 | template<> 521 | inline float convert(const uint64_t &in) { 522 | return convert(uint32_t(in >> 32)); 523 | } 524 | template<> 525 | inline float convert(const int8_t &in) { 526 | return convert(in); 527 | } 528 | template<> 529 | inline float convert(const int16_t &in) { 530 | return convert(in); 531 | } 532 | template<> 533 | inline float convert(const int32_t &in) { 534 | return convert(in); 535 | } 536 | template<> 537 | inline float convert(const int64_t &in) { 538 | return convert(in); 539 | } 540 | template<> 541 | inline float convert(const float &in) { 542 | return in; 543 | } 544 | template<> 545 | inline float convert(const double &in) { 546 | return (float)in; 547 | } 548 | 549 | // Convert to f64 550 | template<> 551 | inline double convert(const bool &in) { 552 | return in; 553 | } 554 | template<> 555 | inline double convert(const uint8_t &in) { 556 | return in / 255.0f; 557 | } 558 | template<> 559 | inline double convert(const uint16_t &in) { 560 | return in / 65535.0f; 561 | } 562 | template<> 563 | inline double convert(const uint32_t &in) { 564 | return (double)(in / 4294967295.0); 565 | } 566 | template<> 567 | inline double convert(const uint64_t &in) { 568 | return convert(uint32_t(in >> 32)); 569 | } 570 | template<> 571 | inline double convert(const int8_t &in) { 572 | return convert(in); 573 | } 574 | template<> 575 | inline double convert(const int16_t &in) { 576 | return convert(in); 577 | } 578 | template<> 579 | inline double convert(const int32_t &in) { 580 | return convert(in); 581 | } 582 | template<> 583 | inline double convert(const int64_t &in) { 584 | return convert(in); 585 | } 586 | template<> 587 | inline double convert(const float &in) { 588 | return (double)in; 589 | } 590 | template<> 591 | inline double convert(const double &in) { 592 | return in; 593 | } 594 | 595 | inline std::string to_lowercase(const std::string &s) { 596 | std::string r = s; 597 | std::transform(r.begin(), r.end(), r.begin(), ::tolower); 598 | return r; 599 | } 600 | 601 | inline std::string get_lowercase_extension(const std::string &path) { 602 | size_t last_dot = path.rfind('.'); 603 | if (last_dot == std::string::npos) { 604 | return ""; 605 | } 606 | return to_lowercase(path.substr(last_dot + 1)); 607 | } 608 | 609 | template 610 | ElemType read_big_endian(const uint8_t *src); 611 | 612 | template<> 613 | inline uint8_t read_big_endian(const uint8_t *src) { 614 | return *src; 615 | } 616 | 617 | template<> 618 | inline uint16_t read_big_endian(const uint8_t *src) { 619 | return (((uint16_t)src[0]) << 8) | ((uint16_t)src[1]); 620 | } 621 | 622 | template 623 | void write_big_endian(const ElemType &src, uint8_t *dst); 624 | 625 | template<> 626 | inline void write_big_endian(const uint8_t &src, uint8_t *dst) { 627 | *dst = src; 628 | } 629 | 630 | template<> 631 | inline void write_big_endian(const uint16_t &src, uint8_t *dst) { 632 | dst[0] = src >> 8; 633 | dst[1] = src & 0xff; 634 | } 635 | 636 | struct FileOpener { 637 | FileOpener(const std::string &filename, const char *mode) 638 | : f(fopen(filename.c_str(), mode)) { 639 | // nothing 640 | } 641 | 642 | ~FileOpener() { 643 | if (f != nullptr) { 644 | fclose(f); 645 | } 646 | } 647 | 648 | // read a line of data, skipping lines that begin with '#" 649 | char *read_line(char *buf, int maxlen) { 650 | char *status; 651 | do { 652 | status = fgets(buf, maxlen, f); 653 | } while (status && buf[0] == '#'); 654 | return (status); 655 | } 656 | 657 | // call read_line and to a sscanf() on it 658 | int scan_line(const char *fmt, ...) { 659 | char buf[1024]; 660 | if (!read_line(buf, 1024)) { 661 | return 0; 662 | } 663 | va_list args; 664 | va_start(args, fmt); 665 | int result = vsscanf(buf, fmt, args); 666 | va_end(args); 667 | return result; 668 | } 669 | 670 | bool read_bytes(void *data, size_t count) { 671 | return fread(data, 1, count, f) == count; 672 | } 673 | 674 | template 675 | bool read_array(T (&data)[N]) { 676 | return read_bytes(&data[0], sizeof(T) * N); 677 | } 678 | 679 | template 680 | bool read_vector(std::vector *v) { 681 | return read_bytes(v->data(), v->size() * sizeof(T)); 682 | } 683 | 684 | bool write_bytes(const void *data, size_t count) { 685 | return fwrite(data, 1, count, f) == count; 686 | } 687 | 688 | template 689 | bool write_vector(const std::vector &v) { 690 | return write_bytes(v.data(), v.size() * sizeof(T)); 691 | } 692 | 693 | template 694 | bool write_array(const T (&data)[N]) { 695 | return write_bytes(&data[0], sizeof(T) * N); 696 | } 697 | 698 | FILE *const f; 699 | }; 700 | 701 | // Read a row of ElemTypes from a byte buffer and copy them into a specific image row. 702 | // Multibyte elements are assumed to be big-endian. 703 | template 704 | void read_big_endian_row(const uint8_t *src, int y, ImageType *im) { 705 | auto im_typed = im->template as(); 706 | const int xmin = im_typed.dim(0).min(); 707 | const int xmax = im_typed.dim(0).max(); 708 | if (im_typed.dimensions() > 2) { 709 | const int cmin = im_typed.dim(2).min(); 710 | const int cmax = im_typed.dim(2).max(); 711 | for (int x = xmin; x <= xmax; x++) { 712 | for (int c = cmin; c <= cmax; c++) { 713 | im_typed(x, y, c + cmin) = read_big_endian(src); 714 | src += sizeof(ElemType); 715 | } 716 | } 717 | } else { 718 | for (int x = xmin; x <= xmax; x++) { 719 | im_typed(x, y) = read_big_endian(src); 720 | src += sizeof(ElemType); 721 | } 722 | } 723 | } 724 | 725 | // Copy a row from an image into a byte buffer. 726 | // Multibyte elements are written in big-endian layout. 727 | template 728 | void write_big_endian_row(const ImageType &im, int y, uint8_t *dst) { 729 | auto im_typed = im.template as::type>(); 730 | const int xmin = im_typed.dim(0).min(); 731 | const int xmax = im_typed.dim(0).max(); 732 | if (im_typed.dimensions() > 2) { 733 | const int cmin = im_typed.dim(2).min(); 734 | const int cmax = im_typed.dim(2).max(); 735 | for (int x = xmin; x <= xmax; x++) { 736 | for (int c = cmin; c <= cmax; c++) { 737 | write_big_endian(im_typed(x, y, c), dst); 738 | dst += sizeof(ElemType); 739 | } 740 | } 741 | } else { 742 | for (int x = xmin; x <= xmax; x++) { 743 | write_big_endian(im_typed(x, y), dst); 744 | dst += sizeof(ElemType); 745 | } 746 | } 747 | } 748 | 749 | #ifndef HALIDE_NO_PNG 750 | 751 | template 752 | bool load_png(const std::string &filename, ImageType *im) { 753 | static_assert(!ImageType::has_static_halide_type, ""); 754 | 755 | /* open file and test for it being a png */ 756 | Internal::FileOpener f(filename, "rb"); 757 | if (!check(f.f != nullptr, "File could not be opened for reading")) { 758 | return false; 759 | } 760 | png_byte header[8]; 761 | if (!check(f.read_array(header), "File ended before end of header")) { 762 | return false; 763 | } 764 | if (!check(!png_sig_cmp(header, 0, 8), "File is not recognized as a PNG file")) { 765 | return false; 766 | } 767 | 768 | /* initialize stuff */ 769 | png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 770 | if (!check(png_ptr != nullptr, "png_create_read_struct failed")) { 771 | return false; 772 | } 773 | 774 | png_infop info_ptr = png_create_info_struct(png_ptr); 775 | if (!check(info_ptr != nullptr, "png_create_info_struct failed")) { 776 | return false; 777 | } 778 | 779 | if (!check(!setjmp(png_jmpbuf(png_ptr)), "Error loading PNG")) { 780 | return false; 781 | } 782 | 783 | png_init_io(png_ptr, f.f); 784 | png_set_sig_bytes(png_ptr, 8); 785 | 786 | png_read_info(png_ptr, info_ptr); 787 | 788 | const int width = png_get_image_width(png_ptr, info_ptr); 789 | const int height = png_get_image_height(png_ptr, info_ptr); 790 | const int channels = png_get_channels(png_ptr, info_ptr); 791 | const int bit_depth = png_get_bit_depth(png_ptr, info_ptr); 792 | 793 | const halide_type_t im_type(halide_type_uint, bit_depth); 794 | std::vector im_dimensions = {width, height}; 795 | if (channels != 1) { 796 | im_dimensions.push_back(channels); 797 | } 798 | 799 | *im = ImageType(im_type, im_dimensions); 800 | 801 | png_read_update_info(png_ptr, info_ptr); 802 | 803 | auto copy_to_image = bit_depth == 8 ? 804 | Internal::read_big_endian_row : 805 | Internal::read_big_endian_row; 806 | 807 | std::vector row(png_get_rowbytes(png_ptr, info_ptr)); 808 | const int ymin = im->dim(1).min(); 809 | const int ymax = im->dim(1).max(); 810 | for (int y = ymin; y <= ymax; ++y) { 811 | png_read_row(png_ptr, row.data(), nullptr); 812 | copy_to_image(row.data(), y, im); 813 | } 814 | 815 | png_destroy_read_struct(&png_ptr, &info_ptr, NULL); 816 | 817 | return true; 818 | } 819 | 820 | inline const std::set &query_png() { 821 | static std::set info = { 822 | {halide_type_t(halide_type_uint, 8), 2}, 823 | {halide_type_t(halide_type_uint, 16), 2}, 824 | {halide_type_t(halide_type_uint, 8), 3}, 825 | {halide_type_t(halide_type_uint, 16), 3}}; 826 | return info; 827 | } 828 | 829 | // "im" is not const-ref because copy_to_host() is not const. 830 | template 831 | bool save_png(ImageType &im, const std::string &filename) { 832 | static_assert(!ImageType::has_static_halide_type, ""); 833 | 834 | im.copy_to_host(); 835 | 836 | const int width = im.width(); 837 | const int height = im.height(); 838 | const int channels = im.channels(); 839 | 840 | if (!check(channels >= 1 && channels <= 4, 841 | "Can't write PNG files that have other than 1, 2, 3, or 4 channels")) { 842 | return false; 843 | } 844 | 845 | const png_byte color_types[4] = { 846 | PNG_COLOR_TYPE_GRAY, 847 | PNG_COLOR_TYPE_GRAY_ALPHA, 848 | PNG_COLOR_TYPE_RGB, 849 | PNG_COLOR_TYPE_RGB_ALPHA}; 850 | png_byte color_type = color_types[channels - 1]; 851 | 852 | // open file 853 | Internal::FileOpener f(filename, "wb"); 854 | if (!check(f.f != nullptr, "[write_png_file] File could not be opened for writing")) { 855 | return false; 856 | } 857 | 858 | // initialize stuff 859 | png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 860 | if (!check(png_ptr != nullptr, "[write_png_file] png_create_write_struct failed")) { 861 | return false; 862 | } 863 | 864 | png_infop info_ptr = png_create_info_struct(png_ptr); 865 | if (!check(info_ptr != nullptr, "[write_png_file] png_create_info_struct failed")) { 866 | return false; 867 | } 868 | 869 | if (!check(!setjmp(png_jmpbuf(png_ptr)), "Error saving PNG")) { 870 | return false; 871 | } 872 | 873 | png_init_io(png_ptr, f.f); 874 | 875 | const halide_type_t im_type = im.type(); 876 | const int bit_depth = im_type.bits; 877 | 878 | png_set_IHDR(png_ptr, info_ptr, width, height, 879 | bit_depth, color_type, PNG_INTERLACE_NONE, 880 | PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); 881 | 882 | png_write_info(png_ptr, info_ptr); 883 | 884 | auto copy_from_image = bit_depth == 8 ? 885 | Internal::write_big_endian_row : 886 | Internal::write_big_endian_row; 887 | 888 | std::vector row(png_get_rowbytes(png_ptr, info_ptr)); 889 | const int ymin = im.dim(1).min(); 890 | const int ymax = im.dim(1).max(); 891 | for (int y = ymin; y <= ymax; ++y) { 892 | copy_from_image(im, y, row.data()); 893 | png_write_row(png_ptr, row.data()); 894 | } 895 | png_write_end(png_ptr, NULL); 896 | png_destroy_write_struct(&png_ptr, &info_ptr); 897 | 898 | return true; 899 | } 900 | 901 | #endif // not HALIDE_NO_PNG 902 | 903 | template 904 | bool read_pnm_header(Internal::FileOpener &f, const std::string &hdr_fmt, int *width, int *height, int *bit_depth) { 905 | if (!check(f.f != nullptr, "File could not be opened for reading")) { 906 | return false; 907 | } 908 | 909 | char header[256]; 910 | if (!check(f.scan_line("%255s", header) == 1, "Could not read header")) { 911 | return false; 912 | } 913 | 914 | if (!check(to_lowercase(hdr_fmt) == to_lowercase(header), "Unexpected file header")) { 915 | return false; 916 | } 917 | 918 | if (!check(f.scan_line("%d %d\n", width, height) == 2, "Could not read width and height")) { 919 | return false; 920 | } 921 | 922 | int maxval; 923 | if (!check(f.scan_line("%d", &maxval) == 1, "Could not read max value")) { 924 | return false; 925 | } 926 | if (maxval == 255) { 927 | *bit_depth = 8; 928 | } else if (maxval == 65535) { 929 | *bit_depth = 16; 930 | } else { 931 | *bit_depth = 0; 932 | return check(false, "Invalid bit depth"); 933 | } 934 | 935 | return true; 936 | } 937 | 938 | template 939 | bool load_pnm(const std::string &filename, int channels, ImageType *im) { 940 | static_assert(!ImageType::has_static_halide_type, ""); 941 | 942 | const char *hdr_fmt = channels == 3 ? "P6" : "P5"; 943 | 944 | Internal::FileOpener f(filename, "rb"); 945 | int width, height, bit_depth; 946 | if (!Internal::read_pnm_header(f, hdr_fmt, &width, &height, &bit_depth)) { 947 | return false; 948 | } 949 | 950 | const halide_type_t im_type(halide_type_uint, bit_depth); 951 | std::vector im_dimensions = {width, height}; 952 | if (channels > 1) { 953 | im_dimensions.push_back(channels); 954 | } 955 | *im = ImageType(im_type, im_dimensions); 956 | 957 | auto copy_to_image = bit_depth == 8 ? 958 | Internal::read_big_endian_row : 959 | Internal::read_big_endian_row; 960 | 961 | std::vector row(width * channels * (bit_depth / 8)); 962 | const int ymin = im->dim(1).min(); 963 | const int ymax = im->dim(1).max(); 964 | for (int y = ymin; y <= ymax; ++y) { 965 | if (!check(f.read_vector(&row), "Could not read data")) { 966 | return false; 967 | } 968 | copy_to_image(row.data(), y, im); 969 | } 970 | 971 | return true; 972 | } 973 | 974 | template 975 | bool save_pnm(ImageType &im, const int channels, const std::string &filename) { 976 | static_assert(!ImageType::has_static_halide_type, ""); 977 | 978 | if (!check(im.channels() == channels, "Wrong number of channels")) { 979 | return false; 980 | } 981 | 982 | im.copy_to_host(); 983 | 984 | const halide_type_t im_type = im.type(); 985 | const int width = im.width(); 986 | const int height = im.height(); 987 | const int bit_depth = im_type.bits; 988 | 989 | Internal::FileOpener f(filename, "wb"); 990 | if (!check(f.f != nullptr, "File could not be opened for writing")) { 991 | return false; 992 | } 993 | const char *hdr_fmt = channels == 3 ? "P6" : "P5"; 994 | fprintf(f.f, "%s\n%d %d\n%d\n", hdr_fmt, width, height, (1 << bit_depth) - 1); 995 | 996 | auto copy_from_image = bit_depth == 8 ? 997 | Internal::write_big_endian_row : 998 | Internal::write_big_endian_row; 999 | 1000 | std::vector row(width * channels * (bit_depth / 8)); 1001 | const int ymin = im.dim(1).min(); 1002 | const int ymax = im.dim(1).max(); 1003 | for (int y = ymin; y <= ymax; ++y) { 1004 | copy_from_image(im, y, row.data()); 1005 | if (!check(f.write_vector(row), "Could not write data")) { 1006 | return false; 1007 | } 1008 | } 1009 | 1010 | return true; 1011 | } 1012 | 1013 | template 1014 | bool load_pgm(const std::string &filename, ImageType *im) { 1015 | return Internal::load_pnm(filename, 1, im); 1016 | } 1017 | 1018 | inline const std::set &query_pgm() { 1019 | static std::set info = { 1020 | {halide_type_t(halide_type_uint, 8), 2}, 1021 | {halide_type_t(halide_type_uint, 16), 2}}; 1022 | return info; 1023 | } 1024 | 1025 | // "im" is not const-ref because copy_to_host() is not const. 1026 | template 1027 | bool save_pgm(ImageType &im, const std::string &filename) { 1028 | return Internal::save_pnm(im, 1, filename); 1029 | } 1030 | 1031 | template 1032 | bool load_ppm(const std::string &filename, ImageType *im) { 1033 | return Internal::load_pnm(filename, 3, im); 1034 | } 1035 | 1036 | inline const std::set &query_ppm() { 1037 | static std::set info = { 1038 | {halide_type_t(halide_type_uint, 8), 3}, 1039 | {halide_type_t(halide_type_uint, 16), 3}}; 1040 | return info; 1041 | } 1042 | 1043 | // "im" is not const-ref because copy_to_host() is not const. 1044 | template 1045 | bool save_ppm(ImageType &im, const std::string &filename) { 1046 | return Internal::save_pnm(im, 3, filename); 1047 | } 1048 | 1049 | #ifndef HALIDE_NO_JPEG 1050 | 1051 | template 1052 | bool load_jpg(const std::string &filename, ImageType *im) { 1053 | static_assert(!ImageType::has_static_halide_type, ""); 1054 | 1055 | Internal::FileOpener f(filename, "rb"); 1056 | if (!check(f.f != nullptr, "File could not be opened for reading")) { 1057 | return false; 1058 | } 1059 | 1060 | struct jpeg_decompress_struct cinfo; 1061 | struct jpeg_error_mgr jerr; 1062 | cinfo.err = jpeg_std_error(&jerr); 1063 | jpeg_create_decompress(&cinfo); 1064 | jpeg_stdio_src(&cinfo, f.f); 1065 | jpeg_read_header(&cinfo, TRUE); 1066 | jpeg_start_decompress(&cinfo); 1067 | 1068 | const int width = cinfo.output_width; 1069 | const int height = cinfo.output_height; 1070 | const int channels = cinfo.output_components; 1071 | 1072 | const halide_type_t im_type(halide_type_uint, 8); 1073 | std::vector im_dimensions = {width, height}; 1074 | if (channels > 1) { 1075 | im_dimensions.push_back(channels); 1076 | } 1077 | *im = ImageType(im_type, im_dimensions); 1078 | 1079 | auto copy_to_image = Internal::read_big_endian_row; 1080 | 1081 | std::vector row(width * channels); 1082 | const int ymin = im->dim(1).min(); 1083 | const int ymax = im->dim(1).max(); 1084 | for (int y = ymin; y <= ymax; ++y) { 1085 | uint8_t *src = row.data(); 1086 | jpeg_read_scanlines(&cinfo, &src, 1); 1087 | copy_to_image(row.data(), y, im); 1088 | } 1089 | 1090 | jpeg_finish_decompress(&cinfo); 1091 | jpeg_destroy_decompress(&cinfo); 1092 | 1093 | return true; 1094 | } 1095 | 1096 | inline const std::set &query_jpg() { 1097 | static std::set info = { 1098 | {halide_type_t(halide_type_uint, 8), 2}, 1099 | {halide_type_t(halide_type_uint, 8), 3}, 1100 | }; 1101 | return info; 1102 | } 1103 | 1104 | template 1105 | bool save_jpg(ImageType &im, const std::string &filename) { 1106 | static_assert(!ImageType::has_static_halide_type, ""); 1107 | 1108 | im.copy_to_host(); 1109 | 1110 | const int width = im.width(); 1111 | const int height = im.height(); 1112 | const int channels = im.channels(); 1113 | if (!check(channels == 1 || channels == 3, "Wrong number of channels")) { 1114 | return false; 1115 | } 1116 | 1117 | Internal::FileOpener f(filename, "wb"); 1118 | if (!check(f.f != nullptr, "File could not be opened for writing")) { 1119 | return false; 1120 | } 1121 | 1122 | // TODO: Make this an argument? 1123 | constexpr int quality = 99; 1124 | 1125 | struct jpeg_compress_struct cinfo; 1126 | struct jpeg_error_mgr jerr; 1127 | cinfo.err = jpeg_std_error(&jerr); 1128 | jpeg_create_compress(&cinfo); 1129 | jpeg_stdio_dest(&cinfo, f.f); 1130 | cinfo.image_width = width; 1131 | cinfo.image_height = height; 1132 | cinfo.input_components = channels; 1133 | cinfo.in_color_space = (channels == 3) ? JCS_RGB : JCS_GRAYSCALE; 1134 | jpeg_set_defaults(&cinfo); 1135 | jpeg_set_quality(&cinfo, quality, TRUE); 1136 | jpeg_start_compress(&cinfo, TRUE); 1137 | 1138 | auto copy_from_image = Internal::write_big_endian_row; 1139 | 1140 | std::vector row(width * channels); 1141 | const int ymin = im.dim(1).min(); 1142 | const int ymax = im.dim(1).max(); 1143 | for (int y = ymin; y <= ymax; ++y) { 1144 | uint8_t *dst = row.data(); 1145 | copy_from_image(im, y, dst); 1146 | jpeg_write_scanlines(&cinfo, &dst, 1); 1147 | } 1148 | 1149 | jpeg_finish_compress(&cinfo); 1150 | jpeg_destroy_compress(&cinfo); 1151 | 1152 | return true; 1153 | } 1154 | 1155 | #endif // not HALIDE_NO_JPEG 1156 | 1157 | constexpr int kNumTmpCodes = 10; 1158 | 1159 | inline const halide_type_t *tmp_code_to_halide_type() { 1160 | static const halide_type_t tmp_code_to_halide_type_[kNumTmpCodes] = { 1161 | {halide_type_float, 32}, 1162 | {halide_type_float, 64}, 1163 | {halide_type_uint, 8}, 1164 | {halide_type_int, 8}, 1165 | {halide_type_uint, 16}, 1166 | {halide_type_int, 16}, 1167 | {halide_type_uint, 32}, 1168 | {halide_type_int, 32}, 1169 | {halide_type_uint, 64}, 1170 | {halide_type_int, 64}}; 1171 | return tmp_code_to_halide_type_; 1172 | } 1173 | 1174 | // return true iff the buffer storage has no padding between 1175 | // any elements, and is in strictly planar order. 1176 | template 1177 | bool buffer_is_compact_planar(ImageType &im) { 1178 | const halide_type_t im_type = im.type(); 1179 | const size_t elem_size = (im_type.bits / 8); 1180 | if (((const uint8_t *)im.begin() + (im.number_of_elements() * elem_size)) != (const uint8_t *)im.end()) { 1181 | return false; 1182 | } 1183 | for (int d = 1; d < im.dimensions(); ++d) { 1184 | if (im.dim(d - 1).stride() > im.dim(d).stride()) { 1185 | return false; 1186 | } 1187 | // Strides can only match if the previous dimension has extent 1 1188 | // (this can happen when artificially adding dimension(s), e.g. 1189 | // to write a .tmp file) 1190 | if (im.dim(d - 1).stride() == im.dim(d).stride() && im.dim(d - 1).extent() != 1) { 1191 | return false; 1192 | } 1193 | } 1194 | return true; 1195 | } 1196 | 1197 | // ".tmp" is a file format used by the ImageStack tool (see https://github.com/abadams/ImageStack) 1198 | template 1199 | bool load_tmp(const std::string &filename, ImageType *im) { 1200 | static_assert(!ImageType::has_static_halide_type, ""); 1201 | 1202 | FileOpener f(filename, "rb"); 1203 | if (!check(f.f != nullptr, "File could not be opened for reading")) { 1204 | return false; 1205 | } 1206 | 1207 | int32_t header[5]; 1208 | if (!check(f.read_array(header), "Count not read .tmp header")) { 1209 | return false; 1210 | } 1211 | 1212 | if (!check(header[0] > 0 && header[1] > 0 && header[2] > 0 && header[3] > 0 && 1213 | header[4] >= 0 && header[4] < kNumTmpCodes, 1214 | "Bad header on .tmp file")) { 1215 | return false; 1216 | } 1217 | 1218 | const halide_type_t im_type = tmp_code_to_halide_type()[header[4]]; 1219 | std::vector im_dimensions = {header[0], header[1], header[2], header[3]}; 1220 | *im = ImageType(im_type, im_dimensions); 1221 | 1222 | // This should never fail unless the default Buffer<> constructor behavior changes. 1223 | if (!check(buffer_is_compact_planar(*im), "load_tmp() requires compact planar images")) { 1224 | return false; 1225 | } 1226 | 1227 | if (!check(f.read_bytes(im->begin(), im->size_in_bytes()), "Count not read .tmp payload")) { 1228 | return false; 1229 | } 1230 | 1231 | im->set_host_dirty(); 1232 | return true; 1233 | } 1234 | 1235 | inline const std::set &query_tmp() { 1236 | // TMP files require exactly 4 dimensions. 1237 | static std::set info = { 1238 | {halide_type_t(halide_type_float, 32), 4}, 1239 | {halide_type_t(halide_type_float, 64), 4}, 1240 | {halide_type_t(halide_type_uint, 8), 4}, 1241 | {halide_type_t(halide_type_int, 8), 4}, 1242 | {halide_type_t(halide_type_uint, 16), 4}, 1243 | {halide_type_t(halide_type_int, 16), 4}, 1244 | {halide_type_t(halide_type_uint, 32), 4}, 1245 | {halide_type_t(halide_type_int, 32), 4}, 1246 | {halide_type_t(halide_type_uint, 64), 4}, 1247 | {halide_type_t(halide_type_int, 64), 4}, 1248 | }; 1249 | return info; 1250 | } 1251 | 1252 | template 1253 | bool write_planar_payload(ImageType &im, FileOpener &f) { 1254 | if (im.dimensions() == 0 || buffer_is_compact_planar(im)) { 1255 | // Contiguous buffer! Write it all in one swell foop. 1256 | if (!check(f.write_bytes(im.begin(), im.size_in_bytes()), "Count not write .tmp payload")) { 1257 | return false; 1258 | } 1259 | } else { 1260 | // We have to do this the hard way. 1261 | int d = im.dimensions() - 1; 1262 | for (int i = im.dim(d).min(); i <= im.dim(d).max(); i++) { 1263 | auto slice = im.sliced(d, i); 1264 | if (!write_planar_payload(slice, f)) { 1265 | return false; 1266 | } 1267 | } 1268 | } 1269 | return true; 1270 | } 1271 | 1272 | // ".tmp" is a file format used by the ImageStack tool (see https://github.com/abadams/ImageStack) 1273 | template 1274 | bool save_tmp(ImageType &im, const std::string &filename) { 1275 | static_assert(!ImageType::has_static_halide_type, ""); 1276 | 1277 | im.copy_to_host(); 1278 | 1279 | int32_t header[5] = {1, 1, 1, 1, -1}; 1280 | for (int i = 0; i < im.dimensions(); ++i) { 1281 | header[i] = im.dim(i).extent(); 1282 | } 1283 | auto *table = tmp_code_to_halide_type(); 1284 | for (int i = 0; i < kNumTmpCodes; i++) { 1285 | if (im.type() == table[i]) { 1286 | header[4] = i; 1287 | break; 1288 | } 1289 | } 1290 | if (!check(header[4] >= 0, "Unsupported type for .tmp file")) { 1291 | return false; 1292 | } 1293 | 1294 | FileOpener f(filename, "wb"); 1295 | if (!check(f.f != nullptr, "File could not be opened for writing")) { 1296 | return false; 1297 | } 1298 | if (!check(f.write_array(header), "Could not write .tmp header")) { 1299 | return false; 1300 | } 1301 | 1302 | if (!write_planar_payload(im, f)) { 1303 | return false; 1304 | } 1305 | 1306 | return true; 1307 | } 1308 | 1309 | // ".mat" is the matlab level 5 format documented here: 1310 | // http://www.mathworks.com/help/pdf_doc/matlab/matfile_format.pdf 1311 | 1312 | enum MatlabTypeCode { 1313 | miINT8 = 1, 1314 | miUINT8 = 2, 1315 | miINT16 = 3, 1316 | miUINT16 = 4, 1317 | miINT32 = 5, 1318 | miUINT32 = 6, 1319 | miSINGLE = 7, 1320 | miDOUBLE = 9, 1321 | miINT64 = 12, 1322 | miUINT64 = 13, 1323 | miMATRIX = 14, 1324 | miCOMPRESSED = 15, 1325 | miUTF8 = 16, 1326 | miUTF16 = 17, 1327 | miUTF32 = 18 1328 | }; 1329 | 1330 | enum MatlabClassCode { 1331 | mxCHAR_CLASS = 3, 1332 | mxDOUBLE_CLASS = 6, 1333 | mxSINGLE_CLASS = 7, 1334 | mxINT8_CLASS = 8, 1335 | mxUINT8_CLASS = 9, 1336 | mxINT16_CLASS = 10, 1337 | mxUINT16_CLASS = 11, 1338 | mxINT32_CLASS = 12, 1339 | mxUINT32_CLASS = 13, 1340 | mxINT64_CLASS = 14, 1341 | mxUINT64_CLASS = 15 1342 | }; 1343 | 1344 | template 1345 | bool load_mat(const std::string &filename, ImageType *im) { 1346 | static_assert(!ImageType::has_static_halide_type, ""); 1347 | 1348 | FileOpener f(filename, "rb"); 1349 | if (!check(f.f != nullptr, "File could not be opened for reading")) { 1350 | return false; 1351 | } 1352 | 1353 | uint8_t header[128]; 1354 | if (!check(f.read_array(header), "Could not read .mat header\n")) { 1355 | return false; 1356 | } 1357 | 1358 | // Matrix header 1359 | uint32_t matrix_header[2]; 1360 | if (!check(f.read_array(matrix_header), "Could not read .mat header\n")) { 1361 | return false; 1362 | } 1363 | if (!check(matrix_header[0] == miMATRIX, "Could not parse this .mat file: bad matrix header\n")) { 1364 | return false; 1365 | } 1366 | 1367 | // Array flags 1368 | uint32_t flags[4]; 1369 | if (!check(f.read_array(flags), "Could not read .mat header\n")) { 1370 | return false; 1371 | } 1372 | if (!check(flags[0] == miUINT32 && flags[1] == 8, "Could not parse this .mat file: bad flags\n")) { 1373 | return false; 1374 | } 1375 | 1376 | // Shape 1377 | uint32_t shape_header[2]; 1378 | if (!check(f.read_array(shape_header), "Could not read .mat header\n")) { 1379 | return false; 1380 | } 1381 | if (!check(shape_header[0] == miINT32, "Could not parse this .mat file: bad shape header\n")) { 1382 | return false; 1383 | } 1384 | int dims = shape_header[1] / 4; 1385 | std::vector extents(dims); 1386 | if (!check(f.read_vector(&extents), "Could not read .mat header\n")) { 1387 | return false; 1388 | } 1389 | if (dims & 1) { 1390 | uint32_t padding; 1391 | if (!check(f.read_bytes(&padding, 4), "Could not read .mat header\n")) { 1392 | return false; 1393 | } 1394 | } 1395 | 1396 | // Skip over the name 1397 | uint32_t name_header[2]; 1398 | if (!check(f.read_array(name_header), "Could not read .mat header\n")) { 1399 | return false; 1400 | } 1401 | 1402 | if (name_header[0] >> 16) { 1403 | // Name must be fewer than 4 chars, and so the whole name 1404 | // field was stored packed into 8 bytes 1405 | } else { 1406 | if (!check(name_header[0] == miINT8, "Could not parse this .mat file: bad name header\n")) { 1407 | return false; 1408 | } 1409 | std::vector scratch((name_header[1] + 7) / 8); 1410 | if (!check(f.read_vector(&scratch), "Could not read .mat header\n")) { 1411 | return false; 1412 | } 1413 | } 1414 | 1415 | // Payload header 1416 | uint32_t payload_header[2]; 1417 | if (!check(f.read_array(payload_header), "Could not read .mat header\n")) { 1418 | return false; 1419 | } 1420 | halide_type_t type; 1421 | switch (payload_header[0]) { 1422 | case miINT8: 1423 | type = halide_type_of(); 1424 | break; 1425 | case miINT16: 1426 | type = halide_type_of(); 1427 | break; 1428 | case miINT32: 1429 | type = halide_type_of(); 1430 | break; 1431 | case miINT64: 1432 | type = halide_type_of(); 1433 | break; 1434 | case miUINT8: 1435 | type = halide_type_of(); 1436 | break; 1437 | case miUINT16: 1438 | type = halide_type_of(); 1439 | break; 1440 | case miUINT32: 1441 | type = halide_type_of(); 1442 | break; 1443 | case miUINT64: 1444 | type = halide_type_of(); 1445 | break; 1446 | case miSINGLE: 1447 | type = halide_type_of(); 1448 | break; 1449 | case miDOUBLE: 1450 | type = halide_type_of(); 1451 | break; 1452 | } 1453 | 1454 | *im = ImageType(type, extents); 1455 | 1456 | // This should never fail unless the default Buffer<> constructor behavior changes. 1457 | if (!check(buffer_is_compact_planar(*im), "load_mat() requires compact planar images")) { 1458 | return false; 1459 | } 1460 | 1461 | if (!check(f.read_bytes(im->begin(), im->size_in_bytes()), "Could not read .tmp payload")) { 1462 | return false; 1463 | } 1464 | 1465 | im->set_host_dirty(); 1466 | return true; 1467 | } 1468 | 1469 | inline const std::set &query_mat() { 1470 | // MAT files must have at least 2 dimensions, but there's no upper 1471 | // bound. Our support arbitrarily stops at 16 dimensions. 1472 | static std::set info = []() { 1473 | std::set s; 1474 | for (int i = 2; i < 16; i++) { 1475 | s.insert({halide_type_t(halide_type_float, 32), i}); 1476 | s.insert({halide_type_t(halide_type_float, 64), i}); 1477 | s.insert({halide_type_t(halide_type_uint, 8), i}); 1478 | s.insert({halide_type_t(halide_type_int, 8), i}); 1479 | s.insert({halide_type_t(halide_type_uint, 16), i}); 1480 | s.insert({halide_type_t(halide_type_int, 16), i}); 1481 | s.insert({halide_type_t(halide_type_uint, 32), i}); 1482 | s.insert({halide_type_t(halide_type_int, 32), i}); 1483 | s.insert({halide_type_t(halide_type_uint, 64), i}); 1484 | s.insert({halide_type_t(halide_type_int, 64), i}); 1485 | } 1486 | return s; 1487 | }(); 1488 | return info; 1489 | } 1490 | 1491 | template 1492 | bool save_mat(ImageType &im, const std::string &filename) { 1493 | static_assert(!ImageType::has_static_halide_type, ""); 1494 | 1495 | im.copy_to_host(); 1496 | 1497 | uint32_t class_code = 0, type_code = 0; 1498 | switch (im.raw_buffer()->type.code) { 1499 | case halide_type_int: 1500 | switch (im.raw_buffer()->type.bits) { 1501 | case 8: 1502 | class_code = mxINT8_CLASS; 1503 | type_code = miINT8; 1504 | break; 1505 | case 16: 1506 | class_code = mxINT16_CLASS; 1507 | type_code = miINT16; 1508 | break; 1509 | case 32: 1510 | class_code = mxINT32_CLASS; 1511 | type_code = miINT32; 1512 | break; 1513 | case 64: 1514 | class_code = mxINT64_CLASS; 1515 | type_code = miINT64; 1516 | break; 1517 | default: 1518 | check(false, "unreachable"); 1519 | }; 1520 | break; 1521 | case halide_type_uint: 1522 | switch (im.raw_buffer()->type.bits) { 1523 | case 8: 1524 | class_code = mxUINT8_CLASS; 1525 | type_code = miUINT8; 1526 | break; 1527 | case 16: 1528 | class_code = mxUINT16_CLASS; 1529 | type_code = miUINT16; 1530 | break; 1531 | case 32: 1532 | class_code = mxUINT32_CLASS; 1533 | type_code = miUINT32; 1534 | break; 1535 | case 64: 1536 | class_code = mxUINT64_CLASS; 1537 | type_code = miUINT64; 1538 | break; 1539 | default: 1540 | check(false, "unreachable"); 1541 | }; 1542 | break; 1543 | case halide_type_float: 1544 | switch (im.raw_buffer()->type.bits) { 1545 | case 16: 1546 | check(false, "float16 not supported by .mat"); 1547 | break; 1548 | case 32: 1549 | class_code = mxSINGLE_CLASS; 1550 | type_code = miSINGLE; 1551 | break; 1552 | case 64: 1553 | class_code = mxDOUBLE_CLASS; 1554 | type_code = miDOUBLE; 1555 | break; 1556 | default: 1557 | check(false, "unreachable"); 1558 | }; 1559 | break; 1560 | case halide_type_bfloat: 1561 | check(false, "bfloat not supported by .mat"); 1562 | break; 1563 | default: 1564 | check(false, "unreachable"); 1565 | } 1566 | 1567 | FileOpener f(filename, "wb"); 1568 | if (!check(f.f != nullptr, "File could not be opened for writing")) { 1569 | return false; 1570 | } 1571 | 1572 | // Pick a name for the array 1573 | size_t idx = filename.rfind('.'); 1574 | std::string name = filename.substr(0, idx); 1575 | idx = filename.rfind('/'); 1576 | if (idx != std::string::npos) { 1577 | name = name.substr(idx + 1); 1578 | } 1579 | 1580 | // Matlab variable names conform to similar rules as C 1581 | if (name.empty() || !std::isalpha(name[0])) { 1582 | name = "v" + name; 1583 | } 1584 | for (size_t i = 0; i < name.size(); i++) { 1585 | if (!std::isalnum(name[i])) { 1586 | name[i] = '_'; 1587 | } 1588 | } 1589 | 1590 | uint32_t name_size = (int)name.size(); 1591 | while (name.size() & 0x7) 1592 | name += '\0'; 1593 | 1594 | char header[128] = "MATLAB 5.0 MAT-file, produced by Halide"; 1595 | int len = strlen(header); 1596 | memset(header + len, ' ', sizeof(header) - len); 1597 | 1598 | // Version 1599 | *((uint16_t *)(header + 124)) = 0x0100; 1600 | 1601 | // Endianness check 1602 | header[126] = 'I'; 1603 | header[127] = 'M'; 1604 | 1605 | uint64_t payload_bytes = im.size_in_bytes(); 1606 | 1607 | if (!check((payload_bytes >> 32) == 0, "Buffer too large to save as .mat")) { 1608 | return false; 1609 | } 1610 | 1611 | int dims = im.dimensions(); 1612 | if (dims < 2) { 1613 | dims = 2; 1614 | } 1615 | int padded_dims = dims + (dims & 1); 1616 | 1617 | uint32_t padding_bytes = 7 - ((payload_bytes - 1) & 7); 1618 | 1619 | // Matrix header 1620 | uint32_t matrix_header[2] = { 1621 | miMATRIX, 40 + padded_dims * 4 + (uint32_t)name.size() + (uint32_t)payload_bytes + padding_bytes}; 1622 | 1623 | // Array flags 1624 | uint32_t flags[4] = { 1625 | miUINT32, 8, class_code, 1}; 1626 | 1627 | // Shape 1628 | int32_t shape[2] = { 1629 | miINT32, 1630 | im.dimensions() * 4, 1631 | }; 1632 | std::vector extents(im.dimensions()); 1633 | for (int d = 0; d < im.dimensions(); d++) { 1634 | extents[d] = im.dim(d).extent(); 1635 | } 1636 | while ((int)extents.size() < dims) { 1637 | extents.push_back(1); 1638 | } 1639 | while ((int)extents.size() < padded_dims) { 1640 | extents.push_back(0); 1641 | } 1642 | 1643 | // Name 1644 | uint32_t name_header[2] = { 1645 | miINT8, name_size}; 1646 | 1647 | // Payload header 1648 | uint32_t payload_header[2] = { 1649 | type_code, (uint32_t)payload_bytes}; 1650 | 1651 | bool success = 1652 | f.write_array(header) && 1653 | f.write_array(matrix_header) && 1654 | f.write_array(flags) && 1655 | f.write_array(shape) && 1656 | f.write_vector(extents) && 1657 | f.write_array(name_header) && 1658 | f.write_bytes(&name[0], name.size()) && 1659 | f.write_array(payload_header); 1660 | 1661 | if (!check(success, "Could not write .mat header")) { 1662 | return false; 1663 | } 1664 | 1665 | if (!write_planar_payload(im, f)) { 1666 | return false; 1667 | } 1668 | 1669 | // Padding 1670 | if (!check(padding_bytes < 8, "Too much padding!\n")) { 1671 | return false; 1672 | } 1673 | uint64_t padding = 0; 1674 | if (!f.write_bytes(&padding, padding_bytes)) { 1675 | return false; 1676 | } 1677 | 1678 | return true; 1679 | } 1680 | 1681 | template 1682 | bool load_tiff(const std::string &filename, ImageType *im) { 1683 | static_assert(!ImageType::has_static_halide_type, ""); 1684 | check(false, "Reading TIFF is not yet supported"); 1685 | return false; 1686 | } 1687 | 1688 | inline const std::set &query_tiff() { 1689 | auto build_set = []() -> std::set { 1690 | std::set s; 1691 | for (halide_type_code_t code : {halide_type_int, halide_type_uint, halide_type_float}) { 1692 | for (int bits : {8, 16, 32, 64}) { 1693 | for (int dims : {1, 2, 3, 4}) { 1694 | if (code == halide_type_float && bits < 32) { 1695 | continue; 1696 | } 1697 | s.insert({halide_type_t(code, bits), dims}); 1698 | } 1699 | } 1700 | } 1701 | return s; 1702 | }; 1703 | 1704 | static std::set info = build_set(); 1705 | return info; 1706 | } 1707 | 1708 | #pragma pack(push) 1709 | #pragma pack(2) 1710 | 1711 | struct halide_tiff_tag { 1712 | uint16_t tag_code; 1713 | int16_t type_code; 1714 | int32_t count; 1715 | union { 1716 | int8_t i8; 1717 | int16_t i16; 1718 | int32_t i32; 1719 | } value; 1720 | 1721 | void assign16(uint16_t tag_code, int32_t count, int16_t value) { 1722 | this->tag_code = tag_code; 1723 | this->type_code = 3; // SHORT 1724 | this->count = count; 1725 | this->value.i16 = value; 1726 | } 1727 | 1728 | void assign32(uint16_t tag_code, int32_t count, int32_t value) { 1729 | this->tag_code = tag_code; 1730 | this->type_code = 4; // LONG 1731 | this->count = count; 1732 | this->value.i32 = value; 1733 | } 1734 | 1735 | void assign32(uint16_t tag_code, int16_t type_code, int32_t count, int32_t value) { 1736 | this->tag_code = tag_code; 1737 | this->type_code = type_code; 1738 | this->count = count; 1739 | this->value.i32 = value; 1740 | } 1741 | }; 1742 | 1743 | struct halide_tiff_header { 1744 | int16_t byte_order_marker; 1745 | int16_t version; 1746 | int32_t ifd0_offset; 1747 | int16_t entry_count; 1748 | halide_tiff_tag entries[15]; 1749 | int32_t ifd0_end; 1750 | int32_t width_resolution[2]; 1751 | int32_t height_resolution[2]; 1752 | }; 1753 | 1754 | #pragma pack(pop) 1755 | 1756 | template 1757 | struct ElemWriter { 1758 | ElemWriter(FileOpener *f) 1759 | : f(f), next(&buf[0]), ok(true) { 1760 | } 1761 | ~ElemWriter() { 1762 | flush(); 1763 | } 1764 | 1765 | void operator()(const ElemType &elem) { 1766 | if (!ok) return; 1767 | 1768 | *next++ = elem; 1769 | if (next == &buf[BUFFER_SIZE]) { 1770 | flush(); 1771 | } 1772 | } 1773 | 1774 | void flush() { 1775 | if (!ok) return; 1776 | 1777 | if (next > buf) { 1778 | if (!f->write_bytes(buf, (next - buf) * sizeof(ElemType))) { 1779 | ok = false; 1780 | } 1781 | next = buf; 1782 | } 1783 | } 1784 | 1785 | FileOpener *const f; 1786 | ElemType buf[BUFFER_SIZE]; 1787 | ElemType *next; 1788 | bool ok; 1789 | }; 1790 | 1791 | // Note that this is a fairly simpleminded TIFF writer that doesn't 1792 | // do any compression. It would be desirable to (optionally) support using libtiff 1793 | // here instead, which would also allow us to provide a useful implementation 1794 | // for TIFF reading. 1795 | template 1796 | bool save_tiff(ImageType &im, const std::string &filename) { 1797 | static_assert(!ImageType::has_static_halide_type, ""); 1798 | 1799 | im.copy_to_host(); 1800 | 1801 | if (!check(im.dimensions() <= 4, "Can only save TIFF files with <= 4 dimensions")) { 1802 | return false; 1803 | } 1804 | 1805 | FileOpener f(filename, "wb"); 1806 | if (!check(f.f != nullptr, "File could not be opened for writing")) { 1807 | return false; 1808 | } 1809 | 1810 | const size_t elements = im.number_of_elements(); 1811 | halide_dimension_t shape[4]; 1812 | for (int i = 0; i < im.dimensions() && i < 4; i++) { 1813 | const auto &d = im.dim(i); 1814 | shape[i].min = d.min(); 1815 | shape[i].extent = d.extent(); 1816 | shape[i].stride = d.stride(); 1817 | } 1818 | for (int i = im.dimensions(); i < 4; i++) { 1819 | shape[i].min = 0; 1820 | shape[i].extent = 1; 1821 | shape[i].stride = 0; 1822 | } 1823 | const halide_type_t im_type = im.type(); 1824 | if (!check(im_type.code >= 0 && im_type.code < 3, "Unsupported image type")) { 1825 | return false; 1826 | } 1827 | const int32_t bytes_per_element = im_type.bytes(); 1828 | const int32_t width = shape[0].extent; 1829 | const int32_t height = shape[1].extent; 1830 | int32_t depth = shape[2].extent; 1831 | int32_t channels = shape[3].extent; 1832 | 1833 | if ((channels == 0 || channels == 1) && (depth < 5)) { 1834 | channels = depth; 1835 | depth = 1; 1836 | } 1837 | 1838 | // TIFF sample type values are: 1839 | // 0 => Signed int 1840 | // 1 => Unsigned int 1841 | // 2 => Floating-point 1842 | static const int16_t type_code_to_tiff_sample_type[] = { 1843 | 2, 1, 3}; 1844 | 1845 | struct halide_tiff_header header; 1846 | memset(&header, 0, sizeof(header)); 1847 | 1848 | const int32_t MMII = 0x4d4d4949; 1849 | // Select the appropriate two bytes signaling byte order automatically 1850 | const char *c = (const char *)&MMII; 1851 | header.byte_order_marker = (c[0] << 8) | c[1]; 1852 | header.version = 42; 1853 | header.ifd0_offset = offsetof(halide_tiff_header, entry_count); 1854 | header.entry_count = sizeof(header.entries) / sizeof(header.entries[0]); 1855 | 1856 | static_assert(sizeof(halide_tiff_tag) == 12, "Unexpected halide_tiff_tag packing"); 1857 | halide_tiff_tag *tag = &header.entries[0]; 1858 | tag++->assign32(256, 1, width); // ImageWidth 1859 | tag++->assign32(257, 1, height); // ImageLength 1860 | tag++->assign16(258, 1, int16_t(bytes_per_element * 8)); // BitsPerSample 1861 | tag++->assign16(259, 1, 1); // Compression -- none 1862 | tag++->assign16(262, 1, channels >= 3 ? 2 : 1); // PhotometricInterpretation -- black is zero or RGB 1863 | tag++->assign32(273, channels, sizeof(header)); // StripOffsets 1864 | tag++->assign16(277, 1, int16_t(channels)); // SamplesPerPixel 1865 | tag++->assign32(278, 1, height); // RowsPerStrip 1866 | tag++->assign32(279, channels, // StripByteCounts 1867 | (channels == 1) ? 1868 | elements * bytes_per_element : 1869 | sizeof(header) + channels * sizeof(int32_t)); // for channels > 1, this is an offset 1870 | tag++->assign32(282, 5, 1, 1871 | offsetof(halide_tiff_header, width_resolution)); // XResolution 1872 | tag++->assign32(283, 5, 1, 1873 | offsetof(halide_tiff_header, height_resolution)); // YResolution 1874 | tag++->assign16(284, 1, 2); // PlanarConfiguration -- planar 1875 | tag++->assign16(296, 1, 1); // ResolutionUnit -- none 1876 | tag++->assign16(339, 1, type_code_to_tiff_sample_type[im_type.code]); // SampleFormat 1877 | tag++->assign32(32997, 1, depth); // Image depth 1878 | 1879 | // Verify we used exactly the number we declared 1880 | assert(tag == &header.entries[header.entry_count]); 1881 | 1882 | header.ifd0_end = 0; 1883 | header.width_resolution[0] = 1; 1884 | header.width_resolution[1] = 1; 1885 | header.height_resolution[0] = 1; 1886 | header.height_resolution[1] = 1; 1887 | 1888 | if (!check(f.write_bytes(&header, sizeof(header)), "TIFF write failed")) { 1889 | return false; 1890 | } 1891 | 1892 | if (channels > 1) { 1893 | // Fill in the values for StripOffsets 1894 | int32_t offset = sizeof(header) + channels * sizeof(int32_t) * 2; 1895 | for (int32_t i = 0; i < channels; i++) { 1896 | if (!check(f.write_bytes(&offset, sizeof(offset)), "TIFF write failed")) { 1897 | return false; 1898 | } 1899 | offset += width * height * depth * bytes_per_element; 1900 | } 1901 | // Fill in the values for StripByteCounts 1902 | int32_t count = width * height * depth * bytes_per_element; 1903 | for (int32_t i = 0; i < channels; i++) { 1904 | if (!check(f.write_bytes(&count, sizeof(count)), "TIFF write failed")) { 1905 | return false; 1906 | } 1907 | } 1908 | } 1909 | 1910 | // If image is dense, we can write it in one fell swoop 1911 | if (elements * bytes_per_element == im.size_in_bytes()) { 1912 | if (!check(f.write_bytes(im.data(), im.size_in_bytes()), "TIFF write failed")) { 1913 | return false; 1914 | } 1915 | return true; 1916 | } 1917 | 1918 | // Otherwise, write it out via manual traversal. 1919 | #define HANDLE_CASE(CODE, BITS, TYPE) \ 1920 | case halide_type_code(CODE, BITS): { \ 1921 | ElemWriter ew(&f); \ 1922 | im.template as().for_each_value(ew); \ 1923 | if (!check(ew.ok, "TIFF write failed")) { \ 1924 | return false; \ 1925 | } \ 1926 | break; \ 1927 | } 1928 | 1929 | switch (halide_type_code((halide_type_code_t)im_type.code, im_type.bits)) { 1930 | HANDLE_CASE(halide_type_float, 32, float) 1931 | HANDLE_CASE(halide_type_float, 64, double) 1932 | HANDLE_CASE(halide_type_int, 8, int8_t) 1933 | HANDLE_CASE(halide_type_int, 16, int16_t) 1934 | HANDLE_CASE(halide_type_int, 32, int32_t) 1935 | HANDLE_CASE(halide_type_int, 64, int64_t) 1936 | HANDLE_CASE(halide_type_uint, 1, bool) 1937 | HANDLE_CASE(halide_type_uint, 8, uint8_t) 1938 | HANDLE_CASE(halide_type_uint, 16, uint16_t) 1939 | HANDLE_CASE(halide_type_uint, 32, uint32_t) 1940 | HANDLE_CASE(halide_type_uint, 64, uint64_t) 1941 | // Note that we don't attempt to handle halide_type_handle here. 1942 | default: 1943 | assert(false && "Unsupported type"); 1944 | return false; 1945 | } 1946 | #undef HANDLE_CASE 1947 | 1948 | return true; 1949 | } 1950 | 1951 | // Given something like ImageType, produce typedef ImageType 1952 | template 1953 | struct ImageTypeWithElemType { 1954 | using type = decltype(std::declval().template as()); 1955 | }; 1956 | 1957 | // Given something like ImageType, produce typedef ImageType 1958 | template 1959 | struct ImageTypeWithConstElemType { 1960 | using type = decltype(std::declval().template as::type>()); 1961 | }; 1962 | 1963 | template 1964 | struct ImageIO { 1965 | using ConstImageType = typename ImageTypeWithConstElemType::type; 1966 | 1967 | std::function load; 1968 | std::function save; 1969 | std::function &()> query; 1970 | }; 1971 | 1972 | template 1973 | bool find_imageio(const std::string &filename, ImageIO *result) { 1974 | static_assert(!ImageType::has_static_halide_type, ""); 1975 | using ConstImageType = typename ImageTypeWithConstElemType::type; 1976 | 1977 | const std::map> m = { 1978 | #ifndef HALIDE_NO_JPEG 1979 | {"jpeg", {load_jpg, save_jpg, query_jpg}}, 1980 | {"jpg", {load_jpg, save_jpg, query_jpg}}, 1981 | #endif 1982 | {"pgm", {load_pgm, save_pgm, query_pgm}}, 1983 | #ifndef HALIDE_NO_PNG 1984 | {"png", {load_png, save_png, query_png}}, 1985 | #endif 1986 | {"ppm", {load_ppm, save_ppm, query_ppm}}, 1987 | {"tmp", {load_tmp, save_tmp, query_tmp}}, 1988 | {"mat", {load_mat, save_mat, query_mat}}, 1989 | {"tiff", {load_tiff, save_tiff, query_tiff}}, 1990 | }; 1991 | std::string ext = Internal::get_lowercase_extension(filename); 1992 | auto it = m.find(ext); 1993 | if (it != m.end()) { 1994 | *result = it->second; 1995 | return true; 1996 | } 1997 | 1998 | std::string err = "unsupported file extension \"" + ext + "\", supported are:"; 1999 | for (auto &it : m) { 2000 | err += " " + it.first; 2001 | } 2002 | err += "\n"; 2003 | return check(false, err.c_str()); 2004 | } 2005 | 2006 | template 2007 | FormatInfo best_save_format(const ImageType &im, const std::set &info) { 2008 | // A bit ad hoc, but will do for now: 2009 | // Perfect score is zero (exact match). 2010 | // The larger the score, the worse the match. 2011 | int best_score = 0x7fffffff; 2012 | FormatInfo best{}; 2013 | const halide_type_t im_type = im.type(); 2014 | const int im_dimensions = im.dimensions(); 2015 | for (auto &f : info) { 2016 | int score = 0; 2017 | // If format has too-few dimensions, that's very bad. 2018 | score += std::max(0, im_dimensions - f.dimensions) * 1024; 2019 | // If format has too-few bits, that's pretty bad. 2020 | score += std::max(0, im_type.bits - f.type.bits) * 8; 2021 | // If format has too-many bits, that's a little bad. 2022 | score += std::max(0, f.type.bits - im_type.bits); 2023 | // If format has different code, that's a little bad. 2024 | score += (f.type.code != im_type.code) ? 1 : 0; 2025 | if (score < best_score) { 2026 | best_score = score; 2027 | best = f; 2028 | } 2029 | } 2030 | 2031 | return best; 2032 | } 2033 | 2034 | } // namespace Internal 2035 | 2036 | struct ImageTypeConversion { 2037 | // Convert an Image from one ElemType to another, where the src and 2038 | // dst types are statically known (e.g. Buffer -> Buffer). 2039 | // Note that this does conversion with scaling -- intepreting integers 2040 | // as fixed-point numbers between 0 and 1 -- not merely C-style casting. 2041 | // 2042 | // You'd normally call this with an explicit type for DstElemType and 2043 | // allow ImageType to be inferred, e.g. 2044 | // Buffer src = ...; 2045 | // Buffer dst = convert_image(src); 2046 | template::value>::type * = nullptr> 2048 | static auto convert_image(const ImageType &src) -> 2049 | typename Internal::ImageTypeWithElemType::type { 2050 | // The enable_if ensures this will never fire; this is here primarily 2051 | // as documentation and a backstop against breakage. 2052 | static_assert(ImageType::has_static_halide_type, 2053 | "This variant of convert_image() requires a statically-typed image"); 2054 | 2055 | using SrcImageType = ImageType; 2056 | using SrcElemType = typename SrcImageType::ElemType; 2057 | 2058 | using DstImageType = typename Internal::ImageTypeWithElemType::type; 2059 | 2060 | DstImageType dst = DstImageType::make_with_shape_of(src); 2061 | const auto converter = [](DstElemType &dst_elem, SrcElemType src_elem) { 2062 | dst_elem = Internal::convert(src_elem); 2063 | }; 2064 | dst.for_each_value(converter, src); 2065 | dst.set_host_dirty(); 2066 | 2067 | return dst; 2068 | } 2069 | 2070 | // Convert an Image from one ElemType to another, where the dst type is statically 2071 | // known but the src type is not (e.g. Buffer<> -> Buffer). 2072 | // You'd normally call this with an explicit type for DstElemType and 2073 | // allow ImageType to be inferred, e.g. 2074 | // Buffer src = ...; 2075 | // Buffer dst = convert_image(src); 2076 | template::value>::type * = nullptr> 2078 | static auto convert_image(const ImageType &src) -> 2079 | typename Internal::ImageTypeWithElemType::type { 2080 | // The enable_if ensures this will never fire; this is here primarily 2081 | // as documentation and a backstop against breakage. 2082 | static_assert(!ImageType::has_static_halide_type, 2083 | "This variant of convert_image() requires a dynamically-typed image"); 2084 | 2085 | const halide_type_t src_type = src.type(); 2086 | switch (Internal::halide_type_code((halide_type_code_t)src_type.code, src_type.bits)) { 2087 | case Internal::halide_type_code(halide_type_float, 32): 2088 | return convert_image(src.template as()); 2089 | case Internal::halide_type_code(halide_type_float, 64): 2090 | return convert_image(src.template as()); 2091 | case Internal::halide_type_code(halide_type_int, 8): 2092 | return convert_image(src.template as()); 2093 | case Internal::halide_type_code(halide_type_int, 16): 2094 | return convert_image(src.template as()); 2095 | case Internal::halide_type_code(halide_type_int, 32): 2096 | return convert_image(src.template as()); 2097 | case Internal::halide_type_code(halide_type_int, 64): 2098 | return convert_image(src.template as()); 2099 | case Internal::halide_type_code(halide_type_uint, 1): 2100 | return convert_image(src.template as()); 2101 | case Internal::halide_type_code(halide_type_uint, 8): 2102 | return convert_image(src.template as()); 2103 | case Internal::halide_type_code(halide_type_uint, 16): 2104 | return convert_image(src.template as()); 2105 | case Internal::halide_type_code(halide_type_uint, 32): 2106 | return convert_image(src.template as()); 2107 | case Internal::halide_type_code(halide_type_uint, 64): 2108 | return convert_image(src.template as()); 2109 | default: 2110 | assert(false && "Unsupported type"); 2111 | using DstImageType = typename Internal::ImageTypeWithElemType::type; 2112 | return DstImageType(); 2113 | } 2114 | } 2115 | 2116 | // Convert an Image from one ElemType to another, where the src type 2117 | // is statically known but the dst type is not 2118 | // (e.g. Buffer -> Buffer<>(halide_type_t)). 2119 | template::value>::type * = nullptr> 2122 | static auto convert_image(const ImageType &src, const halide_type_t &dst_type) -> 2123 | typename Internal::ImageTypeWithElemType::type { 2124 | // The enable_if ensures this will never fire; this is here primarily 2125 | // as documentation and a backstop against breakage. 2126 | static_assert(ImageType::has_static_halide_type, 2127 | "This variant of convert_image() requires a statically-typed image"); 2128 | 2129 | // Call the appropriate static-to-static conversion routine 2130 | // based on the desired dst type. 2131 | switch (Internal::halide_type_code((halide_type_code_t)dst_type.code, dst_type.bits)) { 2132 | case Internal::halide_type_code(halide_type_float, 32): 2133 | return convert_image(src); 2134 | case Internal::halide_type_code(halide_type_float, 64): 2135 | return convert_image(src); 2136 | case Internal::halide_type_code(halide_type_int, 8): 2137 | return convert_image(src); 2138 | case Internal::halide_type_code(halide_type_int, 16): 2139 | return convert_image(src); 2140 | case Internal::halide_type_code(halide_type_int, 32): 2141 | return convert_image(src); 2142 | case Internal::halide_type_code(halide_type_int, 64): 2143 | return convert_image(src); 2144 | case Internal::halide_type_code(halide_type_uint, 1): 2145 | return convert_image(src); 2146 | case Internal::halide_type_code(halide_type_uint, 8): 2147 | return convert_image(src); 2148 | case Internal::halide_type_code(halide_type_uint, 16): 2149 | return convert_image(src); 2150 | case Internal::halide_type_code(halide_type_uint, 32): 2151 | return convert_image(src); 2152 | case Internal::halide_type_code(halide_type_uint, 64): 2153 | return convert_image(src); 2154 | default: 2155 | assert(false && "Unsupported type"); 2156 | return ImageType(); 2157 | } 2158 | } 2159 | 2160 | // Convert an Image from one ElemType to another, where neither src type 2161 | // nor dst type are statically known 2162 | // (e.g. Buffer<>(halide_type_t) -> Buffer<>(halide_type_t)). 2163 | template::value>::type * = nullptr> 2166 | static auto convert_image(const ImageType &src, const halide_type_t &dst_type) -> 2167 | typename Internal::ImageTypeWithElemType::type { 2168 | // The enable_if ensures this will never fire; this is here primarily 2169 | // as documentation and a backstop against breakage. 2170 | static_assert(!ImageType::has_static_halide_type, 2171 | "This variant of convert_image() requires a dynamically-typed image"); 2172 | 2173 | // Sniff the runtime type of src, coerce it to that type using as<>(), 2174 | // and call the static-to-dynamic variant of this method. (Note that 2175 | // this forces instantiation of the complete any-to-any conversion 2176 | // matrix of code.) 2177 | const halide_type_t src_type = src.type(); 2178 | switch (Internal::halide_type_code((halide_type_code_t)src_type.code, src_type.bits)) { 2179 | case Internal::halide_type_code(halide_type_float, 32): 2180 | return convert_image(src.template as(), dst_type); 2181 | case Internal::halide_type_code(halide_type_float, 64): 2182 | return convert_image(src.template as(), dst_type); 2183 | case Internal::halide_type_code(halide_type_int, 8): 2184 | return convert_image(src.template as(), dst_type); 2185 | case Internal::halide_type_code(halide_type_int, 16): 2186 | return convert_image(src.template as(), dst_type); 2187 | case Internal::halide_type_code(halide_type_int, 32): 2188 | return convert_image(src.template as(), dst_type); 2189 | case Internal::halide_type_code(halide_type_int, 64): 2190 | return convert_image(src.template as(), dst_type); 2191 | case Internal::halide_type_code(halide_type_uint, 1): 2192 | return convert_image(src.template as(), dst_type); 2193 | case Internal::halide_type_code(halide_type_uint, 8): 2194 | return convert_image(src.template as(), dst_type); 2195 | case Internal::halide_type_code(halide_type_uint, 16): 2196 | return convert_image(src.template as(), dst_type); 2197 | case Internal::halide_type_code(halide_type_uint, 32): 2198 | return convert_image(src.template as(), dst_type); 2199 | case Internal::halide_type_code(halide_type_uint, 64): 2200 | return convert_image(src.template as(), dst_type); 2201 | default: 2202 | assert(false && "Unsupported type"); 2203 | return ImageType(); 2204 | } 2205 | } 2206 | }; 2207 | 2208 | // Load the Image from the given file. 2209 | // If output Image has a static type, and the loaded image cannot be stored 2210 | // in such an image without losing data, fail. 2211 | // Returns false upon failure. 2212 | template 2213 | bool load(const std::string &filename, ImageType *im) { 2214 | using DynamicImageType = typename Internal::ImageTypeWithElemType::type; 2215 | Internal::ImageIO imageio; 2216 | if (!Internal::find_imageio(filename, &imageio)) { 2217 | return false; 2218 | } 2219 | using DynamicImageType = typename Internal::ImageTypeWithElemType::type; 2220 | DynamicImageType im_d; 2221 | if (!imageio.load(filename, &im_d)) { 2222 | return false; 2223 | } 2224 | // Allow statically-typed images to be passed as the out-param, but do 2225 | // a runtime check to ensure 2226 | if (ImageType::has_static_halide_type) { 2227 | const halide_type_t expected_type = ImageType::static_halide_type(); 2228 | if (!check(im_d.type() == expected_type, "Image loaded did not match the expected type")) { 2229 | return false; 2230 | } 2231 | } 2232 | *im = im_d.template as(); 2233 | im->set_host_dirty(); 2234 | return true; 2235 | } 2236 | 2237 | // Save the Image in the format associated with the filename's extension. 2238 | // If the format can't represent the Image without losing data, fail. 2239 | // Returns false upon failure. 2240 | template 2241 | bool save(ImageType &im, const std::string &filename) { 2242 | using DynamicImageType = typename Internal::ImageTypeWithElemType::type; 2243 | Internal::ImageIO imageio; 2244 | if (!Internal::find_imageio(filename, &imageio)) { 2245 | return false; 2246 | } 2247 | if (!check(imageio.query().count({im.type(), im.dimensions()}) > 0, "Image cannot be saved in this format")) { 2248 | return false; 2249 | } 2250 | 2251 | // Allow statically-typed images to be passed in, but quietly pass them on 2252 | // as dynamically-typed images. 2253 | auto im_d = im.template as(); 2254 | return imageio.save(im_d, filename); 2255 | } 2256 | 2257 | // Return a set of FormatInfo structs that contain the legal type-and-dimensions 2258 | // that can be saved in this format. Most applications won't ever need to use 2259 | // this call. Returns false upon failure. 2260 | template 2261 | bool save_query(const std::string &filename, std::set *info) { 2262 | using DynamicImageType = typename Internal::ImageTypeWithElemType::type; 2263 | Internal::ImageIO imageio; 2264 | if (!Internal::find_imageio(filename, &imageio)) { 2265 | return false; 2266 | } 2267 | *info = imageio.query(); 2268 | return true; 2269 | } 2270 | 2271 | // Fancy wrapper to call load() with CheckFail, inferring the return type; 2272 | // this allows you to simply use 2273 | // 2274 | // Image im = load_image("filename"); 2275 | // 2276 | // without bothering to check error results (all errors simply abort). 2277 | // 2278 | // Note that if the image being loaded doesn't match the static type and 2279 | // dimensions of of the image on the LHS, a runtime error will occur. 2280 | class load_image { 2281 | public: 2282 | load_image(const std::string &f) 2283 | : filename(f) { 2284 | } 2285 | 2286 | template 2287 | operator ImageType() { 2288 | using DynamicImageType = typename Internal::ImageTypeWithElemType::type; 2289 | DynamicImageType im_d; 2290 | (void)load(filename, &im_d); 2291 | Internal::CheckFail(ImageType::can_convert_from(im_d), 2292 | "Type mismatch assigning the result of load_image. " 2293 | "Did you mean to use load_and_convert_image?"); 2294 | return im_d.template as(); 2295 | } 2296 | 2297 | private: 2298 | const std::string filename; 2299 | }; 2300 | 2301 | // Like load_image, but quietly convert the loaded image to the type of the LHS 2302 | // if necessary, discarding information if necessary. 2303 | class load_and_convert_image { 2304 | public: 2305 | load_and_convert_image(const std::string &f) 2306 | : filename(f) { 2307 | } 2308 | 2309 | template 2310 | inline operator ImageType() { 2311 | using DynamicImageType = typename Internal::ImageTypeWithElemType::type; 2312 | DynamicImageType im_d; 2313 | (void)load(filename, &im_d); 2314 | const halide_type_t expected_type = ImageType::static_halide_type(); 2315 | if (im_d.type() == expected_type) { 2316 | return im_d.template as(); 2317 | } else { 2318 | return ImageTypeConversion::convert_image(im_d); 2319 | } 2320 | } 2321 | 2322 | private: 2323 | const std::string filename; 2324 | }; 2325 | 2326 | // Fancy wrapper to call save() with CheckFail; this allows you to simply use 2327 | // 2328 | // save_image(im, "filename"); 2329 | // 2330 | // without bothering to check error results (all errors simply abort). 2331 | // 2332 | // If the specified image file format cannot represent the image without 2333 | // losing data (e.g, a float32 or 4-dimensional image saved as a JPEG), 2334 | // a runtime error will occur. 2335 | template 2336 | void save_image(ImageType &im, const std::string &filename) { 2337 | (void)save(im, filename); 2338 | } 2339 | 2340 | // Like save_image, but quietly convert the saved image to a type that the 2341 | // specified image file format can hold, discarding information if necessary. 2342 | // (Note that the input image is unaffected!) 2343 | template 2344 | void convert_and_save_image(ImageType &im, const std::string &filename) { 2345 | // We'll be doing any conversion on the CPU 2346 | im.copy_to_host(); 2347 | 2348 | std::set info; 2349 | (void)save_query(filename, &info); 2350 | const FormatInfo best = Internal::best_save_format(im, info); 2351 | if (best.type == im.type() && best.dimensions == im.dimensions()) { 2352 | // It's an exact match, we can save as-is. 2353 | (void)save(im, filename); 2354 | } else { 2355 | using DynamicImageType = typename Internal::ImageTypeWithElemType::type; 2356 | DynamicImageType im_converted = ImageTypeConversion::convert_image(im, best.type); 2357 | while (im_converted.dimensions() < best.dimensions) { 2358 | im_converted.add_dimension(); 2359 | } 2360 | (void)save(im_converted, filename); 2361 | } 2362 | } 2363 | 2364 | } // namespace Tools 2365 | } // namespace Halide 2366 | 2367 | #endif // HALIDE_IMAGE_IO_H 2368 | -------------------------------------------------------------------------------- /include/third_party/halide_target_check.h: -------------------------------------------------------------------------------- 1 | #include "Halide.h" 2 | 3 | using namespace Halide; 4 | Target find_gpu_target() { 5 | // Start with a target suitable for the machine you're running this on. 6 | Target target = get_host_target(); 7 | 8 | std::vector features_to_try; 9 | if (target.os == Target::Windows) { 10 | // Try D3D12 first; if that fails, try OpenCL. 11 | if (sizeof(void*) == 8) { 12 | // D3D12Compute support is only available on 64-bit systems at present. 13 | features_to_try.push_back(Target::D3D12Compute); 14 | } 15 | features_to_try.push_back(Target::OpenCL); 16 | } else if (target.os == Target::OSX) { 17 | // OS X doesn't update its OpenCL drivers, so they tend to be broken. 18 | // CUDA would also be a fine choice on machines with NVidia GPUs. 19 | features_to_try.push_back(Target::Metal); 20 | } else { 21 | features_to_try.push_back(Target::OpenCL); 22 | } 23 | // Uncomment the following lines to also try CUDA: 24 | // features_to_try.push_back(Target::CUDA); 25 | 26 | for (Target::Feature f : features_to_try) { 27 | Target new_target = target.with_feature(f); 28 | if (host_supports_target_device(new_target)) { 29 | return new_target; 30 | } 31 | } 32 | 33 | printf("Requested GPU(s) are not supported. (Do you have the proper hardware and/or driver installed?)\n"); 34 | return target; 35 | } 36 | -------------------------------------------------------------------------------- /spook.cpp: -------------------------------------------------------------------------------- 1 | // A example to illustrate the usage of skeletonide. 2 | // We load an in image and call the pipeline in a loop. 3 | // TODO Check for completion flags 4 | 5 | #include "skeletonide.h" 6 | #include "halide_target_check.h" 7 | #include "halide_image_io.h" 8 | #include 9 | #include 10 | 11 | #define ITERS 100 12 | 13 | using namespace Halide; 14 | using namespace Halide::Tools; 15 | 16 | int main(int argc, char **argv){ 17 | Target target = find_gpu_target(); 18 | if (!target.has_gpu_feature()) { 19 | return -1; 20 | } 21 | Runtime::Buffer input = Tools::load_and_convert_image("test/images/tiled_horse.png"); 22 | Runtime::Buffer output(input.width(), input.height()); 23 | auto orig_input = input; 24 | auto t1 = std::chrono::high_resolution_clock::now(); 25 | for(int j=0; j(t2 - t1).count(); 34 | printf("Mean time to skeletonize: %fms\n", dur /(float)ITERS); 35 | Tools::convert_and_save_image(output, "spook_out.png"); 36 | printf("Image saved\n"); 37 | return 0; 38 | } -------------------------------------------------------------------------------- /src/pipeline.cpp: -------------------------------------------------------------------------------- 1 | #include "Halide.h" 2 | #include "halide_target_check.h" 3 | #include "halide_image_io.h" 4 | 5 | using namespace Halide; 6 | 7 | // The pipeline below is a single iteration of Zhang-Suen algorithm. 8 | int main(int argc, char **argv) { 9 | Target target = find_gpu_target(); 10 | if (!target.has_gpu_feature()) { 11 | return false; 12 | } 13 | ImageParam input(type_of(), 2); 14 | Expr W = input.width(); 15 | Expr H = input.height(); 16 | 17 | Var x_outer, x_inner, y_outer, y_inner, tile_index; 18 | 19 | // offsets of neighbours of a given pixel (x, y) 20 | int n_x_idx[8] = {-1, -1, 0, 1, 1, 1, 0, -1}; 21 | int n_y_idx[8] = {0, 1, 1, 1, 0, -1, -1, -1}; 22 | Buffer n_x_idx_buf(n_x_idx); 23 | Buffer n_y_idx_buf(n_y_idx); 24 | 25 | Var x, y, k; 26 | Func in_bounded = BoundaryConditions::repeat_edge(input); 27 | 28 | // fn to get the nth neighbours of a pixel 29 | Func nbr; 30 | nbr(x, y, k) = in_bounded(x + n_x_idx_buf(k), y + n_y_idx_buf(k)); 31 | 32 | // fn to count the number of non-zero neighbours 33 | RDom j(0, 8); 34 | Func nbr_cnt("nbr_cnt"); 35 | nbr_cnt(x, y) = sum(select(nbr(x, y, j) > 0, 1, 0)); 36 | 37 | // fn to count the number of transitions 38 | Func zero2one("zero2one"); 39 | RDom t(0, 8); 40 | Expr zero2one_cond = nbr(x, y, t % 8) == 0 && nbr(x, y, (t + 1) % 8) == 255; 41 | zero2one(x, y) = sum(select(zero2one_cond, 1, 0)); 42 | 43 | // step-1 of Zhang-Suen method 44 | Expr fst_cnd; 45 | fst_cnd = (nbr_cnt(x, y) >= 2 && nbr_cnt(x, y) <= 6 && zero2one(x, y) == 1 && 46 | nbr(x, y, 0) * nbr(x, y, 2) * nbr(x, y, 4) == 0 && 47 | nbr(x, y, 0) * nbr(x, y, 2) * nbr(x, y, 6) == 0); 48 | 49 | Func skel1("skel1"); 50 | skel1(x, y) = cast(select(fst_cnd, 255, in_bounded(x, y))); 51 | // skel1.compute_root().vectorize(x, 8).parallel(y); 52 | 53 | Var c, i, block, thread, x0, y0, xi, yi; 54 | skel1.compute_root().gpu_tile(x, y, x0, y0, xi, yi, 8, 8); 55 | 56 | // step-2 of Zhang-Suen method 57 | // operate on the array modified in step-1 58 | Func nbr2; 59 | nbr2(x, y, k) = 60 | skel1(clamp(x + n_x_idx_buf(k), 0, W), clamp(y + n_y_idx_buf(k), 0, H)); 61 | 62 | RDom j2(0, 8); 63 | Func nbr_cnt2("nbr_cnt2"); 64 | nbr_cnt2(x, y) = sum(select(nbr2(x, y, j2) > 0, 1, 0)); 65 | 66 | Func zero2one2("zero2one2"); 67 | RDom t2(0, 8); 68 | Expr zero2one_cond2 = 69 | nbr2(x, y, t2 % 8) == 0 && nbr2(x, y, (t2 + 1) % 8) == 255; 70 | zero2one2(x, y) = sum(select(zero2one_cond2, 1, 0)); 71 | 72 | Expr snd_cnd; 73 | snd_cnd = 74 | (nbr_cnt2(x, y) >= 2 && nbr_cnt2(x, y) <= 6 && zero2one2(x, y) == 1 && 75 | nbr2(x, y, 0) * nbr2(x, y, 4) * nbr2(x, y, 6) == 0 && 76 | nbr2(x, y, 2) * nbr2(x, y, 4) * nbr2(x, y, 6) == 0); 77 | 78 | Func skel2("skel2"); 79 | skel2(x, y) = cast(select(snd_cnd, 255, skel1(x, y))); 80 | // skel2.vectorize(x, 8).parallel(y); 81 | skel2.gpu_tile(x, y, x0, y0, xi, yi, 8, 8); 82 | 83 | // Compiling to a static lib 84 | skel2.compile_to_static_library("skeletonide", {input}, "skel", target); 85 | 86 | return 0; 87 | } 88 | -------------------------------------------------------------------------------- /test/images/horse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmalloc/skeletonide/05f76ef65e7d29efb19ebd27bd88de8f5e5db46b/test/images/horse.png -------------------------------------------------------------------------------- /test/output/skel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmalloc/skeletonide/05f76ef65e7d29efb19ebd27bd88de8f5e5db46b/test/output/skel.png -------------------------------------------------------------------------------- /test/output/spook_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postmalloc/skeletonide/05f76ef65e7d29efb19ebd27bd88de8f5e5db46b/test/output/spook_out.png --------------------------------------------------------------------------------