-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathransac.py
135 lines (104 loc) · 3.74 KB
/
ransac.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
from __future__ import division
import numpy as np
from random import shuffle
class Model(object):
def fit(self, data):
raise NotImplementedError
def distance(self, samples):
raise NotImplementedError
class LineModel(Model):
"""
A 2D line model.
"""
def fit(self, data):
"""
Fits the model to the data, minimizing the sum of absolute
errors.
The fitting is done in the simplest manner possible; drawing a
line through two of the samples instead of the more common
least squares method.
"""
X = data[:,0]
Y = data[:,1]
denom = (X[-1] - X[0])
if denom == 0:
raise ZeroDivisionError
k = (Y[-1] - Y[0]) / denom
m = Y[0] - k * X[0]
self.params = [k, m]
self.residual = sum(abs(k * X + m - Y))
def distance(self, samples):
"""
Calculates the vertical distances from the samples to the model.
"""
X = samples[:,0]
Y = samples[:,1]
k = self.params[0]
m = self.params[1]
dists = abs(k * X + m - Y)
return dists
def ransac(data, model, min_samples, min_inliers, iterations=100, eps=1e-10):
"""
Fits a model to observed data.
Uses the RANSAC iterative method of fitting a model to observed
data. The method is robust as it ignores outlier data.
Parameters
----------
data : numpy.ndarray
The data that the model should be fitted to.
model : Model
The model that is used to describe the data.
min_samples : int
The minimum number of samples needed to fit the model.
min_inliers : number
The number of inliers required to assert that the model
is a good fit. If 0 < min_inliers < 1 then min_inliers
is considered a percentage of the number of samples in
the data.
iterations : int
The number of iterations that the algorithm should run.
eps : float
The maximum allowed distance from the model that a sample is
allowed to be to be counted as an inlier.
Returns
-------
best_params : list
The parameters of the model with the best fit.
best_inliers : numpy.ndarray
A list of the inliers belonging to the model with the best fit.
best_residual : float
The residual of the inliers.
Raises
------
ValueError
If the algorithm could not find a good fit for the data.
"""
if len(data) <= min_samples:
raise ValueError("Not enough input data to fit the model.")
if 0 < min_inliers < 1:
min_inliers = int(min_inliers * len(data))
best_params = None
best_inliers = None
best_residual = np.inf
for i in xrange(iterations):
indices = range(len(data))
shuffle(indices)
inliers = np.asarray([data[i] for i in indices[:min_samples]])
shuffled_data = np.asarray([data[i] for i in indices[min_samples:]])
try:
model.fit(inliers)
dists = model.distance(shuffled_data)
more_inliers = shuffled_data[np.where(dists <= eps)[0]]
inliers = np.concatenate((inliers, more_inliers))
if len(inliers) >= min_inliers:
model.fit(inliers)
if model.residual < best_residual:
best_params = model.params
best_inliers = inliers
best_residual = model.residual
except ZeroDivisionError:
pass
if not best_params:
raise ValueError("RANSAC failed to find a sufficiently good fit for "
"the data. Check that input data has sufficient rank.")
return (best_params, best_inliers, best_residual)