1 /** 2 Module introduces Randomized Hough Transform implementation. 3 4 Example: 5 ---- 6 7 // Load an image from filesystem. 8 Image image = imread("/path/to/image.png"); 9 10 // Calculate edges by use of canny algorithm 11 auto edges = image 12 .sliced 13 .rgb2gray 14 .as!float 15 .slice 16 .conv(gaussian!float(1.0f, 3, 3)) 17 .canny!ubyte(100); 18 19 // Setup RHT line extraction context 20 auto lines = RhtLines().epouchs(50).iterations(250).minCurve(25); 21 22 // Lazily iterate and detect lines in pre-processed image 23 foreach(line; lines(edges)) { 24 // do stuff with lines.. 25 } 26 ---- 27 28 For more elaborated module description visit the $(LINK2 http://dcv.dlang.io/?loc=example_rht,RHT example). 29 30 Copyright: Copyright © 2016, Dmitry Olshansky 31 32 Authors: Dmitry Olshansky 33 34 License: $(LINK3 http://www.boost.org/LICENSE_1_0.txt, Boost Software License - Version 1.0). 35 */ 36 37 module dcv.features.rht; 38 39 import std.typecons, std.range.primitives; 40 41 import mir.ndslice; 42 43 /++ 44 A template that bootstraps a full Randomized Hough transform implementation. 45 The basic primitives required are as follows. 46 47 Types: Curve and Key tuples that define curve parameters and accumulator key for the curve. 48 Functions: curveKey - accumulator key for a curve, onCurve - test if a point is on curve, 49 fitCurve - fit a curve to a given random access range of points. 50 +/ 51 mixin template BaseRht() 52 { 53 alias This = typeof(this); 54 alias Point = Tuple!(int, "x", int, "y"); 55 int _thrd = 2; // threshold for a maximum to be detected in accumulator 56 int _epouchs = 50; // number of epouchs to iterate (attempts to find a shape) 57 int _iters = 1000; // iterations in each epouch 58 int _minCurve = 25; // minimal curve length in pixels 59 60 static auto toInt(double a) 61 { 62 import std.math; 63 64 return cast(int)lrint(a); 65 } 66 67 // invalid (or empty) curve if any of parameters is NaN 68 static bool isInvalidCurve(Curve c) 69 { 70 import std.math; 71 72 foreach (v; c.tupleof) 73 if (v.isNaN) 74 return true; 75 return false; 76 } 77 78 /// Set threshold for a curve to be considered in an accumulator 79 ref threshold(int threshold) 80 { 81 _thrd = threshold; 82 return this; 83 } 84 85 ref epouchs(int epouchs) 86 { 87 _epouchs = epouchs; 88 return this; 89 } 90 91 ref iterations(int iters) 92 { 93 _iters = iters; 94 return this; 95 } 96 97 ref minCurve(int minimalCurve) 98 { 99 _minCurve = minimalCurve; 100 return this; 101 } 102 103 static auto opCall() 104 { 105 This zis; 106 return zis; 107 } 108 109 /// Run RHT using non-zero points in image as edge points. 110 auto opCall(T, SliceKind kind)(Slice!(kind, [2], T*) image) 111 { 112 import std.array : appender; 113 114 auto points = appender!(Point[])(); 115 int x, y = 0; 116 117 foreach(row; image) 118 { 119 x = 0; 120 foreach(e; row) 121 { 122 if (e > 0) 123 points.put(Point(x, y)); 124 ++x; 125 } 126 ++y; 127 } 128 return this.opCall(image, points.data); 129 } 130 131 /// Run RHT using prepopullated array of edge points (that may be filtered beforehand). 132 auto opCall(T, SliceKind kind, Range)(Slice!(kind, [2], T*) image, Range points) if (isInputRange!Range) 133 { 134 auto r = RhtRange!(T, kind, ElementType!Range)(this, image, points); 135 r.popFront(); // prime the detection process 136 return r; 137 } 138 139 static struct RhtRange(T, SliceKind kind, P) 140 { 141 private: 142 import std.container, std.random; 143 144 This _rht; // RHT struct with key primitives and parameters 145 Slice!(kind, [2], T*) _image; // image with edge points 146 Tuple!(Curve, int)[Key] _accum; // accumulator, parametrized on Key/Curve tuples 147 Array!P _points; // extracted edge points 148 int _epouch; // current epouch of iteration 149 Curve _current; // current detected curve 150 Xorshift rng; 151 this(Range)(This rht, Slice!(kind, [2], T*) image, Range points) 152 { 153 _rht = rht; 154 _image = image; 155 _points = make!(Array!P)(points); 156 rng = Xorshift(unpredictableSeed); 157 } 158 159 void accumulate(Curve curve) 160 { 161 auto key = _rht.curveKey(curve); 162 // if have a curve with the same key 163 // replace it with weighted average 164 if (auto p = (key in _accum)) 165 { 166 auto prior = (*p)[0]; 167 auto w = (*p)[1]; 168 Curve weighted; 169 foreach (i, _; prior.tupleof) 170 { 171 weighted[i] = (prior[i] * w + curve[i]) / (w + 1.0); 172 } 173 auto newKey = _rht.curveKey(weighted); 174 if (key != newKey) 175 { 176 _accum.remove(key); 177 _accum[newKey] = tuple(weighted, w + 1); 178 } 179 else 180 *p = tuple(weighted, w + 1); 181 } 182 // simply add current curve to accum 183 else 184 { 185 _accum[key] = tuple(curve, 1); 186 } 187 } 188 189 Curve bestCurve() 190 { 191 Curve best; 192 int maxW = 0; 193 foreach (_, v; _accum) 194 { 195 if (v[1] > maxW) 196 { 197 maxW = v[1]; 198 best = v[0]; 199 } 200 } 201 return best; 202 } 203 204 public: 205 Curve front() 206 { 207 return _current; 208 } 209 210 bool empty() 211 { 212 return isInvalidCurve(_current); 213 } 214 215 void popFront() 216 { 217 import std.algorithm, std.range; 218 219 foreach (e; _epouch .. _rht._epouchs) 220 { 221 _accum.clear(); 222 Curve best; 223 foreach (i; 0 .. _rht._iters) 224 { 225 if (_points.length < _rht.sampleSize) 226 break; 227 // TODO: avoid heap allocation 228 auto sample = randomSample(_points[], _rht.sampleSize, &rng).array; 229 auto curve = _rht.fitCurve(_image, sample); 230 if (!isInvalidCurve(curve)) 231 accumulate(curve); 232 } 233 best = bestCurve(); 234 if (isInvalidCurve(best)) 235 continue; 236 auto newPoints = make!Array(_points[].filter!(x => !_rht.onCurve(best, x))); 237 if (_points.length - newPoints.length > _rht._minCurve) 238 { 239 // remove fitted curve from the set of points 240 copy(newPoints[], _points[0 .. newPoints.length]); 241 _points.length = newPoints.length; 242 _current = best; 243 _epouch = e + 1; // skip current epouch 244 return; // stop prematurely 245 } 246 } 247 // spinned through all of epouchs - no more curves to detect 248 _current = Curve.init; 249 } 250 251 /// Resulting points that are not fitted to any shape yet 252 @property auto points() 253 { 254 return _points; 255 } 256 } 257 } 258 259 /// Randomized Hough Transform for lines 260 struct RhtLines 261 { 262 alias Key = Tuple!(double, double); 263 alias Curve = Tuple!(double, "m", double, "b"); 264 enum sampleSize = 2; // sample points to use in each iteration 265 double _angleTol = 1.0; // tolerance in angle approximation (deg) 266 double _interceptTol = 1.0; // tolerance in intercept approximation (pixels) 267 double _curveTol = 1.5; // tolerance of on-curve check (pixels) 268 mixin BaseRht; 269 270 // does coarsening to the multiple of tolerance 271 auto curveKey(Curve c) 272 { 273 import std.math; 274 275 auto angle = atan(c.m) / PI * 180; 276 return Key(rint(angle / _angleTol), rint(c.b / _interceptTol)); 277 } 278 279 auto fitCurve(Range, SliceKind kind, Sample)(Slice!(kind, [2], Range) image, Sample sample) 280 { 281 int x1 = sample[0].x, y1 = sample[0].y; 282 int x2 = sample[1].x, y2 = sample[1].y; 283 if (x1 == x2) 284 return Curve(double.infinity, x1); 285 else 286 { 287 auto m = cast(double)(y1 - y2) / (x1 - x2); 288 auto b = y1 - m * x1; 289 return Curve(m, b); 290 } 291 } 292 293 bool onCurve(Curve curve, Point p) 294 { 295 import std.math; 296 297 int x = p.x, y = p.y; 298 double m = curve.m, b = curve.b; 299 if (m == double.infinity && fabs(x - b) < _curveTol) 300 return true; 301 else if (fabs(x * m + b - y) < _curveTol) 302 return true; 303 return false; 304 } 305 } 306 307 struct RhtCircles 308 { 309 alias Key = Tuple!(int, int, int); 310 alias Curve = Tuple!(double, "x", double, "y", double, "r"); 311 enum sampleSize = 3; 312 double _centerTol = 5.0; // tolerance of center location (pixels) 313 double _radiusTol = 5.0; // tolerance of radius approfixmation (pixels) 314 double _curveTol = 8; // tolerance of on-curve check (proportional to radius) 315 mixin BaseRht; 316 317 // does coarsening to the multiple of tolerance 318 auto curveKey(Curve c) 319 { 320 return Key(toInt(c.x / _centerTol), toInt(c.y / _centerTol), toInt(c.r / _radiusTol)); 321 } 322 323 auto fitCurve(Range, SliceKind kind, Sample)(Slice!(kind, [2], Range) image, Sample sample) 324 { 325 import std.math : sqrt, pow; 326 327 double x1 = sample[0].x, y1 = sample[0].y; 328 double x2 = sample[1].x, y2 = sample[1].y; 329 double x3 = sample[2].x, y3 = sample[2].y; 330 double ynorm = (y2 * y2 - y3 * y3) * (x2 - x1) / 2 + (x3 - x2) * (y2 * y2 - y1 * y1) / 2 + ( 331 x1 - x3) * (y2 - y3) * (x2 - x1); 332 double y = ynorm / ((y2 - y3) * (x2 - x1) + (x3 - x2) * (y2 - y1)); 333 double x = (y1 - y2) * (2 * y - y1 - y2) / (x2 - x1) / 2 + (x1 + x2) / 2; 334 double r = sqrt(pow(x - x1, 2) + pow(y - y1, 2)); 335 return Curve(x, y, r); 336 } 337 338 bool onCurve(Curve curve, Point p) 339 { 340 import std.math : fabs; 341 342 double x = p.x, y = p.y; 343 double x0 = curve.x, y0 = curve.y, R = curve.r; 344 x -= x0; 345 y -= y0; 346 if (fabs(x * x + y * y - R * R) < _curveTol * _curveTol) 347 return true; 348 return false; 349 } 350 } 351 352 struct RhtEllipses 353 { 354 alias Key = Tuple!(int, int, int, int, int); 355 alias Curve = Tuple!(double, "x", double, "y", double, "a", double, "b", double, "phi"); 356 enum sampleSize = 3; 357 double _centerTol = 2.0; // tolerance of center location (pixels) 358 double _axisTol = 2.0; // tolerance of axis approximation (pixels) 359 double _phiTol = 5.0; // tolerance of angle approximation (degrees) 360 double _curveTol = 0.05; // tolerance of on-curve check (proportional to axis) 361 mixin BaseRht; 362 363 // does coarsening to the multiple of tolerance 364 auto curveKey(Curve c) 365 { 366 import std.math; 367 368 return Key(toInt(c.x / _centerTol), toInt(c.y / _centerTol), toInt(c.a / _axisTol), 369 toInt(c.b / _axisTol), toInt(c.phi / _phiTol)); 370 } 371 372 auto fitCurve(Range, SliceKind kind, Sample)(Slice!(kind, [2], Range) image, Sample sample) 373 { 374 Curve c; 375 //TODO: 376 return c; 377 } 378 379 bool onCurve(Curve curve, Point p) 380 { 381 import std.math; 382 383 double x = p.x, y = p.y; 384 double x0 = curve.x, y0 = curve.y, a = curve.a, b = curve.b; 385 x -= x0; 386 y -= y0; 387 double th = curve.phi * PI / 180; 388 double nx = x * cos(th) + y * sin(th); 389 double ny = -x * sin(th) + y * cos(th); 390 if (fabs(nx * nx / (a * a) + ny * ny / (b * b) - 1.0) < _curveTol) 391 return true; 392 return false; 393 } 394 }