1 module photog.utils;
2 
3 import mir.ndslice : Slice, sliced, SliceKind;
4 
5 /**
6 Convert [M, N, P] slice to [MxN] slice of [P] arrays.
7 */
8 auto pixelPack(size_t chnls, InputType, size_t dims, SliceKind kind)(
9         Slice!(InputType*, dims, kind) image)
10 in
11 {
12     assert(dims == 3, "Image must have 3 dimensions.");
13 }
14 do
15 {
16     // TODO: How to we specify strides? e.g. Slice!(ReturnType*, 1, kind)(shape, strides, iterator);
17     size_t[1] shape = [image.shape[0] * image.shape[1]];
18     alias ReturnType = InputType[chnls];
19     ReturnType* iterator = cast(ReturnType*) image.iterator;
20     return Slice!(ReturnType*, 1, kind)(shape, iterator);
21 }
22 
23 ///
24 unittest
25 {
26     // dfmt off
27     Slice!(double*, 3) rgb = [
28         1.0, 0.0, 0.0,
29         0.0, 1.0, 0.0,
30         0.0, 0.0, 1.0,
31         0.470588, 0.470588, 0.470588
32     ].sliced(4, 1, 3);
33     // dfmt on
34 
35     auto packed = pixelPack!3(rgb);
36     assert(packed.shape == [4 * 1]);
37     assert(packed[0].length == 3);
38 }
39 
40 /**
41 Convert [MxN] slice of [P] arrays to [M, N, P] slice.
42 */
43 auto pixelUnpack(size_t chnls, InputType, size_t dims, SliceKind kind)(
44         Slice!(InputType*, dims, kind) image, size_t height, size_t width)
45 in
46 {
47     // TODO: Validate chnls * width * height == # of elements in array underlying image slice.
48     assert(dims == 1, "Image must have 1 dimension.");
49 }
50 do
51 {
52     size_t[3] shape = [height, width, chnls];
53     alias ReturnType = IteratorType!InputType;
54     ReturnType* iterator = cast(ReturnType*) image.iterator;
55     return Slice!(ReturnType*, 3, kind)(shape, iterator);
56 }
57 
58 ///
59 unittest
60 {
61     // dfmt off
62     Slice!(double[]*, 1) rgb = [
63         [1.0, 0.0, 0.0],
64         [0.0, 1.0, 0.0],
65         [0.0, 0.0, 1.0],
66         [0.470588, 0.470588, 0.470588]
67     ].sliced(4);
68     // dfmt on
69 
70     auto unpacked = pixelUnpack!3(rgb, 4, 1);
71     assert(unpacked.shape == [4, 1, 3]);
72     assert(unpacked[0][0].length == 3);
73 }
74 
75 /**
76 Clip value to the range provided.
77 */
78 T clip(double low, double high, T)(T value)
79 in
80 {
81     import std.traits : isFloatingPoint;
82 
83     static assert(isFloatingPoint!T, "Value to clip must be floating point.");
84     static assert(low >= -T.max);
85     static assert(high <= T.max);
86 }
87 do
88 {
89     if (value >= high)
90         return high;
91     else if (value <= low)
92         return low;
93     else
94         return value;
95 }
96 
97 /**
98 Grabs iterator type at compile-time.
99 */
100 template IteratorType(Iterator)
101 {
102     import std.traits : Unqual;
103 
104     alias T = Unqual!(typeof(Iterator.init[0]));
105     alias IteratorType = T;
106 }
107 
108 unittest
109 {
110     import mir.ndslice : slice;
111 
112     alias ExpectedType = long;
113 
114     void testIteratorType(Iterator)(Slice!(Iterator, 3) testSlice)
115     {
116         assert(is(IteratorType!Iterator == ExpectedType));
117     }
118 
119     auto a = slice!(immutable(ExpectedType))([1, 2, 3], 0);
120     testIteratorType(a);
121 }
122 
123 /**
124 Recursively determine dimensions of a Slice.
125 */
126 size_t[] dimensions(T)(T arr, size_t[] dims = [])
127 {
128     import std.traits : isNumeric;
129 
130     static if (isNumeric!T)
131         return dims;
132     else
133     {
134         dims ~= arr.length;
135         return dimensions(arr[0], dims);
136     }
137 }
138 
139 ///
140 unittest
141 {
142     import mir.ndslice : slice;
143 
144     size_t[3] expectedDims = [1, 2, 3];
145     auto a = slice!double(expectedDims, 0);
146     assert(a.dimensions == expectedDims);
147 }
148 
149 /**
150 Convert floating point input to unsigned.
151 */
152 Slice!(ReturnType*, dims) toUnsigned(ReturnType = ubyte, InputType, size_t dims)(
153         Slice!(InputType*, dims) input)
154 in
155 {
156     import std.traits : isFloatingPoint, isUnsigned;
157 
158     static assert(isFloatingPoint!InputType);
159     static assert(isUnsigned!ReturnType);
160 }
161 do
162 {
163     // TODO: Handle being passed an unsigned slice.
164     import mir.ndslice : each, uninitSlice, zip;
165 
166     auto output = uninitSlice!ReturnType(input.shape);
167     auto zipped = zip(input, output);
168     zipped.each!((z) { toUnsignedImpl(z); });
169 
170     return output;
171 }
172 
173 ///
174 unittest
175 {
176     import std.math : approxEqual;
177     import mir.ndslice : sliced;
178 
179     // dfmt off
180     Slice!(double*, 3) rgb = [
181         1.0, 0.0, 0.0,
182         0.0, 1.0, 0.0,
183         0.0, 0.0, 1.0,
184         0.470588, 0.470588, 0.470588
185     ].sliced(4, 1, 3);
186 
187     ubyte[] rgbU = [
188         255, 0, 0,
189         0, 255, 0,
190         0, 0, 255,
191         120, 120, 120
192     ];
193     Slice!(ubyte*, 3) rgbUnsigned = rgbU.sliced(4, 1, 3);
194     //dfmt on
195 
196     assert(approxEqual(rgb.toUnsigned, rgbUnsigned));
197 }
198 
199 private void toUnsignedImpl(T)(T zippedChnls)
200 {
201     import std.math : round;
202 
203     alias UnsignedType = typeof(zippedChnls[1].__value());
204     zippedChnls[1].__value() = cast(UnsignedType) round(zippedChnls[0].__value()
205             .clip!(0, 1) * UnsignedType.max);
206 }
207 
208 /**
209 Convert unsigned input to floating point.
210 */
211 Slice!(ReturnType*, 3) toFloating(ReturnType = double, Iterator)(Slice!(Iterator, 3) input)
212 in
213 {
214     import std.traits : isFloatingPoint, isUnsigned;
215 
216     static assert(isFloatingPoint!ReturnType);
217     static assert(isUnsigned!(IteratorType!Iterator));
218 }
219 do
220 {
221     import mir.ndslice : each, uninitSlice, zip;
222 
223     auto output = uninitSlice!ReturnType(input.shape);
224     auto zipped = zip(input, output);
225     zipped.each!((z) { toFloatingImpl(z); });
226 
227     return output;
228 }
229 
230 ///
231 unittest
232 {
233     import std.math : approxEqual;
234 
235     // dfmt off
236     ubyte[] rgb = [
237         255, 0, 0,
238         0, 255, 0,
239         0, 0, 255,
240         120, 120, 120
241     ];
242 
243     Slice!(double*, 3) rgbDouble = [
244         1.0, 0.0, 0.0,
245         0.0, 1.0, 0.0,
246         0.0, 0.0, 1.0,
247         0.470588, 0.470588, 0.470588
248     ].sliced(4, 1, 3);
249     //dfmt on
250 
251     assert(approxEqual(rgb.sliced(4, 1, 3).toFloating, rgbDouble));
252 }
253 
254 private void toFloatingImpl(T)(T zippedChnls)
255 {
256     alias UnsignedType = typeof(zippedChnls[0].__value());
257     alias FloatingType = typeof(zippedChnls[1].__value());
258     zippedChnls[1].__value() = cast(FloatingType) zippedChnls[0] / UnsignedType.max;
259 }
260 
261 /**
262 Calculate the mean pixel value for an image.
263 
264 Return semantics match mir.math.stat.mean.
265 */
266 auto imageMean(T)(Slice!(T, 3) image)
267 {
268     import mir.math.stat : mean;
269     import mir.ndslice : byDim, map, slice;
270 
271     // TODO: Add option for segmenting image for calculating mean.
272     // TODO: Add option to exclude top and bottom percentiles from mean.
273     // dfmt off
274     return image
275         .byDim!2
276         .map!(mean)
277         .slice;
278     //dfmt on
279 }
280 
281 ///
282 unittest
283 {
284     // dfmt off
285     ubyte[] rgb = [
286         255, 0, 0,
287         0, 255, 0,
288         0, 0, 255,
289         120, 120, 120
290     ];
291     //dfmt on
292 
293     assert(rgb.sliced(4, 1, 3).imageMean == [93.75, 93.75, 93.75]);
294 }
295 
296 /**
297 Map a function across an image's pixels.
298 */
299 auto pixelMap(alias fun, Iterator)(Slice!(Iterator, 3) image)
300 {
301     import mir.ndslice : fuse, map, pack;
302 
303     return image.pack!1
304         .map!(fun)
305         .fuse;
306 }