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 }