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 }