46 std::span<const glm::vec2> path,
size_t count)
50 return std::vector<glm::vec2>(
count, path.empty() ? glm::vec2 { 0.0F } : path[0]);
52 std::vector<float> arc;
53 arc.reserve(path.size());
56 for (
size_t i = 1; i < path.size(); ++i) {
57 total += glm::length(path[i] - path[i - 1]);
62 return std::vector<glm::vec2>(
count, path[0]);
64 std::vector<glm::vec2>
out;
66 const float step =
total /
static_cast<float>(
count - 1);
69 for (
size_t i = 0; i <
count; ++i) {
70 const float target =
static_cast<float>(i) * step;
71 while (upper < arc.size() - 1 && arc[upper] < target)
73 const size_t lower = upper - 1;
74 const float segment = arc[upper] - arc[lower];
75 const float t = (segment > 1e-9F) ? ((target - arc[lower]) / segment) : 0.0F;
76 out.push_back(glm::mix(path[lower], path[upper], t));
98[[nodiscard]]
inline float signed_turning(std::span<const glm::vec2> path)
noexcept
104 float prev =
heading(path[1] - path[0]);
105 for (
size_t i = 2; i < path.size(); ++i) {
106 const float h =
heading(path[i] - path[i - 1]);
122[[nodiscard]]
inline float winding_number(std::span<const glm::vec2> path)
noexcept
139[[nodiscard]]
inline float signed_area(std::span<const glm::vec2> path)
noexcept
145 for (
size_t i = 0; i < path.size(); ++i) {
146 const glm::vec2&
a = path[i];
147 const glm::vec2&
b = path[(i + 1) % path.size()];
200 std::span<const glm::vec2> path,
size_t samples,
bool rotation_invariant =
true)
202 samples = samples < 3 ? 3 : samples;
205 profile.
turning.assign(samples - 1, 0.0F);
210 for (
size_t i = 1; i < path.size(); ++i)
211 profile.
length += glm::length(path[i] - path[i - 1]);
215 float unwrapped =
heading(even[1] - even[0]);
217 profile.
turning[0] = unwrapped;
219 float prev = unwrapped;
220 for (
size_t i = 2; i < even.size(); ++i) {
221 const float h =
heading(even[i] - even[i - 1]);
224 profile.
turning[i - 1] = unwrapped;
227 if (rotation_invariant) {
228 for (
auto& t : profile.
turning)
252 if (
a.turning.size() !=
b.turning.size() ||
a.turning.empty())
253 return std::numeric_limits<float>::infinity();
256 for (
size_t i = 0; i <
a.turning.size(); ++i) {
257 const float d =
a.turning[i] -
b.turning[i];
260 return std::sqrt(acc /
static_cast<float>(
a.turning.size()));
278 std::span<const glm::vec2> path)
280 std::vector<glm::vec2>
out(path.begin(), path.end());
284 glm::vec2
mean { 0.0F };
285 for (
const auto& p :
out)
287 mean /=
static_cast<float>(
out.size());
290 for (
auto& p :
out) {
292 acc += glm::dot(p, p);
295 const float rms = std::sqrt(acc /
static_cast<float>(
out.size()));
318 std::span<const glm::vec2>
a, std::span<const glm::vec2>
b,
size_t samples = 32)
322 if (ra.size() != rb.size() || ra.empty())
323 return std::numeric_limits<float>::infinity();
326 for (
size_t i = 0; i < ra.size(); ++i) {
327 const glm::vec2 d = ra[i] - rb[i];
328 acc += glm::dot(d, d);
330 return std::sqrt(acc /
static_cast<float>(ra.size()));
362 std::span<const T>
a,
363 std::span<const T>
b,
364 const std::function<
float(
const T&,
const T&)>& distance,
367 if (
a.empty() ||
b.empty())
368 return std::numeric_limits<float>::infinity();
370 const size_t n =
a.size();
371 const size_t m =
b.size();
372 constexpr float inf = std::numeric_limits<float>::infinity();
374 std::vector<float> prev(m + 1, inf);
375 std::vector<float> curr(m + 1, inf);
378 for (
size_t i = 1; i <= n; ++i) {
379 std::ranges::fill(curr, inf);
384 const auto centre =
static_cast<size_t>(
385 (
static_cast<double>(i) *
static_cast<double>(m)) /
static_cast<double>(n));
386 lo = (centre > band) ? (centre - band) : 1;
387 hi = std::min(m, centre + band);
392 for (
size_t j =
lo; j <=
hi; ++j) {
393 const float cost =
distance(
a[i - 1],
b[j - 1]);
394 const float best = std::min({ prev[j], curr[j - 1], prev[j - 1] });
395 curr[j] = (best == inf) ? inf : (cost + best);
397 std::swap(prev, curr);
std::vector< float > * out
float shape_distance(std::span< const glm::vec2 > a, std::span< const glm::vec2 > b, size_t samples=32)
Root mean square point distance between two paths after resampling and normalization.
float winding_number(std::span< const glm::vec2 > path) noexcept
Net revolutions a path turned through.
float angular_delta(float to, float from) noexcept
Shortest signed angular difference between two headings.
TurningProfile turning_profile(std::span< const glm::vec2 > path, size_t samples, bool rotation_invariant=true)
Build a turning profile from a path.
float cross_2d(const glm::vec2 &a, const glm::vec2 &b) noexcept
2D scalar cross product, sign of turn direction
std::vector< glm::vec2 > normalize_shape(std::span< const glm::vec2 > path)
Centre a path at the origin and scale it to unit spread.
std::vector< glm::vec2 > resample_uniform(std::span< const glm::vec2 > path, size_t count)
Resample a path to a fixed point count at uniform arc length.
float signed_turning(std::span< const glm::vec2 > path) noexcept
Turning accumulated along a path, with sign retained.
SpatialField distance(const glm::vec3 &anchor, float radius, DistanceMetric metric=DistanceMetric::EUCLIDEAN)
Normalized distance from an anchor point using the specified metric.
float signed_area(std::span< const glm::vec2 > path) noexcept
Signed area enclosed by treating the path as a closed polygon.
float heading(const glm::vec2 &v) noexcept
Heading angle of a 2D vector.
float profile_distance(const TurningProfile &a, const TurningProfile &b) noexcept
Root mean square difference between two turning profiles.
float dtw_cost(std::span< const T > a, std::span< const T > b, const std::function< float(const T &, const T &)> &distance, size_t band=0)
Dynamic time warping cost between two sequences.
double rms(const std::vector< double > &data)
Calculate RMS (Root Mean Square) energy of single-channel data.
double mean(const std::vector< double > &data)
Calculate mean of single-channel data.
float initial_heading
Heading of the first segment, before any invariance subtraction.
float length
Total arc length of the source path.
std::vector< float > turning
Unwrapped heading at each arc-length sample.
A path's heading as a function of arc length, at a fixed sample count.