From a4efaedd18ba406eaac5d4c9d35b0a0340656192 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Mar 2026 20:03:38 -0300 Subject: [PATCH 1/5] feat: add dtw types and interfaces Co-Authored-By: Claude Opus 4.6 --- src/dtw/types.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/dtw/types.ts diff --git a/src/dtw/types.ts b/src/dtw/types.ts new file mode 100644 index 0000000..dfa4c49 --- /dev/null +++ b/src/dtw/types.ts @@ -0,0 +1,25 @@ +export interface Landmark { + x: number; + y: number; + z: number; + visibility?: number; +} + +export interface TimeSeriesFrame { + timestamp: number; + landmarks: Landmark[]; +} + +export interface DTWResult { + distance: number; + normalizedDistance: number; + path: [number, number][]; + similarity: number; +} + +export interface DTWConfig { + windowSize?: number; + distanceMetric?: 'euclidean' | 'manhattan' | 'weighted'; + weights?: number[]; + threshold?: number; +} From a388e928a86db22dc603f48b91d4603ade286fb7 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Mar 2026 20:03:51 -0300 Subject: [PATCH 2/5] feat: add distance metrics Co-Authored-By: Claude Opus 4.6 --- src/dtw/distance.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/dtw/distance.ts diff --git a/src/dtw/distance.ts b/src/dtw/distance.ts new file mode 100644 index 0000000..8c5e8b5 --- /dev/null +++ b/src/dtw/distance.ts @@ -0,0 +1,37 @@ +import { Landmark } from './types'; + +export function euclideanDistance(a: Landmark[], b: Landmark[]): number { + const len = Math.min(a.length, b.length); + let sum = 0; + for (let i = 0; i < len; i++) { + const dx = a[i].x - b[i].x; + const dy = a[i].y - b[i].y; + const dz = a[i].z - b[i].z; + sum += dx * dx + dy * dy + dz * dz; + } + return Math.sqrt(sum); +} + +export function manhattanDistance(a: Landmark[], b: Landmark[]): number { + const len = Math.min(a.length, b.length); + let sum = 0; + for (let i = 0; i < len; i++) { + sum += Math.abs(a[i].x - b[i].x); + sum += Math.abs(a[i].y - b[i].y); + sum += Math.abs(a[i].z - b[i].z); + } + return sum; +} + +export function weightedDistance(a: Landmark[], b: Landmark[], weights: number[]): number { + const len = Math.min(a.length, b.length); + let sum = 0; + for (let i = 0; i < len; i++) { + const w = weights[i] ?? 1; + const dx = a[i].x - b[i].x; + const dy = a[i].y - b[i].y; + const dz = a[i].z - b[i].z; + sum += w * (dx * dx + dy * dy + dz * dz); + } + return Math.sqrt(sum); +} From a84a2f5d13a5328780ade9f760e1b5fe92e1c115 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Mar 2026 20:04:08 -0300 Subject: [PATCH 3/5] feat: add core dtw algorithm Co-Authored-By: Claude Opus 4.6 --- src/dtw/dtw.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/dtw/dtw.ts diff --git a/src/dtw/dtw.ts b/src/dtw/dtw.ts new file mode 100644 index 0000000..f653046 --- /dev/null +++ b/src/dtw/dtw.ts @@ -0,0 +1,80 @@ +import { TimeSeriesFrame, DTWResult, DTWConfig } from './types'; +import { euclideanDistance, manhattanDistance, weightedDistance } from './distance'; + +export function computeDTW( + series1: TimeSeriesFrame[], + series2: TimeSeriesFrame[], + config: DTWConfig = {}, +): DTWResult { + const n = series1.length; + const m = series2.length; + + if (n === 0 || m === 0) { + return { distance: Infinity, normalizedDistance: Infinity, path: [], similarity: 0 }; + } + + const { windowSize, distanceMetric = 'euclidean', weights } = config; + + const getDistance = (a: TimeSeriesFrame, b: TimeSeriesFrame): number => { + switch (distanceMetric) { + case 'manhattan': + return manhattanDistance(a.landmarks, b.landmarks); + case 'weighted': + return weightedDistance(a.landmarks, b.landmarks, weights ?? []); + default: + return euclideanDistance(a.landmarks, b.landmarks); + } + }; + + const matrix: number[][] = Array.from({ length: n }, () => Array(m).fill(Infinity)); + matrix[0][0] = getDistance(series1[0], series2[0]); + + for (let i = 1; i < n; i++) { + matrix[i][0] = matrix[i - 1][0] + getDistance(series1[i], series2[0]); + } + for (let j = 1; j < m; j++) { + matrix[0][j] = matrix[0][j - 1] + getDistance(series1[0], series2[j]); + } + + for (let i = 1; i < n; i++) { + const jStart = windowSize ? Math.max(1, i - windowSize) : 1; + const jEnd = windowSize ? Math.min(m - 1, i + windowSize) : m - 1; + + for (let j = jStart; j <= jEnd; j++) { + const cost = getDistance(series1[i], series2[j]); + matrix[i][j] = cost + Math.min( + matrix[i - 1][j], + matrix[i][j - 1], + matrix[i - 1][j - 1], + ); + } + } + + const path: [number, number][] = []; + let i = n - 1; + let j = m - 1; + path.push([i, j]); + + while (i > 0 || j > 0) { + if (i === 0) { + j--; + } else if (j === 0) { + i--; + } else { + const candidates = [matrix[i - 1][j - 1], matrix[i - 1][j], matrix[i][j - 1]]; + const minIndex = candidates.indexOf(Math.min(...candidates)); + if (minIndex === 0) { i--; j--; } + else if (minIndex === 1) { i--; } + else { j--; } + } + path.push([i, j]); + } + + path.reverse(); + + const distance = matrix[n - 1][m - 1]; + const normalizedDistance = distance / path.length; + const similarity = 1 / (1 + normalizedDistance); + + return { distance, normalizedDistance, path, similarity }; +} From 9d920ba041df30d51170e38c1007e8f2d893a226 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Mar 2026 20:04:29 -0300 Subject: [PATCH 4/5] feat: add normalization stubs Co-Authored-By: Claude Opus 4.6 --- src/dtw/normalize.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/dtw/normalize.ts diff --git a/src/dtw/normalize.ts b/src/dtw/normalize.ts new file mode 100644 index 0000000..3269d6d --- /dev/null +++ b/src/dtw/normalize.ts @@ -0,0 +1,40 @@ +import { Landmark, TimeSeriesFrame } from './types'; + +/** + * TODO: Normalização espacial dos landmarks. + * - Centralização pelo pulso/centro da mão + * - Normalização por escala + * - Coordenadas relativas + */ +export function normalizeSpatial(frames: TimeSeriesFrame[]): TimeSeriesFrame[] { + return frames; +} + +export function centerByWrist(landmarks: Landmark[], wristIndex: number = 0): Landmark[] { + const wrist = landmarks[wristIndex]; + if (!wrist) return landmarks; + + return landmarks.map((lm) => ({ + x: lm.x - wrist.x, + y: lm.y - wrist.y, + z: lm.z - wrist.z, + visibility: lm.visibility, + })); +} + +export function scaleNormalize(landmarks: Landmark[]): Landmark[] { + let maxDist = 0; + for (const lm of landmarks) { + const dist = Math.sqrt(lm.x * lm.x + lm.y * lm.y + lm.z * lm.z); + if (dist > maxDist) maxDist = dist; + } + + if (maxDist === 0) return landmarks; + + return landmarks.map((lm) => ({ + x: lm.x / maxDist, + y: lm.y / maxDist, + z: lm.z / maxDist, + visibility: lm.visibility, + })); +} From 8e2d560833f26d250fcd0d94d29d0e1220bd7148 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Mon, 2 Mar 2026 20:04:29 -0300 Subject: [PATCH 5/5] feat: add interpolation stubs Co-Authored-By: Claude Opus 4.6 --- src/dtw/interpolate.ts | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/dtw/interpolate.ts diff --git a/src/dtw/interpolate.ts b/src/dtw/interpolate.ts new file mode 100644 index 0000000..2e05fe1 --- /dev/null +++ b/src/dtw/interpolate.ts @@ -0,0 +1,43 @@ +import { TimeSeriesFrame, Landmark } from './types'; + +/** + * TODO: Interpolação temporal para uniformizar a quantidade de frames. + * - Reamostragem para N frames + * - Interpolação linear entre frames + */ +export function interpolateToLength(frames: TimeSeriesFrame[], targetLength: number): TimeSeriesFrame[] { + if (frames.length === 0 || targetLength <= 0) return []; + if (frames.length === targetLength) return frames; + + const result: TimeSeriesFrame[] = []; + const ratio = (frames.length - 1) / (targetLength - 1); + + for (let i = 0; i < targetLength; i++) { + const pos = i * ratio; + const low = Math.floor(pos); + const high = Math.min(Math.ceil(pos), frames.length - 1); + const t = pos - low; + + if (low === high) { + result.push(frames[low]); + continue; + } + + const interpolated: Landmark[] = frames[low].landmarks.map((lm, idx) => { + const other = frames[high].landmarks[idx]; + return { + x: lm.x + t * (other.x - lm.x), + y: lm.y + t * (other.y - lm.y), + z: lm.z + t * (other.z - lm.z), + visibility: lm.visibility, + }; + }); + + result.push({ + timestamp: frames[low].timestamp + t * (frames[high].timestamp - frames[low].timestamp), + landmarks: interpolated, + }); + } + + return result; +}