-
Notifications
You must be signed in to change notification settings - Fork 0
60 task write trails drawing feature functions total distance updated #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f40b12
de32337
591a752
ac26494
0952f4a
3983c9c
3843b46
a3efd89
e0cdea0
3489b85
eb4b522
4e4ca3e
a3a1918
e6c7536
467553c
10957f9
9e4883e
eded58d
679cffc
e9f54eb
c5ce67c
0c40daf
215f701
9d4e462
abf56bd
3c0dea4
fa2a84c
8292a6c
dc4a43f
8838cbc
3b5907a
ecd4ea1
d33c221
de67b85
078743b
d9d382e
faf5d73
baa1638
a6a02b3
384a139
52d16fe
7d2a165
00e9968
0fbf5a5
a4ebe58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,8 @@ | |
| import pydantic | ||
| import scipy.spatial.distance as dist | ||
|
|
||
| from graphomotor.core import config | ||
|
|
||
|
|
||
| class Drawing(pydantic.BaseModel): | ||
| """Class representing a drawing task, encapsulating both raw data and metadata. | ||
|
|
@@ -248,12 +250,6 @@ def valid_ink_trajectory( | |
| ): | ||
| ink_end_idx = idx | ||
| break | ||
| if ( | ||
| ink_start_idx is not None | ||
| and ink_end_idx is not None | ||
| and ink_end_idx > ink_start_idx | ||
| ): | ||
| self.ink_points = self.points.iloc[ink_start_idx : ink_end_idx + 1].copy() | ||
|
|
||
| return ink_start_idx, ink_end_idx | ||
|
|
||
|
|
@@ -352,3 +348,140 @@ def detect_hesitations(self, threshold_percentile: int = 20) -> None: | |
| self.hesitation_duration = np.sum(hesitations) * dt[0] | ||
|
|
||
| return | ||
|
|
||
| def calculate_smoothness(self) -> None: | ||
| """Calculate path smoothness based on Root Mean Square (RMS) curvature. | ||
|
|
||
| Represents the curvature per unit arc length. | ||
| Lower values indicate smoother drawings. Penalizes sharp corners (e.g., | ||
| 90° turns) and noisy corrections. Normalized by arc length to reduce | ||
| sampling-rate dependence. | ||
| """ | ||
| if len(self.ink_points) < 3: | ||
| return | ||
|
|
||
| xy = self.ink_points[["x", "y"]].to_numpy() | ||
|
|
||
| forward_vector = xy[1:-1] - xy[:-2] | ||
| backward_vector = xy[2:] - xy[1:-1] | ||
|
|
||
| forward_norm = np.linalg.norm(forward_vector, axis=1) | ||
| backward_norm = np.linalg.norm(backward_vector, axis=1) | ||
|
|
||
| valid = (forward_norm > 0) & (backward_norm > 0) | ||
| if not np.any(valid): | ||
| return | ||
|
|
||
| valid_forward_vector = forward_vector[valid] | ||
| valid_backward_vector = backward_vector[valid] | ||
| valid_forward_norm = forward_norm[valid] | ||
| valid_backward_norm = backward_norm[valid] | ||
|
|
||
| cos_angle = (valid_forward_vector * valid_backward_vector).sum(axis=1) / ( | ||
| valid_forward_norm * valid_backward_norm | ||
| ) | ||
| cos_angle = np.clip(cos_angle, -1.0, 1.0) | ||
|
|
||
| angles = np.arccos(cos_angle) | ||
|
|
||
| avg_segment_length = (valid_forward_norm + valid_backward_norm) / 2.0 | ||
| curvatures = angles / avg_segment_length | ||
|
|
||
| self.smoothness = float(np.sqrt(np.mean(curvatures**2))) | ||
|
|
||
| return | ||
|
|
||
| def compute_segment_metrics( | ||
| self, circles: dict[str, dict[str, CircleTarget]], trail_id: str | ||
| ) -> None: | ||
| """Compute all metrics for a line segment. | ||
|
|
||
| This function computes various metrics for the line segment, including ink time, | ||
| velocity metrics, path optimality, smoothness, and hesitation detection. It | ||
| first determines the valid ink trajectory between the start and end circles. If | ||
| a valid trajectory is found, it updates the ink_points attribute and calculates | ||
| the metrics. | ||
|
|
||
| Args: | ||
| circles: A dictionary mapping each trail type to dictionaries of | ||
| CircleTarget instances (output of load_scaled_circles in config). | ||
| trail_id: Trail identifier for circle lookup. | ||
| """ | ||
| logger = config.get_logger() | ||
| trail_circles = circles[trail_id] | ||
| points = self.points.copy() | ||
|
|
||
| if len(points) < 2: | ||
| logger.warning( | ||
| "Not enough points to calculate metrics for line segment: " | ||
| "start=%s end=%s", | ||
| self.start_label, | ||
| self.end_label, | ||
| ) | ||
| return | ||
|
|
||
| if self.start_label not in trail_circles or self.end_label not in trail_circles: | ||
| logger.warning( | ||
| "Missing start/end labels: start=%s end=%s available=%s", | ||
| self.start_label, | ||
| self.end_label, | ||
| list(trail_circles.keys()), | ||
| ) | ||
| return | ||
|
|
||
| start_circle = trail_circles[self.start_label] | ||
| end_circle = trail_circles[self.end_label] | ||
|
|
||
| ink_start_idx, ink_end_idx = self.valid_ink_trajectory(start_circle, end_circle) | ||
|
|
||
| if ink_start_idx is None: | ||
| logger.warning( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should exit and return right? I prefer that over the long if/elif chain. Let me know if this still isn't clear, we can chat about it 1-1
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was more compressed the other way, I still have to do these checks but since you wanted it to be more readable I broke it all up |
||
| "No valid ink trajectory found for line segment: start=%s end=%s", | ||
| self.start_label, | ||
| self.end_label, | ||
| ) | ||
| return | ||
| if ink_end_idx is None: | ||
| self.ink_points = points.iloc[ink_start_idx:].copy() | ||
| if len(self.ink_points) < 2: | ||
| logger.warning( | ||
| "Not enough ink points to calculate metrics for line segment: " | ||
| "start=%s end=%s", | ||
| self.start_label, | ||
| self.end_label, | ||
| ) | ||
| return | ||
| self.ink_time = ( | ||
| self.ink_points.iloc[-1]["seconds"] - self.ink_points.iloc[0]["seconds"] | ||
| ) | ||
| return | ||
| if ink_end_idx <= ink_start_idx: | ||
| logger.warning( | ||
| "Invalid ink trajectory: end index (%d) is not greater than " | ||
| "start index (%d) for line segment: start=%s end=%s", | ||
| ink_end_idx, | ||
| ink_start_idx, | ||
| self.start_label, | ||
| self.end_label, | ||
| ) | ||
| return | ||
| self.ink_points = self.points.iloc[ink_start_idx : ink_end_idx + 1].copy() | ||
|
|
||
| if len(self.ink_points) < 2: | ||
| logger.warning( | ||
| "Not enough ink points to calculate metrics for line segment: " | ||
| "start=%s end=%s", | ||
| self.start_label, | ||
| self.end_label, | ||
| ) | ||
| return | ||
|
|
||
| self.ink_time = ( | ||
| self.ink_points.iloc[-1]["seconds"] - self.ink_points.iloc[0]["seconds"] | ||
| ) | ||
| self.calculate_velocity_metrics() | ||
| self.calculate_path_optimality(start_circle, end_circle) | ||
| self.calculate_smoothness() | ||
| self.detect_hesitations() | ||
|
|
||
| return | ||
Uh oh!
There was an error while loading. Please reload this page.