Download ecglib/correction.py from pashaalex/ecg-rectifier: direct link, hf CLI and curl.
- Browser
- Download file 3.92 kB
-
https://huggingface.co/pashaalex/ecg-rectifier/resolve/main/ecglib/correction.py
- Command line
-
hf download hf://pashaalex/ecg-rectifier/ecglib/correction.py
-
curl -L -o correction.py https://huggingface.co/pashaalex/ecg-rectifier/resolve/main/ecglib/correction.py
3.92 kB
| """Global correction of the network map by the numbered grid nodes, and the self-check. | |
| The network map F places node (i, j) at F(node); a cubic polynomial T is fitted so that | |
| T(F(node)) lies on the 5 mm lattice, and the corrected map is T(F). A second candidate is a | |
| homography fitted to the nodes directly. | |
| Self-check: nodes are split like a chessboard by the parity of i + j; the correction is fitted on | |
| the even nodes and measured on the odd ones after the best similarity to the lattice. It measures | |
| the SHAPE of the lattice (the global scale is fitted out) and is blind to detector errors. | |
| """ | |
| import cv2 | |
| import numpy as np | |
| OUTLIER_MM = 1.0 | |
| def design(z): | |
| """Cubic polynomial columns of (x, y) = (Re z, Im z).""" | |
| x, y = z.real, z.imag | |
| return np.stack([np.ones_like(x), x, y, x * x, x * y, y * y, x ** 3, x * x * y, x * y * y, y ** 3], 1) | |
| def fit_correction(mapped, ij): | |
| """Fit T: mapped node positions (complex mm) -> 5 * (i + 1j * j), refit without > OUTLIER_MM outliers. | |
| The lattice may come mirrored (j up), so both orientations are tried; reflections are rejected. | |
| -> function applying T to map values.""" | |
| target = 5.0 * (ij[:, 0] + 1j * ij[:, 1]) | |
| centre = mapped.mean() | |
| z = mapped - centre | |
| scale = np.abs(z).max() | |
| keep = np.ones(len(z), bool) | |
| for _ in range(2): | |
| best = None | |
| for q in (target, np.conj(target)): | |
| m = design(z[keep] / scale) | |
| cu, *_ = np.linalg.lstsq(m, q[keep].real, rcond=None) | |
| cv, *_ = np.linalg.lstsq(m, q[keep].imag, rcond=None) | |
| if cu[1] * cv[2] - cu[2] * cv[1] <= 0: | |
| continue | |
| residual = np.abs(design(z / scale) @ cu + 1j * (design(z / scale) @ cv) - q) | |
| if best is None or np.median(residual) < np.median(best[2]): | |
| best = (cu, cv, residual) | |
| cu, cv, residual = best | |
| keep = residual < OUTLIER_MM | |
| def apply(values): | |
| m = design((values - centre) / scale) | |
| return m @ cu + 1j * (m @ cv) | |
| return apply | |
| def _fit_similarity(p, q): | |
| """Residuals |p - a*q - b| of the best similarity (complex a, b).""" | |
| pc, qc = p - p.mean(), q - q.mean() | |
| a = (np.conj(qc) * pc).sum() / (np.abs(qc) ** 2).sum() | |
| return np.abs(pc - a * qc) | |
| def score(p, ij): | |
| """Residuals (mm) of points p against the 5 mm lattice ij after the best similarity (mirror allowed).""" | |
| q = 5.0 * (ij[:, 0] + 1j * ij[:, 1]) | |
| r1, r2 = _fit_similarity(p, q), _fit_similarity(p, np.conj(q)) | |
| r = r1 if np.sqrt((r1 ** 2).mean()) <= np.sqrt((r2 ** 2).mean()) else r2 | |
| trimmed = np.sort(r)[:int(0.98 * len(r))] | |
| return {"rms_mm": float(np.sqrt((r ** 2).mean())), "rms98_mm": float(np.sqrt((trimmed ** 2).mean())), | |
| "median_mm": float(np.median(r)), "p95_mm": float(np.percentile(r, 95)), "points": int(len(r))} | |
| def halves(ij): | |
| """Chessboard split: even i + j to fit, odd to check.""" | |
| return (ij.sum(1) % 2) == 0, (ij.sum(1) % 2) == 1 | |
| def holdout_network(mesh, nodes, pts, ij): | |
| fit, test = halves(ij) | |
| apply = fit_correction(mesh.sample(nodes, pts[fit]), ij[fit]) | |
| return score(apply(mesh.sample(nodes, pts[test])), ij[test]) | |
| def homography_nodes(mesh, pts, ij): | |
| """Homography map from the nodes, evaluated at the mesh nodes; None if it fails.""" | |
| h, _ = cv2.findHomography(pts.astype(np.float64), 5.0 * ij.astype(np.float64), cv2.RANSAC, 5.0) | |
| if h is None: | |
| return None | |
| gx, gy = np.meshgrid(np.arange(mesh.nx) * mesh.step, np.arange(mesh.ny) * mesh.step) | |
| q = cv2.perspectiveTransform(np.stack([gx.ravel(), gy.ravel()], 1).reshape(-1, 1, 2).astype(np.float64), h) | |
| return q[:, 0, 0] + 1j * q[:, 0, 1] | |
| def holdout_homography(mesh, pts, ij): | |
| fit, test = halves(ij) | |
| h_nodes = homography_nodes(mesh, pts[fit], ij[fit]) | |
| if h_nodes is None: | |
| return None | |
| return score(mesh.sample(h_nodes, pts[test]), ij[test]) | |