|
6 | 6 | by Ti Wang, Xiaohang Yu, and Mackenzie Weygandt Mathis |
7 | 7 | Licensed under Apache 2.0 |
8 | 8 |
|
9 | | -Tests for fmpose3d/fmpose3d.py — the high-level inference API. |
10 | | -
|
11 | | -Organised into: |
12 | | - * Unit tests for individual components (no pretrained weights needed). |
13 | | - * Functional integration tests for the full human and animal pipelines |
14 | | - (require pretrained weights on disk; marked ``@pytest.mark.functional``). |
| 9 | +Unit tests for fmpose3d/fmpose3d.py — the high-level inference API. |
15 | 10 | """ |
16 | 11 |
|
17 | 12 | from __future__ import annotations |
|
42 | 37 | ) |
43 | 38 | from fmpose3d.common.config import FMPose3DConfig, InferenceConfig |
44 | 39 |
|
45 | | -# --------------------------------------------------------------------------- |
46 | | -# Paths |
47 | | -# --------------------------------------------------------------------------- |
48 | | - |
49 | | -PROJECT_ROOT = Path(__file__).resolve().parent.parent |
50 | | - |
51 | | -def _find_first(candidates: list[Path]) -> Path | None: |
52 | | - """Return the first path that exists, or ``None``.""" |
53 | | - for p in candidates: |
54 | | - if p.exists(): |
55 | | - return p |
56 | | - return None |
57 | | - |
58 | | - |
59 | | -HUMAN_WEIGHTS = _find_first([ |
60 | | - PROJECT_ROOT / "pre_trained_models" / "fmpose3d_h36m" / "FMpose3D_pretrained_weights.pth", |
61 | | -]) |
62 | | -ANIMAL_WEIGHTS = _find_first([ |
63 | | - PROJECT_ROOT / "animals" / "pre_trained_models" / "fmpose3d_animals" / "fmpose3d_animals_pretrained_weights.pth", |
64 | | - PROJECT_ROOT / "pre_trained_models" / "fmpose3d_animals" / "fmpose3d_animals_pretrained_weights.pth", |
65 | | -]) |
66 | | -HUMAN_TEST_IMAGE = _find_first([ |
67 | | - PROJECT_ROOT / "demo" / "images" / "running.png", |
68 | | -]) |
69 | | -ANIMAL_TEST_IMAGE = _find_first([ |
70 | | - PROJECT_ROOT / "animals" / "demo" / "images" / "dog.JPEG", |
71 | | - PROJECT_ROOT / "animals" / "demo" / "images" / "dog.jpeg", |
72 | | - PROJECT_ROOT / "animals" / "demo" / "images" / "dog.jpg", |
73 | | -]) |
74 | | - |
75 | | -# Evaluated at collection time — files either exist or they don't. |
76 | | -_human_pipeline_available = HUMAN_WEIGHTS is not None and HUMAN_TEST_IMAGE is not None |
77 | | -_animal_pipeline_available = ANIMAL_WEIGHTS is not None and ANIMAL_TEST_IMAGE is not None |
78 | | - |
79 | | -try: |
80 | | - import deeplabcut # noqa: F401 |
81 | | - |
82 | | - _dlc_available = True |
83 | | -except ImportError: |
84 | | - _dlc_available = False |
85 | | - |
86 | | -_animal_pipeline_available = _animal_pipeline_available and _dlc_available |
87 | | - |
88 | | - |
89 | 40 | # --------------------------------------------------------------------------- |
90 | 41 | # Helpers |
91 | 42 | # --------------------------------------------------------------------------- |
@@ -747,174 +698,3 @@ def test_predict_maps_valid_bodyparts(self): |
747 | 698 | assert scores.shape == (1, 1, 26) |
748 | 699 | # target[24] ← source[0] → (0*3, 0*3+1) = (0.0, 1.0) |
749 | 700 | np.testing.assert_allclose(kpts[0, 0, 24], fake_bp[0, 0, :2]) |
750 | | - |
751 | | - |
752 | | -# ========================================================================= |
753 | | -# Functional tests — human pipeline |
754 | | -# ========================================================================= |
755 | | - |
756 | | - |
757 | | -@pytest.mark.functional |
758 | | -@pytest.mark.skipif( |
759 | | - not _human_pipeline_available, |
760 | | - reason="Human pretrained weights or test image not found", |
761 | | -) |
762 | | -class TestHumanPipeline: |
763 | | - """End-to-end integration test for the human (17-joint H36M) pipeline.""" |
764 | | - |
765 | | - @pytest.fixture(scope="class") |
766 | | - def pipeline(self): |
767 | | - """Run the full pipeline once and cache all results.""" |
768 | | - from PIL import Image |
769 | | - |
770 | | - api = FMPose3DInference( |
771 | | - model_weights_path=str(HUMAN_WEIGHTS), |
772 | | - device="cpu", |
773 | | - ) |
774 | | - |
775 | | - # 2D estimation |
776 | | - result_2d = api.prepare_2d(source=str(HUMAN_TEST_IMAGE)) |
777 | | - |
778 | | - # Image size |
779 | | - img = Image.open(str(HUMAN_TEST_IMAGE)) |
780 | | - w, h = img.size |
781 | | - image_size = (h, w) |
782 | | - |
783 | | - # 3D lifting — two runs with the same seed for reproducibility check |
784 | | - result_3d_a = api.pose_3d( |
785 | | - result_2d.keypoints, image_size=image_size, seed=42, |
786 | | - ) |
787 | | - result_3d_b = api.pose_3d( |
788 | | - result_2d.keypoints, image_size=image_size, seed=42, |
789 | | - ) |
790 | | - |
791 | | - return { |
792 | | - "result_2d": result_2d, |
793 | | - "image_size": image_size, |
794 | | - "result_3d_a": result_3d_a, |
795 | | - "result_3d_b": result_3d_b, |
796 | | - } |
797 | | - |
798 | | - def test_2d_keypoints_shape(self, pipeline): |
799 | | - r2d = pipeline["result_2d"] |
800 | | - P, F, J, C = r2d.keypoints.shape |
801 | | - assert J == 17 |
802 | | - assert C == 2 |
803 | | - assert F >= 1 |
804 | | - |
805 | | - def test_2d_scores_shape(self, pipeline): |
806 | | - r2d = pipeline["result_2d"] |
807 | | - assert r2d.scores.ndim == 3 |
808 | | - assert r2d.scores.shape[2] == 17 |
809 | | - |
810 | | - def test_2d_image_size(self, pipeline): |
811 | | - r2d = pipeline["result_2d"] |
812 | | - h, w = pipeline["image_size"] |
813 | | - assert r2d.image_size == (h, w) |
814 | | - |
815 | | - def test_3d_poses_shape(self, pipeline): |
816 | | - r3d = pipeline["result_3d_a"] |
817 | | - F = pipeline["result_2d"].keypoints.shape[1] |
818 | | - assert r3d.poses_3d.shape == (F, 17, 3) |
819 | | - assert r3d.poses_3d_world.shape == (F, 17, 3) |
820 | | - |
821 | | - def test_root_joint_zeroed(self, pipeline): |
822 | | - r3d = pipeline["result_3d_a"] |
823 | | - np.testing.assert_allclose(r3d.poses_3d[:, 0, :], 0.0, atol=1e-6) |
824 | | - |
825 | | - def test_world_z_floor(self, pipeline): |
826 | | - r3d = pipeline["result_3d_a"] |
827 | | - assert np.min(r3d.poses_3d_world[:, :, 2]) >= -1e-6 |
828 | | - |
829 | | - def test_poses_finite(self, pipeline): |
830 | | - r3d = pipeline["result_3d_a"] |
831 | | - assert np.all(np.isfinite(r3d.poses_3d)) |
832 | | - assert np.all(np.isfinite(r3d.poses_3d_world)) |
833 | | - |
834 | | - def test_reproducibility(self, pipeline): |
835 | | - r3d_a = pipeline["result_3d_a"] |
836 | | - r3d_b = pipeline["result_3d_b"] |
837 | | - np.testing.assert_allclose(r3d_a.poses_3d, r3d_b.poses_3d, atol=1e-6) |
838 | | - np.testing.assert_allclose( |
839 | | - r3d_a.poses_3d_world, r3d_b.poses_3d_world, atol=1e-6, |
840 | | - ) |
841 | | - |
842 | | - |
843 | | -# ========================================================================= |
844 | | -# Functional tests — animal pipeline |
845 | | -# ========================================================================= |
846 | | - |
847 | | - |
848 | | -@pytest.mark.functional |
849 | | -@pytest.mark.skipif( |
850 | | - not _animal_pipeline_available, |
851 | | - reason="Animal pretrained weights, test image, or DeepLabCut not available", |
852 | | -) |
853 | | -class TestAnimalPipeline: |
854 | | - """End-to-end integration test for the animal (26-joint Animal3D) pipeline.""" |
855 | | - |
856 | | - @pytest.fixture(scope="class") |
857 | | - def pipeline(self): |
858 | | - """Run the full pipeline once and cache all results.""" |
859 | | - from PIL import Image |
860 | | - |
861 | | - api = FMPose3DInference.for_animals( |
862 | | - model_weights_path=str(ANIMAL_WEIGHTS), |
863 | | - device="cpu", |
864 | | - ) |
865 | | - |
866 | | - result_2d = api.prepare_2d(source=str(ANIMAL_TEST_IMAGE)) |
867 | | - |
868 | | - img = Image.open(str(ANIMAL_TEST_IMAGE)) |
869 | | - w, h = img.size |
870 | | - image_size = (h, w) |
871 | | - |
872 | | - result_3d_a = api.pose_3d( |
873 | | - result_2d.keypoints, image_size=image_size, seed=42, |
874 | | - ) |
875 | | - result_3d_b = api.pose_3d( |
876 | | - result_2d.keypoints, image_size=image_size, seed=42, |
877 | | - ) |
878 | | - |
879 | | - return { |
880 | | - "result_2d": result_2d, |
881 | | - "image_size": image_size, |
882 | | - "result_3d_a": result_3d_a, |
883 | | - "result_3d_b": result_3d_b, |
884 | | - } |
885 | | - |
886 | | - def test_2d_keypoints_shape(self, pipeline): |
887 | | - r2d = pipeline["result_2d"] |
888 | | - _, F, J, C = r2d.keypoints.shape |
889 | | - assert J == 26 |
890 | | - assert C == 2 |
891 | | - |
892 | | - def test_2d_scores_shape(self, pipeline): |
893 | | - r2d = pipeline["result_2d"] |
894 | | - assert r2d.scores.ndim == 3 |
895 | | - assert r2d.scores.shape[2] == 26 |
896 | | - |
897 | | - def test_3d_poses_shape(self, pipeline): |
898 | | - r3d = pipeline["result_3d_a"] |
899 | | - F = pipeline["result_2d"].keypoints.shape[1] |
900 | | - assert r3d.poses_3d.shape == (F, 26, 3) |
901 | | - assert r3d.poses_3d_world.shape == (F, 26, 3) |
902 | | - |
903 | | - def test_poses_finite(self, pipeline): |
904 | | - r3d = pipeline["result_3d_a"] |
905 | | - assert np.all(np.isfinite(r3d.poses_3d)) |
906 | | - assert np.all(np.isfinite(r3d.poses_3d_world)) |
907 | | - |
908 | | - def test_poses_reasonable_magnitude(self, pipeline): |
909 | | - """Poses should not be excessively large (basic sanity).""" |
910 | | - r3d = pipeline["result_3d_a"] |
911 | | - assert np.max(np.abs(r3d.poses_3d)) < 1e4 |
912 | | - assert np.max(np.abs(r3d.poses_3d_world)) < 1e4 |
913 | | - |
914 | | - def test_reproducibility(self, pipeline): |
915 | | - r3d_a = pipeline["result_3d_a"] |
916 | | - r3d_b = pipeline["result_3d_b"] |
917 | | - np.testing.assert_allclose(r3d_a.poses_3d, r3d_b.poses_3d, atol=1e-6) |
918 | | - np.testing.assert_allclose( |
919 | | - r3d_a.poses_3d_world, r3d_b.poses_3d_world, atol=1e-6, |
920 | | - ) |
0 commit comments