diff --git a/.github/workflows/build_test_wheel.yml b/.github/workflows/build_test_wheel.yml index 70ad4a1..4bba9a4 100644 --- a/.github/workflows/build_test_wheel.yml +++ b/.github/workflows/build_test_wheel.yml @@ -38,42 +38,88 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 + - name: Cache conda packages + id: conda-cache + uses: actions/cache@v4 with: - python-version: '3.11' - - name: Add conda to system path - run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH + path: /home/runner/miniconda3/pkgs + key: ${{ runner.os }}-conda-${{ hashFiles('environment-ci.yml') }} + restore-keys: | + ${{ runner.os }}-conda- + - name: Set up conda + uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-variant: Miniforge3 + miniforge-version: latest + environment-file: environment-ci.yml + activate-environment: autoforge + installation-dir: /home/runner/miniconda3 + auto-activate-base: false + pkgs-dirs: /home/runner/miniconda3/pkgs + use-mamba: true + use-only-tar-bz2: true - name: Install dependencies run: | - #conda env update --file environment.yml --name base - pip install . + "${CONDA}/bin/conda" run -n autoforge python -m pip install . verify-lint: runs-on: ubuntu-latest needs: build-package steps: - uses: actions/checkout@v4 + - name: Cache conda packages + id: conda-cache + uses: actions/cache@v4 + with: + path: /home/runner/miniconda3/pkgs + key: ${{ runner.os }}-conda-${{ hashFiles('environment-ci.yml') }} + restore-keys: | + ${{ runner.os }}-conda- + - name: Set up conda + uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-variant: Miniforge3 + miniforge-version: latest + environment-file: environment-ci.yml + activate-environment: autoforge + installation-dir: /home/runner/miniconda3 + auto-activate-base: false + pkgs-dirs: /home/runner/miniconda3/pkgs + use-mamba: true + use-only-tar-bz2: true - name: Lint with flake8 run: | - conda install flake8 - conda init - conda activate base + "${CONDA}/bin/conda" run -n autoforge python -m pip install flake8 # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + "${CONDA}/bin/conda" run -n autoforge flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + "${CONDA}/bin/conda" run -n autoforge flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics test-package: runs-on: ubuntu-latest needs: build-package steps: - uses: actions/checkout@v4 + - name: Cache conda packages + id: conda-cache + uses: actions/cache@v4 + with: + path: /home/runner/miniconda3/pkgs + key: ${{ runner.os }}-conda-${{ hashFiles('environment-ci.yml') }} + restore-keys: | + ${{ runner.os }}-conda- + - name: Set up conda + uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-variant: Miniforge3 + miniforge-version: latest + environment-file: environment-ci.yml + activate-environment: autoforge + installation-dir: /home/runner/miniconda3 + auto-activate-base: false + pkgs-dirs: /home/runner/miniconda3/pkgs + use-mamba: true + use-only-tar-bz2: true - name: Test with pytest run: | - conda install pytest - conda init - conda activate base - pytest # TODO add specifications of test + "${CONDA}/bin/conda" run -n autoforge python -m pytest # TODO add specifications of test diff --git a/.gitignore b/.gitignore index 600228f..d065b29 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +.vscode/browse.vc.db +.vscode/browse.vc.db-shm +.vscode/browse.vc.db-wal diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..5751b8f --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,20 @@ +{ + "configurations": [ + { + "browse": { + "databaseFilename": "${workspaceFolder}/.vscode/browse.vc.db", + "limitSymbolsToIncludedHeaders": false + }, + "includePath": [ + "/opt/ros/jazzy/include/**", + "/usr/include/**" + ], + "name": "ros2", + "intelliSenseMode": "gcc-x64", + "compilerPath": "/usr/bin/gcc", + "cStandard": "gnu11", + "cppStandard": "c++17" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/TODO b/TODO index 192a772..b0785ea 100644 --- a/TODO +++ b/TODO @@ -44,6 +44,7 @@ - [ ] Review and unit test selection criteria implementation - [ ] Extend augs-trainer integration to allow "discard" option in validation module - [x] Implement tailoring of RandomAffine to make it border crossing aware + - [ ] Migrate documentation from Read The Docs to something more controllable and less restrictive - [ ] v0.3.X - [x] Implement custom adaptive pooling layers for ONNx static export diff --git a/environment-ci.yml b/environment-ci.yml new file mode 100644 index 0000000..969da53 --- /dev/null +++ b/environment-ci.yml @@ -0,0 +1,9 @@ +name: autoforge +channels: + - conda-forge + - defaults +dependencies: + - python=3.11 + - pip + - setuptools + - wheel diff --git a/environment.yml b/environment.yml index c9d2869..4346132 100644 --- a/environment.yml +++ b/environment.yml @@ -197,7 +197,6 @@ dependencies: - pyasn1-modules==0.4.1 - pybind11==2.13.6 - pycparser==2.22 - - pycuda==2025.1 - pydantic==2.10.6 - pydantic-core==2.27.2 - pygments==2.19.1 diff --git a/pyTorchAutoForge.code-workspace b/pyTorchAutoForge.code-workspace index 09d71ce..c90466c 100644 --- a/pyTorchAutoForge.code-workspace +++ b/pyTorchAutoForge.code-workspace @@ -46,5 +46,13 @@ "path": "../ml-based-centroiding" } ], - "settings": {} + "settings": { + "ROS2.distro": "jazzy", + "python.autoComplete.extraPaths": [ + "/opt/ros/jazzy/lib/python3.12/site-packages" + ], + "python.analysis.extraPaths": [ + "/opt/ros/jazzy/lib/python3.12/site-packages" + ] + } } \ No newline at end of file diff --git a/pyTorchAutoForge/api/tensorrt/TRTengineExporter.py b/pyTorchAutoForge/api/tensorrt/TRTengineExporter.py index 07a6b5a..ef92fbc 100644 --- a/pyTorchAutoForge/api/tensorrt/TRTengineExporter.py +++ b/pyTorchAutoForge/api/tensorrt/TRTengineExporter.py @@ -2,7 +2,6 @@ import torch import numpy as np import sys, os, shutil -import pycuda as cuda import subprocess from enum import Enum from pyTorchAutoForge.api.onnx import ModelHandlerONNx @@ -167,4 +166,4 @@ def build_engine_from_onnx(self, input_onnx_model: str | None = None, output_eng print("Engine built successfully and saved to:", output_engine_path) else: - raise ValueError("Invalid TRTengineExporterMode value. Please select TRTEXEC or PYTHON.") \ No newline at end of file + raise ValueError("Invalid TRTengineExporterMode value. Please select TRTEXEC or PYTHON.") diff --git a/pyTorchAutoForge/datasets/LabelsClasses.py b/pyTorchAutoForge/datasets/LabelsClasses.py index 408be66..1cce880 100644 --- a/pyTorchAutoForge/datasets/LabelsClasses.py +++ b/pyTorchAutoForge/datasets/LabelsClasses.py @@ -30,6 +30,8 @@ class PTAF_Datakey(enum.Enum): PHASE_ANGLE = 9 # Phase angle of the scene CENTRE_OF_FIGURE = 10 # Centre of figure of the object APPARENT_SIZE = 11 # Apparent size of the object in pixels + SUN_DIRECTION_ANGLE_FROM_X = 12 # Angle of the sun direction from the x-axis in radians + def get_lbl_vector_size(self): # Define sizes for data keys @@ -49,7 +51,8 @@ def get_lbl_vector_size(self): PTAF_Datakey.REFERENCE_SIZE: 1, PTAF_Datakey.PHASE_ANGLE: 1, PTAF_Datakey.CENTRE_OF_FIGURE: 2, # x, y coordinates of the centre of figure - PTAF_Datakey.APPARENT_SIZE: 1 # Apparent size in pixels + PTAF_Datakey.APPARENT_SIZE: 1, # Apparent size in pixels + PTAF_Datakey.SUN_DIRECTION_ANGLE_FROM_X: 1 # Angle of the sun direction from the x-axis in radians } return sizes.get(self, None) @@ -235,7 +238,7 @@ class AuxiliaryLabels(BaseLabelsContainer): default=-1.0, metadata={'yaml': 'dPhaseAngleInDeg'}) light_direction_rad_angle_from_x: float = field( - default=0.0, metadata={'yaml': 'dLightDirectionRadAngleFromX'}) + default=-1.0, metadata={'yaml': 'dLightDirectionRadAngleFromX'}) object_shape_matrix_cam_frame: list[list[float]] = field( default_factory=list, metadata={'yaml': 'dObjectShapeMatrix_CamFrame'}) @@ -311,6 +314,9 @@ def __getattr__(self, item: str) -> Any: elif item == PTAF_Datakey.APPARENT_SIZE: return float(self.geometric.obj_apparent_size_in_pix) + + elif item == PTAF_Datakey.SUN_DIRECTION_ANGLE_FROM_X: + return float(self.auxiliary.light_direction_rad_angle_from_x) else: # Raise AttributeError to maintain standard behavior diff --git a/pyTorchAutoForge/model_building/backbones/base_backbones.py b/pyTorchAutoForge/model_building/backbones/base_backbones.py index d87e789..4f8f17c 100644 --- a/pyTorchAutoForge/model_building/backbones/base_backbones.py +++ b/pyTorchAutoForge/model_building/backbones/base_backbones.py @@ -25,6 +25,7 @@ class FeatureExtractorConfig(BaseConfigClass): # Dimension of the final linear layer (if you want to add a linear layer) output_size: int | None = None remove_classifier: bool = True + remove_gap_layer: bool = False device: torch.device | str | None = None input_channels: int = 3 # Placeholder value diff --git a/pyTorchAutoForge/model_building/backbones/efficient_net.py b/pyTorchAutoForge/model_building/backbones/efficient_net.py index 97c60ea..65a0b96 100644 --- a/pyTorchAutoForge/model_building/backbones/efficient_net.py +++ b/pyTorchAutoForge/model_building/backbones/efficient_net.py @@ -5,6 +5,7 @@ import torch import torch.nn.functional as F +import warnings from pyTorchAutoForge.model_building.backbones.spatial_features_operators import SpatialKptFeatureSoftmaxLocator from pyTorchAutoForge.model_building.poolingBlocks import CustomAdaptiveMaxPool2d @@ -22,7 +23,13 @@ def __init__(self, cfg: EfficientNetConfig): # Extract the “features” part all children except the final classifier/sequential modules = list(model.children())[:-1] + if cfg.output_type == 'last': + + if hasattr(cfg, 'remove_gap_layer'): + # Remove the final global average pooling layer (AdaptiveAvgPool2d) if specified in config + modules = modules[0] if isinstance(modules[-1], nn.AdaptiveAvgPool2d) and cfg.remove_gap_layer else modules + # Wrap as a single ModuleList so that forward is simple self.feature_extractor = nn.ModuleList([nn.Sequential(*modules)]) @@ -35,8 +42,10 @@ def __init__(self, cfg: EfficientNetConfig): self.feature_extractor = nn.ModuleList( list(feature_extractor_modules.children())) - # Add last layer (global adaptive pooling) from modules - self.feature_extractor.append(modules[1]) + # Add last layer (global adaptive pooling) from modules if not removed + if hasattr(cfg, 'remove_gap_layer'): + if isinstance(modules[-1], nn.AdaptiveAvgPool2d) and not(cfg.remove_gap_layer): + self.feature_extractor.append(modules[-1]) # Build average pooling layer self.feature_spill_preprocessor = nn.ModuleDict() diff --git a/pyproject.toml b/pyproject.toml index 56bb5cf..4f43fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ classifiers=[ [project.optional-dependencies] cuda_all = [ "pynvml; platform_machine == 'x86_64'", - "pycuda; platform_machine == 'x86_64'", ] [tool.setuptools.packages.find] diff --git a/tests/datasets/test_LabelsClasses.py b/tests/datasets/test_LabelsClasses.py index 87b469d..c023807 100644 --- a/tests/datasets/test_LabelsClasses.py +++ b/tests/datasets/test_LabelsClasses.py @@ -123,7 +123,12 @@ def test_labels_containers_instantiation() -> None: # Serialize to YAML string yaml_str = container.to_yaml() - print("YAML Output:\n", yaml_str) + yaml_dict = yaml.safe_load(yaml_str) + assert "geometric" in yaml_dict + assert "auxiliary" in yaml_dict + assert "kpts_heatmaps" in yaml_dict + assert yaml_dict["geometric"]["ui32ImageSize"] == [1296, 966] + assert yaml_dict["geometric"]["charBBoxCoordsOrder"] == "xywh" def test_lbl_containers_loading_from_yaml(): @@ -140,9 +145,13 @@ def test_lbl_containers_loading_from_yaml(): # Load from YAML file container = LabelsContainer.load_from_yaml(filepath) - import pprint - print("Loaded Container:") - pprint.pprint(container, indent=2) + assert container.geometric.ui32_image_size == (1296, 966) + assert container.geometric.bbox_coords_order == "xywh" + assert container.geometric.centre_of_figure == pytest.approx( + (522.6458119377869, 590.409585241252) + ) + assert container.geometric.distance_to_obj_centre == pytest.approx(1.3174400439758018e8) + assert container.auxiliary.phase_angle_in_deg == pytest.approx(73.39248871205007) def test_parse_ptaf_datakeys_valid_input(): @@ -199,7 +208,113 @@ def test_parse_ptaf_datakeys_partial_invalid(): assert "Unknown PTAF_Datakey: INVALID" in str(exc_info.value) +def test_to_yaml_uses_yaml_aliases(): + geom = GeometricLabels( + ui32_image_size=(10, 20), + centre_of_figure=(1.0, 2.0), + distance_to_obj_centre=3.0, + length_units="m", + bound_box_coordinates=(4.0, 5.0, 6.0, 7.0), + bbox_coords_order="xywh", + obj_apparent_size_in_pix=8.0, + object_reference_size=9.0, + object_ref_size_units="m", + obj_projected_ellipsoid_matrix=[[1.0, 0.0, 0.0]], + ) + aux = AuxiliaryLabels( + phase_angle_in_deg=15.0, + light_direction_rad_angle_from_x=0.5, + object_shape_matrix_cam_frame=[[1.0, 0.0, 0.0]], + ) + kpts = KptsHeatmapsLabels( + num_of_kpts=3, + heatmap_size=(32, 32), + heatmap_datatype="single", + ) + container = LabelsContainer(geometric=geom, auxiliary=aux, kpts_heatmaps=kpts) + + yaml_dict = yaml.safe_load(container.to_yaml()) + assert "geometric" in yaml_dict + assert "ui32ImageSize" in yaml_dict["geometric"] + assert "dCentreOfFigure" in yaml_dict["geometric"] + assert "auxiliary" in yaml_dict + assert "dPhaseAngleInDeg" in yaml_dict["auxiliary"] + assert "kpts_heatmaps" in yaml_dict + assert "ui32HeatmapSize" in yaml_dict["kpts_heatmaps"] + + +def test_from_dict_with_yaml_aliases_converts_types(): + data = { + "geometric": { + "ui32ImageSize": [10, 20], + "dCentreOfFigure": [1.0, 2.0], + "dDistanceToObjCentre": "3.5", + "charLengthUnits": "m", + "dBoundBoxCoordinates": [4.0, 5.0, 6.0, 7.0], + "charBBoxCoordsOrder": "xywh", + "dObjApparentSizeInPix": "8.25", + "dObjectReferenceSize": "9.0", + "dObjectRefSizeUnits": "m", + "dObjProjectedEllipsoidMatrix": [[1.0, 0.0, 0.0]], + }, + "auxiliary": { + "dPhaseAngleInDeg": "10.0", + "dLightDirectionRadAngleFromX": "0.5", + "dObjectShapeMatrix_CamFrame": [[1.0, 0.0, 0.0]], + }, + "kpts_heatmaps": { + "ui32NumOfKpts": 2, + "ui32HeatmapSize": [16, 24], + "charHeatMapDatatype": "double", + }, + } + + container = LabelsContainer.from_dict(data, yaml_aliases=True) + + assert container.geometric.ui32_image_size == (10, 20) + assert isinstance(container.geometric.ui32_image_size, tuple) + assert container.geometric.distance_to_obj_centre == 3.5 + assert isinstance(container.geometric.distance_to_obj_centre, float) + assert container.geometric.bound_box_coordinates == (4.0, 5.0, 6.0, 7.0) + assert container.kpts_heatmaps.heatmap_size == (16, 24) + + +def test_bbox_order_getattr_guard(): + geom = GeometricLabels( + bound_box_coordinates=(1.0, 2.0, 3.0, 4.0), + bbox_coords_order="xywh", + ) + container = LabelsContainer(geometric=geom) + + assert getattr(container, "BBOX_XYWH") == geom.bound_box_coordinates + with pytest.raises(AssertionError): + _ = getattr(container, "BBOX_XYXY") + + +def test_get_lbl_1d_vector_size(): + size, sizes_dict = LabelsContainer.get_lbl_1d_vector_size( + (PTAF_Datakey.CENTRE_OF_FIGURE, PTAF_Datakey.BBOX_XYWH) + ) + assert size == 6 + assert sizes_dict == {"CENTRE_OF_FIGURE": 2, "BBOX_XYWH": 4} + + +def test_get_labels_with_datakeys(): + geom = GeometricLabels( + centre_of_figure=(1.0, 2.0), + distance_to_obj_centre=3.0, + bound_box_coordinates=(4.0, 5.0, 6.0, 7.0), + ) + container = LabelsContainer(geometric=geom) + + labels = container.get_labels( + (PTAF_Datakey.CENTRE_OF_FIGURE, PTAF_Datakey.RANGE_TO_COM) + ) + assert labels.tolist() == [1.0, 2.0, 3.0] + + if __name__ == "__main__": #test_labels_containers_instantiation() #test_lbl_containers_loading_from_yaml() test_to_dict_and_from_dict() + pass