-
Notifications
You must be signed in to change notification settings - Fork 2
Distance measure signature #17
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
Merged
AmitMY
merged 23 commits into
sign-language-processing:main
from
cleong110:distance_measure_signature
Mar 6, 2025
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
5404307
Adding DistanceMeasure signature
cleong110 6ef9b51
Throwing in some .gitignores
cleong110 6606e8b
Implemented and tested PowerDistance
cleong110 2150c21
Merge branch 'main' into distance_measure_signature
cleong110 28e5aba
Add a ScoreWithSignature, and do some pylint fixes
cleong110 09e85cd
More pylint updates
cleong110 7cb1561
Take out unused test
cleong110 7965943
dedupe gitignore
cleong110 ab3a88d
cross platform paths
cleong110 ac8f6f6
np.ma to ma
cleong110 6e61688
Cleaned up example metrics
cleong110 f2e425b
Distance Measure cleanup and documentation, created with chatgpt help…
cleong110 8ab300e
Add some docstrings to DistanceMetric
cleong110 3b0c820
example metric construction uses generated poses by default
cleong110 00415f3
More updates to example script, including zero-padding
cleong110 9f37ee4
Remove unused/commented UnitTest
cleong110 d58833a
change constant naming for pylint
cleong110 d5a34ec
cleanup of test_distance_metric.py
cleong110 32233d9
Some cleanup of base including slightly simplified format()
cleong110 7d4843c
Don't print name: or n:
cleong110 2d8c800
Basic Score class
cleong110 4702d6e
Trying to fix a pytest bug
cleong110 55833ad
Unnecessary else after return
cleong110 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| from pathlib import Path | ||
| from pose_format import Pose | ||
| from pose_evaluation.metrics.distance_metric import DistanceMetric | ||
| from pose_evaluation.metrics.distance_measure import AggregatedPowerDistance | ||
| from pose_evaluation.metrics.base import BaseMetric | ||
| from pose_evaluation.metrics.test_distance_metric import get_poses | ||
| from pose_evaluation.utils.pose_utils import zero_pad_shorter_poses | ||
|
|
||
| if __name__ == "__main__": | ||
| # Define file paths for test pose data | ||
| reference_file = ( | ||
| Path("pose_evaluation") / "utils" / "test" / "test_data" / "colin-1-HOUSE.pose" | ||
| ) | ||
| hypothesis_file = ( | ||
| Path("pose_evaluation") / "utils" / "test" / "test_data" / "colin-2-HOUSE.pose" | ||
| ) | ||
|
|
||
| # Choose whether to load real files or generate test poses | ||
| # They have different lengths, and so some metrics will crash! | ||
| # Change to False to generate fake poses with known distances, e.g. all 0 and all 1 | ||
| USE_REAL_FILES = True | ||
|
|
||
| if USE_REAL_FILES: | ||
| poses = [ | ||
| Pose.read(hypothesis_file.read_bytes()), | ||
| Pose.read(reference_file.read_bytes()), | ||
| ] | ||
| # TODO: add PosePreprocessors to PoseDistanceMetrics, with their own signatures | ||
| poses = zero_pad_shorter_poses(poses) | ||
|
|
||
| else: | ||
| hypothesis, reference = get_poses(2, 2, conf1=1, conf2=1) | ||
| poses = [hypothesis, reference] | ||
|
|
||
| # Define distance metrics | ||
| mean_l1_metric = DistanceMetric( | ||
| "mean_l1_metric", distance_measure=AggregatedPowerDistance(1, 17) | ||
| ) | ||
| metrics = [ | ||
| BaseMetric("base"), | ||
| DistanceMetric("PowerDistanceMetric", AggregatedPowerDistance(2, 1)), | ||
| DistanceMetric("AnotherPowerDistanceMetric", AggregatedPowerDistance(1, 10)), | ||
| mean_l1_metric, | ||
| DistanceMetric( | ||
| "max_l1_metric", | ||
| AggregatedPowerDistance( | ||
| order=1, aggregation_strategy="max", default_distance=0 | ||
| ), | ||
| ), | ||
| DistanceMetric( | ||
| "MeanL2Score", | ||
| AggregatedPowerDistance( | ||
| order=2, aggregation_strategy="mean", default_distance=0 | ||
| ), | ||
| ), | ||
| ] | ||
|
|
||
| # Evaluate each metric on the test poses | ||
| for metric in metrics: | ||
| print("*" * 10) | ||
| print(metric.get_signature().format()) | ||
| print(metric.get_signature().format(short=True)) | ||
|
|
||
| try: | ||
| score = metric.score(poses[0], poses[1]) | ||
| print(f"SCORE: {score}") | ||
| print("SCORE With Signature:") | ||
| score_with_sig = metric.score_with_signature(poses[0], poses[1]) | ||
| print(score_with_sig) | ||
| print(repr(score_with_sig)) | ||
| print(f"{type(score_with_sig)}") | ||
|
|
||
| print(metric.score_with_signature(poses[0], poses[1], short=True)) | ||
|
|
||
| except NotImplementedError: | ||
| print(f"{metric} score not implemented") | ||
| print("*" * 10) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| from typing import Literal, Dict, Any | ||
| import numpy.ma as ma # pylint: disable=consider-using-from-import | ||
| from pose_evaluation.metrics.base import Signature | ||
|
|
||
| AggregationStrategy = Literal["max", "min", "mean", "sum"] | ||
|
|
||
| class DistanceMeasureSignature(Signature): | ||
| """Signature for distance measure metrics.""" | ||
| def __init__(self, name: str, args: Dict[str, Any]) -> None: | ||
| super().__init__(name=name, args=args) | ||
| self.update_abbr("distance", "dist") | ||
| self.update_abbr("power", "pow") | ||
|
|
||
|
|
||
| class DistanceMeasure: | ||
| """Abstract base class for distance measures.""" | ||
| _SIGNATURE_TYPE = DistanceMeasureSignature | ||
|
|
||
| def __init__(self, name: str) -> None: | ||
| self.name = name | ||
|
|
||
| def get_distance(self, hyp_data: ma.MaskedArray, ref_data: ma.MaskedArray) -> float: | ||
| """ | ||
| Compute the distance between hypothesis and reference data. | ||
|
|
||
| This method should be implemented by subclasses. | ||
| """ | ||
| raise NotImplementedError | ||
|
|
||
| def __call__(self, hyp_data: ma.MaskedArray, ref_data: ma.MaskedArray) -> float: | ||
| return self.get_distance(hyp_data, ref_data) | ||
|
|
||
| def get_signature(self) -> Signature: | ||
| """Return the signature of the distance measure.""" | ||
| return self._SIGNATURE_TYPE(self.name, self.__dict__) | ||
|
|
||
|
|
||
| class PowerDistanceSignature(DistanceMeasureSignature): | ||
| """Signature for power distance measures.""" | ||
| def __init__(self, name: str, args: Dict[str, Any]) -> None: | ||
| super().__init__(name=name, args=args) | ||
| self.update_signature_and_abbr("order", "ord", args) | ||
| self.update_signature_and_abbr("default_distance", "dflt", args) | ||
| self.update_signature_and_abbr("aggregation_strategy", "agg", args) | ||
|
|
||
|
|
||
| class AggregatedPowerDistance(DistanceMeasure): | ||
| """Aggregated power distance metric using a specified aggregation strategy.""" | ||
| _SIGNATURE_TYPE = PowerDistanceSignature | ||
|
|
||
| def __init__( | ||
| self, | ||
| order: int = 2, | ||
| default_distance: float = 0.0, | ||
| aggregation_strategy: AggregationStrategy = "mean", | ||
| ) -> None: | ||
| """ | ||
| Initialize the aggregated power distance metric. | ||
|
|
||
| :param order: The exponent to which differences are raised. | ||
| :param default_distance: The value to fill in for masked entries. | ||
| :param aggregation_strategy: Strategy to aggregate computed distances. | ||
| """ | ||
| super().__init__(name="power_distance") | ||
| self.power = float(order) | ||
| self.default_distance = default_distance | ||
| self.aggregation_strategy = aggregation_strategy | ||
|
|
||
| def _aggregate(self, distances: ma.MaskedArray) -> float: | ||
| """ | ||
| Aggregate computed distances using the specified strategy. | ||
|
|
||
| :param distances: A masked array of computed distances. | ||
| :return: A single aggregated distance value. | ||
| """ | ||
| aggregation_funcs = { | ||
| "mean": distances.mean, | ||
| "max": distances.max, | ||
| "min": distances.min, | ||
| "sum": distances.sum, | ||
| } | ||
| if self.aggregation_strategy in aggregation_funcs: | ||
| return aggregation_funcs[self.aggregation_strategy]() | ||
|
|
||
| raise NotImplementedError( | ||
| f"Aggregation Strategy {self.aggregation_strategy} not implemented" | ||
| ) | ||
|
|
||
| def _calculate_distances( | ||
| self, hyp_data: ma.MaskedArray, ref_data: ma.MaskedArray | ||
| ) -> ma.MaskedArray: | ||
| """ | ||
| Compute element-wise distances between hypothesis and reference data. | ||
|
|
||
| Steps: | ||
| 1. Compute the absolute differences. | ||
| 2. Raise the differences to the specified power. | ||
| 3. Sum the powered differences along the last axis. | ||
| 4. Extract the root corresponding to the power. | ||
| 5. Fill masked values with the default distance. | ||
|
|
||
| :param hyp_data: Hypothesis data as a masked array. | ||
| :param ref_data: Reference data as a masked array. | ||
| :return: A masked array of computed distances. | ||
| """ | ||
| diffs = ma.abs(hyp_data - ref_data) | ||
| raised_to_power = ma.power(diffs, self.power) | ||
| summed_results = ma.sum(raised_to_power, axis=-1, keepdims=True) | ||
| roots = ma.power(summed_results, 1 / self.power) | ||
| return ma.filled(roots, self.default_distance) | ||
|
|
||
| def get_distance(self, hyp_data: ma.MaskedArray, ref_data: ma.MaskedArray) -> float: | ||
| """Compute and aggregate the distance between hypothesis and reference data.""" | ||
| calculated = self._calculate_distances(hyp_data, ref_data) | ||
| return self._aggregate(calculated) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.