1 /** 2 Module implements Image utility class, and basic API for image manipulation. 3 4 Image class encapsulates image properties with minimal functionality. It is primarily designed to be used as I/O unit. 5 For any image processing needs, image data can be sliced to mir.ndslice.slice.Slice. 6 7 Example: 8 ---- 9 Image image = new Image(32, 32, ImageFormat.IF_MONO, BitDepth.BD_32); 10 11 Slice!(Contiguous, [3], float*) slice = image.sliced!float; // slice image data, considering the data is of float type. 12 13 assert(image.height == slice.length!0 && image.width == slice.length!1); 14 assert(image.channels == 1); 15 16 image = slice.asImage(ImageFormat.IF_MONO); // create the image back from sliced data. 17 ---- 18 19 Copyright: Copyright Relja Ljubobratovic 2016. 20 21 Authors: Relja Ljubobratovic 22 23 License: $(LINK3 http://www.boost.org/LICENSE_1_0.txt, Boost Software License - Version 1.0). 24 */ 25 module dcv.core.image; 26 27 import std.exception : enforce; 28 29 public import mir.ndslice.slice; 30 import mir.ndslice.allocation; 31 32 33 /// Image (pixel) format. 34 enum ImageFormat 35 { 36 IF_UNASSIGNED = 0, /// Not assigned format. 37 IF_MONO, /// Mono, single channel format. 38 IF_MONO_ALPHA, /// Mono with alpha channel. 39 IF_RGB, /// RGB format. 40 IF_BGR, /// BGR format. 41 IF_YUV, /// YUV (YCbCr) format. 42 IF_RGB_ALPHA, /// RGB format with alpha. 43 IF_BGR_ALPHA /// BGR format with alpha. 44 } 45 46 immutable size_t[] imageFormatChannelCount = [0, // unassigned 47 1, // mono 48 2, // mono alpha 49 3, // rgb 50 3, // bgr 51 3, // yuv 52 4, // rgba 53 4 // bgra 54 ]; 55 56 /// Bit depth of a pixel in an image. 57 enum BitDepth : size_t 58 { 59 BD_UNASSIGNED = 0, /// Not assigned depth info. 60 BD_8 = 8, /// 8-bit (ubyte) depth type. 61 BD_16 = 16, /// 16-bit (ushort) depth type. 62 BD_32 = 32 /// 32-bit (float) depth type. 63 } 64 65 private pure nothrow @safe auto getDepthFromType(T)() 66 { 67 static if (is(T == ubyte)) 68 { 69 return BitDepth.BD_8; 70 } 71 else static if (is(T == ushort)) 72 { 73 return BitDepth.BD_16; 74 } 75 else static if (is(T == float)) 76 { 77 return BitDepth.BD_32; 78 } 79 else 80 { 81 return BitDepth.BD_UNASSIGNED; 82 } 83 } 84 85 unittest 86 { 87 assert(getDepthFromType!ubyte == BitDepth.BD_8); 88 assert(getDepthFromType!ushort == BitDepth.BD_16); 89 assert(getDepthFromType!float == BitDepth.BD_32); 90 assert(getDepthFromType!real == BitDepth.BD_UNASSIGNED); 91 } 92 93 /** 94 Image abstraction type. 95 */ 96 class Image 97 { 98 private: 99 // Format of an image. 100 ImageFormat _format = ImageFormat.IF_UNASSIGNED; 101 // Bit depth of a pixel: (8 - uchar, 16 - ushort, 32 - float) 102 BitDepth _depth = BitDepth.BD_UNASSIGNED; 103 // Width of the image. 104 size_t _width = 0; 105 // Height of the image. 106 size_t _height = 0; 107 // Image pixel (data) array. 108 ubyte[] _data = null; 109 110 public: 111 112 pure @safe nothrow this() 113 { 114 } 115 116 /** 117 Copy constructor. 118 119 Params: 120 copy = Input image, which is copied into this image structure. 121 deepCopy = if false (default) the data array will be referenced 122 from copy, esle values will be copied to newly allocated array. 123 */ 124 pure this(in Image copy, bool deepCopy = false) 125 { 126 if (copy is null || copy._data is null) 127 { 128 return; 129 } 130 _format = copy._format; 131 _depth = copy._depth; 132 _width = copy._width; 133 _height = copy._height; 134 if (deepCopy) 135 { 136 _data = new ubyte[copy._data.length]; 137 _data[] = copy._data[]; 138 } 139 else 140 { 141 _data = cast(ubyte[])copy._data; 142 } 143 } 144 145 unittest 146 { 147 Image image = new Image(null, false); 148 assert(image.width == 0); 149 assert(image.height == 0); 150 assert(image.format == ImageFormat.IF_UNASSIGNED); 151 assert(image.depth == BitDepth.BD_UNASSIGNED); 152 assert(image.data == null); 153 assert(image.empty == true); 154 } 155 156 /** 157 Construct an image by given size, format and bit depth information. 158 159 Params: 160 width = width of a newly created image. 161 height = height of a newly created image. 162 format = format of a newly created image. 163 depth = bit depth of a newly created image. 164 data = potential data of an image, pre-allocated. If not a null, data array 165 has to be of correct size = width*height*channels*depth, where channels are 166 defined by the format, and depth is counded in bytes. 167 */ 168 @safe pure nothrow this(size_t width, size_t height, ImageFormat format = ImageFormat.IF_RGB, 169 BitDepth depth = BitDepth.BD_8, ubyte[] data = null) 170 in 171 { 172 assert(width > 0 && height > 0); 173 assert(depth != BitDepth.BD_UNASSIGNED && format != ImageFormat.IF_UNASSIGNED); 174 if (data !is null) 175 { 176 assert(data.length == width * height * imageFormatChannelCount[cast(size_t)format] * (cast(size_t)depth / 8)); 177 } 178 } 179 body 180 { 181 _width = width; 182 _height = height; 183 _depth = depth; 184 _format = format; 185 _data = (data !is null) ? data : new ubyte[width * height * channels * (cast(size_t)depth / 8)]; 186 } 187 188 unittest 189 { 190 Image image = new Image(1, 1, ImageFormat.IF_BGR, BitDepth.BD_8); 191 assert(image.isOfType!ubyte); 192 } 193 194 unittest 195 { 196 Image image = new Image(1, 1, ImageFormat.IF_BGR, BitDepth.BD_16); 197 assert(image.isOfType!ushort); 198 } 199 200 unittest 201 { 202 Image image = new Image(1, 1, ImageFormat.IF_BGR, BitDepth.BD_32); 203 assert(image.isOfType!float); 204 } 205 206 unittest 207 { 208 import std.algorithm.comparison : equal; 209 210 immutable width = 10; 211 immutable height = 15; 212 immutable format = ImageFormat.IF_BGR; 213 immutable depth = BitDepth.BD_8; 214 immutable channels = 3; 215 Image image = new Image(width, height, format, depth); 216 assert(image.width == width); 217 assert(image.height == height); 218 assert(image.format == format); 219 assert(image.channels == channels); 220 assert(image.depth == depth); 221 assert(image.empty == false); 222 assert(image.size == cast(size_t[3])[width, height, channels]); 223 } 224 225 unittest 226 { 227 immutable width = 10; 228 immutable height = 15; 229 immutable format = ImageFormat.IF_BGR; 230 immutable depth = BitDepth.BD_8; 231 immutable channels = 3; 232 Image image = new Image(width, height, format, depth); 233 Image copy = new Image(image, false); 234 assert(copy.width == image.width); 235 assert(copy.height == image.height); 236 assert(copy.channels == image.channels); 237 assert(copy.format == image.format); 238 assert(copy.depth == image.depth); 239 assert(copy.data.ptr == image.data.ptr); 240 } 241 242 unittest 243 { 244 immutable width = 10; 245 immutable height = 15; 246 immutable format = ImageFormat.IF_BGR; 247 immutable depth = BitDepth.BD_8; 248 immutable channels = 3; 249 Image image = new Image(width, height, format, depth); 250 Image copy = new Image(image, true); 251 assert(copy.width == image.width); 252 assert(copy.height == image.height); 253 assert(copy.channels == image.channels); 254 assert(copy.format == image.format); 255 assert(copy.depth == image.depth); 256 assert(copy.data.ptr != image.data.ptr); 257 } 258 259 /// Get format of an image. 260 @property auto format() const @safe pure nothrow 261 { 262 return _format; 263 } 264 /// Get height of an image. 265 @property auto width() const @safe pure nothrow 266 { 267 return _width; 268 } 269 /// Get height of an image. 270 @property auto height() const @safe pure nothrow 271 { 272 return _height; 273 } 274 /// Get bit depth of the image. 275 @property auto depth() const @safe pure nothrow 276 { 277 return _depth; 278 } 279 /// Check if image is empty (there's no data present). 280 @property auto empty() const @safe pure nothrow 281 { 282 return _data is null; 283 } 284 /// Channel count of the image. 285 @property auto channels() const @safe pure nothrow 286 { 287 return imageFormatChannelCount[cast(int)format]; 288 } 289 290 /// Number of bytes contained in one pixel of the image. 291 @property auto pixelSize() const @safe pure nothrow 292 { 293 return channels * (cast(size_t)_depth / 8); 294 } 295 /// Number of bytes contained in the image. 296 @property auto byteSize() const @safe pure nothrow 297 { 298 return width * height * pixelSize; 299 } 300 /// Number of bytes contained in one row of the image. 301 @property auto rowStride() const @safe pure nothrow 302 { 303 return pixelSize * _width; 304 } 305 306 /// Size of the image. 307 /// Returns an array of 3 sizes: [width, height, channels] 308 @property size_t[3] size() const @safe pure nothrow 309 { 310 return [width, height, channels]; 311 } 312 313 /** 314 Check if this images data corresponds to given value type. 315 316 Given value type is checked against the image data bit depth. 317 Data of 8-bit image is considered to be typed as ubyte array, 318 16-bit as ushort, and 32-bit as float array. Any other type as 319 input returns false result. 320 321 Params: 322 T = (template parameter) value type which is tested against the bit depth of the image data. 323 */ 324 @safe pure nothrow const bool isOfType(T)() 325 { 326 return (depth != BitDepth.BD_UNASSIGNED && ((depth == BitDepth.BD_8 && is(T == ubyte)) 327 || (depth == BitDepth.BD_16 && is(T == ushort)) || (depth == BitDepth.BD_32 && is(T == float)))); 328 } 329 330 @safe pure nothrow unittest 331 { 332 Image image = new Image(1, 1, ImageFormat.IF_BGR, BitDepth.BD_8); 333 assert(image.isOfType!ubyte); 334 assert(!image.isOfType!ushort); 335 assert(!image.isOfType!float); 336 assert(!image.isOfType!real); 337 } 338 339 @safe pure nothrow unittest 340 { 341 Image image = new Image(1, 1, ImageFormat.IF_BGR, BitDepth.BD_16); 342 assert(!image.isOfType!ubyte); 343 assert(image.isOfType!ushort); 344 assert(!image.isOfType!float); 345 assert(!image.isOfType!real); 346 } 347 348 @safe pure nothrow unittest 349 { 350 Image image = new Image(1, 1, ImageFormat.IF_BGR, BitDepth.BD_32); 351 assert(!image.isOfType!ubyte); 352 assert(!image.isOfType!ushort); 353 assert(image.isOfType!float); 354 assert(!image.isOfType!real); 355 } 356 357 /** 358 Convert image data type to given type. 359 360 Creates new image with data typed as given value type. 361 If this image's data type is the same as given type, deep 362 copy of this image is returned. 363 364 Params: 365 T = (template parameter) value type to which image's data is converted. 366 367 Returns: 368 Copy of this image with casted data to given type. If given type is same as 369 current data of this image, deep copy is returned. 370 */ 371 inout auto asType(T)() 372 in 373 { 374 assert(_data); 375 static assert(is(T == ubyte) || is(T == ushort) || is(T == float), 376 "Given type is invalid - only ubyte (8) ushort(16) or float(32) are supported"); 377 } 378 body 379 { 380 import std.range : lockstep; 381 import std.algorithm.mutation : copy; 382 import std.traits : isAssignable; 383 384 auto depth = getDepthFromType!T; 385 if (depth == _depth) 386 return new Image(this, true); 387 388 Image newim = new Image(width, height, format, depth); 389 390 if (_depth == BitDepth.BD_8) 391 { 392 foreach (v1, ref v2; lockstep(data!ubyte, newim.data!T)) 393 { 394 v2 = cast(T)v1; 395 } 396 } 397 else if (_depth == BitDepth.BD_16) 398 { 399 foreach (v1, ref v2; lockstep(data!ushort, newim.data!T)) 400 { 401 v2 = cast(T)v1; 402 } 403 } 404 else if (_depth == BitDepth.BD_32) 405 { 406 foreach (v1, ref v2; lockstep(data!float, newim.data!T)) 407 { 408 v2 = cast(T)v1; 409 } 410 } 411 412 return newim; 413 } 414 415 /** 416 Get data array from this image. 417 418 Cast data array to corresponding dynamic array type, 419 and return it. 420 8-bit data is considered ubyte, 16-bit ushort, and 32-bit float. 421 422 Params: 423 T = (template parameter) value type (default ubyte) to which data array is casted to. 424 */ 425 pure inout auto data(T = ubyte)() 426 { 427 import std.range : ElementType; 428 429 if (_data is null) 430 { 431 return null; 432 } 433 static assert(is(T == ubyte) || is(T == ushort) || is(T == float), 434 "Pixel data type not supported. Supported ones are: ubyte(8bit), ushort(16bit), float(32bit)"); 435 enforce(isOfType!T, "Invalid pixel data type cast."); 436 static if (is(ElemetType!(typeof(_data)) == T)) 437 return _data; 438 else 439 return cast(T[])_data; 440 } 441 442 override string toString() const 443 { 444 import std.conv : to; 445 446 return "Image [" ~ width.to!string ~ "x" ~ height.to!string ~ "]"; 447 } 448 449 auto sliced(T = ubyte)() inout 450 { 451 return data!T.sliced(height, width, channels); 452 } 453 } 454 455 version (unittest) 456 { 457 import std.range : iota, lockstep; 458 import std.array : array; 459 import std.algorithm.iteration : map; 460 461 immutable width = 3; 462 immutable height = 3; 463 immutable format = ImageFormat.IF_MONO; 464 immutable depth = BitDepth.BD_8; 465 auto data = (width * height).iota.map!(v => cast(ubyte)v).array; 466 } 467 468 unittest 469 { 470 Image image = new Image; 471 assert(image.format == ImageFormat.IF_UNASSIGNED); 472 assert(image.depth == BitDepth.BD_UNASSIGNED); 473 assert(image.width == 0); 474 assert(image.height == 0); 475 assert(image.data == null); 476 assert(image.empty == true); 477 } 478 479 // Image.asType! 480 unittest 481 { 482 Image image = new Image(width, height, format, depth, data); 483 Image sameImage = image.asType!ubyte; 484 assert(image.data == sameImage.data); 485 assert(image.data.ptr != sameImage.data.ptr); 486 } 487 488 unittest 489 { 490 Image image = new Image(width, height, format, depth, data); 491 assert(image.data.ptr == data.ptr); 492 assert(image.width == width); 493 assert(image.height == height); 494 Image oImage = image.asType!ushort; 495 assert(oImage.width == image.width); 496 assert(oImage.height == image.height); 497 foreach (bv, fv; lockstep(image.data!ubyte, oImage.data!ushort)) 498 { 499 assert(cast(ushort)bv == fv); 500 } 501 } 502 503 unittest 504 { 505 ubyte[] shdata = new ubyte[18]; 506 ubyte* ptr = cast(ubyte*)(data.map!(v => cast(ushort)v).array.ptr); 507 shdata[] = ptr[0 .. width * height * 2][]; 508 Image image = new Image(width, height, format, BitDepth.BD_16, shdata); 509 Image oImage = image.asType!float; 510 assert(oImage.width == image.width); 511 assert(oImage.height == image.height); 512 foreach (bv, fv; lockstep(image.data!ushort, oImage.data!float)) 513 { 514 assert(cast(float)bv == fv); 515 } 516 } 517 518 unittest 519 { 520 size_t floatsize = width * height * 4; 521 ubyte[] fdata = new ubyte[floatsize]; 522 ubyte* ptr = cast(ubyte*)(data.map!(v => cast(float)v).array.ptr); 523 fdata[] = ptr[0 .. floatsize][]; 524 Image image = new Image(width, height, format, BitDepth.BD_32, fdata); 525 Image oImage = image.asType!ushort; 526 assert(oImage.width == image.width); 527 assert(oImage.height == image.height); 528 foreach (bv, fv; lockstep(image.data!float, oImage.data!ushort)) 529 { 530 assert(cast(ushort)bv == fv); 531 } 532 } 533 534 /** 535 Convert a ndslice object to an Image, with defined image format. 536 */ 537 Image asImage(SliceKind kind, size_t[] packs, T)(Slice!(kind, packs, T*) slice, ImageFormat format) 538 { 539 static assert(packs.length == 1, "Packed slices are not supported."); 540 541 BitDepth depth = getDepthFromType!T; 542 enforce(depth != BitDepth.BD_UNASSIGNED, "Invalid type of slice for convertion to image: ", T.stringof); 543 544 static if (packs[0] == 2) 545 { 546 ubyte* _iterator = cast(ubyte*)slice.slice._iterator; 547 ubyte[] s_arr = _iterator[0 .. slice.elementsCount * T.sizeof][]; 548 enforce(format == 1, "Invalid image format - has to be single channel"); 549 return new Image(slice.shape[1], slice.shape[0], format, depth, s_arr); 550 } 551 else static if (packs[0] == 3) 552 { 553 ubyte* _iterator = cast(ubyte*)slice.slice._iterator; 554 ubyte[] s_arr = _iterator[0 .. slice.elementsCount * T.sizeof][]; 555 auto ch = slice.shape[2]; 556 enforce(ch >= 1 && ch <= 4, 557 "Invalid slice shape - third dimension should contain from 1(grayscale) to 4(rgba) values."); 558 enforce(ch == imageFormatChannelCount[format], "Invalid image format - channel count missmatch"); 559 return new Image(slice.shape[1], slice.shape[0], format, depth, s_arr); 560 } 561 else 562 { 563 static assert(0, "Invalid slice dimension - should be 2(mono image) or 3(channel image) dimensional."); 564 } 565 } 566 567 /** 568 Convert ndslice object into an image, with default format setup, regarding to slice dimension. 569 */ 570 Image asImage(SliceKind kind, size_t[] packs, T)(Slice!(kind, packs, T*) slice) 571 { 572 enum N = packs[0]; 573 574 ImageFormat format; 575 static if (N == 2) 576 { 577 format = ImageFormat.IF_MONO; 578 } 579 else static if (N == 3) 580 { 581 switch (slice.length!2) 582 { 583 case 1: 584 format = ImageFormat.IF_MONO; 585 break; 586 case 2: 587 format = ImageFormat.IF_MONO_ALPHA; 588 break; 589 case 3: 590 format = ImageFormat.IF_RGB; 591 break; 592 case 4: 593 format = ImageFormat.IF_RGB_ALPHA; 594 break; 595 default: 596 import std.conv : to; 597 598 assert(0, "Invalid channel count: " ~ slice.length!2.to!string); 599 } 600 } 601 else 602 { 603 static assert(0, "Invalid slice dimension - should be 2(mono image) or 3(channel image) dimensional."); 604 } 605 return slice.asImage(format); 606 }