| 152 | |
| 153 | |
| 154 | class EllipseFitter: |
| 155 | def __init__(self, sd=2): |
| 156 | self.sd = sd |
| 157 | self.x = None |
| 158 | self.y = None |
| 159 | self.params = None |
| 160 | self._coeffs = None |
| 161 | |
| 162 | def fit(self, xy): |
| 163 | self.x, self.y = xy[np.isfinite(xy).all(axis=1)].T |
| 164 | if len(self.x) < 3: |
| 165 | return None |
| 166 | if self.sd: |
| 167 | self.params = self._fit_error(self.x, self.y, self.sd) |
| 168 | else: |
| 169 | self._coeffs = self._fit(self.x, self.y) |
| 170 | self.params = self.calc_parameters(self._coeffs) |
| 171 | if not np.isnan(self.params).any(): |
| 172 | return Ellipse(*self.params) |
| 173 | return None |
| 174 | |
| 175 | @staticmethod |
| 176 | @jit(nopython=True) |
| 177 | def _fit(x, y): |
| 178 | """Least Squares ellipse fitting algorithm Fit an ellipse to a set of X- and |
| 179 | Y-coordinates. See Halir and Flusser, 1998 for implementation details. |
| 180 | |
| 181 | :param x: ndarray, 1D trajectory |
| 182 | :param y: ndarray, 1D trajectory |
| 183 | :return: 1D ndarray of 6 coefficients of the general quadratic curve: ax^2 + |
| 184 | 2bxy + cy^2 + 2dx + 2fy + g = 0 |
| 185 | """ |
| 186 | D1 = np.vstack((x * x, x * y, y * y)) |
| 187 | D2 = np.vstack((x, y, np.ones_like(x))) |
| 188 | S1 = D1 @ D1.T |
| 189 | S2 = D1 @ D2.T |
| 190 | S3 = D2 @ D2.T |
| 191 | T = -np.linalg.inv(S3) @ S2.T |
| 192 | temp = S1 + S2 @ T |
| 193 | M = np.zeros_like(temp) |
| 194 | M[0] = temp[2] * 0.5 |
| 195 | M[1] = -temp[1] |
| 196 | M[2] = temp[0] * 0.5 |
| 197 | E, V = np.linalg.eig(M) |
| 198 | cond = 4 * V[0] * V[2] - V[1] ** 2 |
| 199 | a1 = V[:, cond > 0][:, 0] |
| 200 | a2 = T @ a1 |
| 201 | return np.hstack((a1, a2)) |
| 202 | |
| 203 | @staticmethod |
| 204 | @jit(nopython=True) |
| 205 | def _fit_error(x, y, sd): |
| 206 | """Fit a sd-sigma covariance error ellipse to the data. |
| 207 | |
| 208 | :param x: ndarray, 1D input of X coordinates |
| 209 | :param y: ndarray, 1D input of Y coordinates |
| 210 | :param sd: int, size of the error ellipse in 'standard deviation' |
| 211 | :return: ellipse center, semi-axes length, angle to the X-axis |
no outgoing calls
no test coverage detected