poseaudit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- poseaudit/__init__.py +33 -0
- poseaudit/_version.py +6 -0
- poseaudit/agreement.py +139 -0
- poseaudit/audit.py +843 -0
- poseaudit/cli.py +520 -0
- poseaudit/confidence.py +45 -0
- poseaudit/io/__init__.py +5 -0
- poseaudit/io/coco.py +145 -0
- poseaudit/io/supervision.py +220 -0
- poseaudit/io/yolo.py +166 -0
- poseaudit/measures.py +132 -0
- poseaudit/pairing.py +195 -0
- poseaudit/plot.py +59 -0
- poseaudit/py.typed +0 -0
- poseaudit/reference.py +195 -0
- poseaudit/report.py +374 -0
- poseaudit/thresholds.py +83 -0
- poseaudit/types.py +57 -0
- poseaudit-0.1.0.dist-info/METADATA +487 -0
- poseaudit-0.1.0.dist-info/RECORD +23 -0
- poseaudit-0.1.0.dist-info/WHEEL +4 -0
- poseaudit-0.1.0.dist-info/entry_points.txt +2 -0
- poseaudit-0.1.0.dist-info/licenses/LICENSE +21 -0
poseaudit/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Audit a pose model used as a measuring instrument."""
|
|
2
|
+
|
|
3
|
+
from poseaudit._version import __version__
|
|
4
|
+
from poseaudit.audit import AuditResult, Band, NotRead, Reading, audit
|
|
5
|
+
from poseaudit.io import from_supervision, load_coco, load_coco_results, load_yolo
|
|
6
|
+
from poseaudit.measures import Measure, angle, length, ratio, tilt
|
|
7
|
+
from poseaudit.pairing import Pair, Pairing, pair
|
|
8
|
+
from poseaudit.thresholds import ThresholdAgreement
|
|
9
|
+
from poseaudit.types import Dataset, Instance
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AuditResult",
|
|
13
|
+
"Band",
|
|
14
|
+
"Dataset",
|
|
15
|
+
"Instance",
|
|
16
|
+
"Measure",
|
|
17
|
+
"NotRead",
|
|
18
|
+
"Pair",
|
|
19
|
+
"Pairing",
|
|
20
|
+
"Reading",
|
|
21
|
+
"ThresholdAgreement",
|
|
22
|
+
"__version__",
|
|
23
|
+
"angle",
|
|
24
|
+
"audit",
|
|
25
|
+
"from_supervision",
|
|
26
|
+
"length",
|
|
27
|
+
"load_coco",
|
|
28
|
+
"load_coco_results",
|
|
29
|
+
"load_yolo",
|
|
30
|
+
"pair",
|
|
31
|
+
"ratio",
|
|
32
|
+
"tilt",
|
|
33
|
+
]
|
poseaudit/_version.py
ADDED
poseaudit/agreement.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Agreement statistics between two readings of the same quantity.
|
|
2
|
+
|
|
3
|
+
`t` is the reference (ground truth), `p` the prediction, `e = p - t`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from poseaudit.confidence import Z95
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def gain(t: np.ndarray, p: np.ndarray) -> tuple[float, float]:
|
|
12
|
+
"""(slope, intercept) of predicted on truth by least squares.
|
|
13
|
+
|
|
14
|
+
A slope of 0.6 means the model reads a 10 degree change as 6. Unbiased
|
|
15
|
+
when the truth is much less noisy than the prediction, the usual case for
|
|
16
|
+
human labels or motion capture against a model.
|
|
17
|
+
"""
|
|
18
|
+
if len(t) < 3 or _flat(t):
|
|
19
|
+
return float("nan"), float("nan")
|
|
20
|
+
dt = t - t.mean()
|
|
21
|
+
slope = float(dt @ (p - p.mean()) / (dt @ dt))
|
|
22
|
+
return slope, float(p.mean() - slope * t.mean())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def robust_gain(
|
|
26
|
+
t: np.ndarray, p: np.ndarray, pairs: int = 500_000, seed: int = 0
|
|
27
|
+
) -> float:
|
|
28
|
+
"""Theil-Sen slope of predicted on truth: the median of the slopes between
|
|
29
|
+
pairs of readings, all of them up to about 1000 readings and a random
|
|
30
|
+
`pairs` of them above. A few gross failures move a least-squares gain a lot
|
|
31
|
+
and this one little; so does noise that grows with the value, so a gap
|
|
32
|
+
between the two is not by itself proof of gross failures."""
|
|
33
|
+
n = len(t)
|
|
34
|
+
if n < 3 or _flat(t):
|
|
35
|
+
return float("nan")
|
|
36
|
+
rng = np.random.default_rng(seed)
|
|
37
|
+
if n * (n - 1) // 2 <= pairs:
|
|
38
|
+
i, j = np.triu_indices(n, k=1)
|
|
39
|
+
else:
|
|
40
|
+
i, j = rng.integers(0, n, pairs), rng.integers(0, n, pairs)
|
|
41
|
+
dt = t[j] - t[i]
|
|
42
|
+
keep = dt != 0
|
|
43
|
+
return float(np.median((p[j] - p[i])[keep] / dt[keep]))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ba_slope(t: np.ndarray, p: np.ndarray) -> float:
|
|
47
|
+
"""Slope of the error on the mean of both readings (Bland-Altman).
|
|
48
|
+
|
|
49
|
+
Equal to (var p - var t) / (2 var mean): a test of equal spread, which reads
|
|
50
|
+
as proportional bias only when both readings are about equally noisy. A
|
|
51
|
+
noisier prediction pushes it upward and can hide real compression.
|
|
52
|
+
"""
|
|
53
|
+
m = (t + p) / 2
|
|
54
|
+
if len(t) < 3 or np.ptp(m) == 0:
|
|
55
|
+
return float("nan")
|
|
56
|
+
return float(np.polyfit(m, p - t, 1)[0])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def deming(t: np.ndarray, p: np.ndarray, noise_ratio: float) -> float:
|
|
60
|
+
"""Slope of predicted on truth when both carry noise, `noise_ratio` being
|
|
61
|
+
var(prediction noise) / var(truth noise). Infinity gives `gain`."""
|
|
62
|
+
if len(t) < 3:
|
|
63
|
+
return float("nan")
|
|
64
|
+
sxx, syy = np.var(t, ddof=1), np.var(p, ddof=1)
|
|
65
|
+
sxy = np.cov(t, p, ddof=1)[0, 1]
|
|
66
|
+
if sxy == 0:
|
|
67
|
+
return float("nan")
|
|
68
|
+
if np.isinf(noise_ratio):
|
|
69
|
+
return float(sxy / sxx)
|
|
70
|
+
a = syy - noise_ratio * sxx
|
|
71
|
+
return float((a + np.sqrt(a * a + 4 * noise_ratio * sxy * sxy)) / (2 * sxy))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def icc_a1(t: np.ndarray, p: np.ndarray) -> float:
|
|
75
|
+
"""ICC(A,1): two-way, absolute agreement, single measurement
|
|
76
|
+
(McGraw and Wong 1996)."""
|
|
77
|
+
y = np.column_stack([t, p])
|
|
78
|
+
n, k = y.shape
|
|
79
|
+
if n < 2:
|
|
80
|
+
return float("nan")
|
|
81
|
+
grand = y.mean()
|
|
82
|
+
ss_rows = k * ((y.mean(axis=1) - grand) ** 2).sum()
|
|
83
|
+
ss_cols = n * ((y.mean(axis=0) - grand) ** 2).sum()
|
|
84
|
+
ss_error = ((y - grand) ** 2).sum() - ss_rows - ss_cols
|
|
85
|
+
ms_r = ss_rows / (n - 1)
|
|
86
|
+
ms_c = ss_cols / (k - 1)
|
|
87
|
+
ms_e = ss_error / ((n - 1) * (k - 1))
|
|
88
|
+
denominator = ms_r + (k - 1) * ms_e + k * (ms_c - ms_e) / n
|
|
89
|
+
return float((ms_r - ms_e) / denominator) if denominator else float("nan")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def ccc(t: np.ndarray, p: np.ndarray) -> float:
|
|
93
|
+
"""Lin's concordance correlation coefficient."""
|
|
94
|
+
if len(t) < 2:
|
|
95
|
+
return float("nan")
|
|
96
|
+
cov = np.mean((t - t.mean()) * (p - p.mean()))
|
|
97
|
+
denominator = t.var() + p.var() + (t.mean() - p.mean()) ** 2
|
|
98
|
+
return float(2 * cov / denominator) if denominator else float("nan")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def limits(e: np.ndarray) -> tuple[float, float]:
|
|
102
|
+
"""Bias +/- 1.96 SD: 95% of errors, if they are roughly normal."""
|
|
103
|
+
if len(e) < 2:
|
|
104
|
+
return float("nan"), float("nan")
|
|
105
|
+
half = Z95 * e.std(ddof=1)
|
|
106
|
+
return float(e.mean() - half), float(e.mean() + half)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def empirical_limits(e: np.ndarray) -> tuple[float, float]:
|
|
110
|
+
"""The 2.5th and 97.5th percentiles: no assumption about the shape, which
|
|
111
|
+
matters when a few gross failures make the tails heavy."""
|
|
112
|
+
if len(e) < 2:
|
|
113
|
+
return float("nan"), float("nan")
|
|
114
|
+
low, high = np.percentile(e, [2.5, 97.5])
|
|
115
|
+
return float(low), float(high)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def repeated_limits(e: np.ndarray, clusters: np.ndarray) -> tuple[float, float]:
|
|
119
|
+
"""Limits for several readings per subject, the true value varying within
|
|
120
|
+
a subject (Bland and Altman 2007): between- and within-subject variance
|
|
121
|
+
are added rather than every reading counted as independent."""
|
|
122
|
+
labels, inverse = np.unique(clusters, return_inverse=True)
|
|
123
|
+
counts = np.bincount(inverse)
|
|
124
|
+
n, groups = len(e), len(labels)
|
|
125
|
+
if groups < 2 or groups == n:
|
|
126
|
+
return limits(e)
|
|
127
|
+
means = np.bincount(inverse, weights=e) / counts
|
|
128
|
+
grand = e.mean()
|
|
129
|
+
ms_between = (counts * (means - grand) ** 2).sum() / (groups - 1)
|
|
130
|
+
ms_within = ((e - means[inverse]) ** 2).sum() / (n - groups)
|
|
131
|
+
divisor = (n * n - (counts**2).sum()) / ((groups - 1) * n)
|
|
132
|
+
between = max(0.0, (ms_between - ms_within) / divisor)
|
|
133
|
+
half = Z95 * np.sqrt(between + ms_within)
|
|
134
|
+
return float(grand - half), float(grand + half)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _flat(t: np.ndarray) -> bool:
|
|
138
|
+
"""No spread to fit a slope on: a range that is only rounding noise."""
|
|
139
|
+
return bool(np.ptp(t) <= 1e-9 * max(1.0, float(np.abs(t).max())))
|