Skip to content

Commit

Permalink
Merge pull request #642 from mprib/lint-with-tests
Browse files Browse the repository at this point in the history
Lint with tests

Lots of code changes here which are generally superficial adjustments to allow for ruff linting. Test automation was greatly improved by testing across multiple OS's and python versions. Much thanks to DavidPagnon for feedback stemming from the JOSS review.
  • Loading branch information
mprib committed Sep 15, 2024
2 parents 161cb8b + ee8e0ac commit 844e6ef
Show file tree
Hide file tree
Showing 94 changed files with 2,071 additions and 3,212 deletions.
53 changes: 26 additions & 27 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
if: >
github.event.pull_request.merged == true ||
github.event_name == 'workflow_dispatch'
strategy:
fail-fast: true
matrix:
python-version: ['3.10','3.11']
os: [ubuntu-latest, macos-latest, windows-latest]
Expand All @@ -29,40 +30,38 @@ jobs:
path: ~\AppData\Local\pip\Cache

steps:
- uses: actions/checkout@v2
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

# note this install is likely relevant for all ubuntu users...
# taken from https://www.reddit.com/r/github/comments/10aenmk/help_linux_action_cant_find_opengl_all_of_a_sudden/
- name: Update packages
# Ubuntu-specific steps
- name: Update packages (Ubuntu)
if: runner.os == 'Linux'
run: sudo apt-get update

- name: Install packages
- name: Install packages (Ubuntu)
if: runner.os == 'Linux'
run: sudo apt-get install --fix-missing libgl1-mesa-dev

# - name: Install X11 and xcb dependencies
# run: |
# sudo apt-get update
# sudo apt-get install -y xvfb libxcb1 x11-apps

- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Configure Poetry

# macOS-specific steps
- name: Set environment variables (macOS)
if: runner.os == 'macOS'
run: |
echo "$HOME/.local/bin" >> $GITHUB_PATH
poetry config virtualenvs.create false
echo "MKL_NUM_THREADS=1" >> $GITHUB_ENV
echo "NUMEXPR_NUM_THREADS=1" >> $GITHUB_ENV
echo "OMP_NUM_THREADS=1" >> $GITHUB_ENV
- name: Install dependencies
run: poetry install

# - name: Run tests with xvfb
# run: xvfb-run --auto-servernum --server-args='-screen 0, 1024x768x24' poetry run pytest
run: |
python -m pip install --upgrade pip
python -m pip install ruff pytest
pip install -e .
- name: Run tests
run: poetry run pytest
- name: Lint with Ruff
run: ruff check .
- name: Test with pytest
run: pytest

48 changes: 24 additions & 24 deletions caliscope/__init__.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
"""Top-level package for basic_template_repo."""

import os
import platform
from pathlib import Path

import rtoml
import platform

__package_name__ = "caliscope"
__version__ = "v0.2.4"

__author__ = """Mac Prible"""
__email__ = "prible@gmail.com"
__repo_owner_github_user_name__ = "mprib"
__repo_url__ = (
f"https://github.com/{__repo_owner_github_user_name__}/{__package_name__}/"
)
__repo_url__ = f"https://github.com/{__repo_owner_github_user_name__}/{__package_name__}/"
__repo_issues_url__ = f"{__repo_url__}issues"



# Determine platform-specific application data directory
if platform.system() == "Windows":
print("Windows platform identified")
app_data_dir = os.getenv('LOCALAPPDATA')
app_data_dir = os.getenv("LOCALAPPDATA")
else: # macOS, Linux, and other UNIX variants
print(f"Non-windows platform identified: {platform.system()}")
app_data_dir = os.path.join(os.path.expanduser("~"), '.local', 'share')
app_data_dir = os.path.join(os.path.expanduser("~"), ".local", "share")

__app_dir__ = Path(app_data_dir, __package_name__)
__app_dir__.mkdir(exist_ok=True, parents=True)

# Create a toml file for user settings in app data directory and default the project folder to USER
__settings_path__ = Path(__app_dir__, 'settings.toml')
__settings_path__ = Path(__app_dir__, "settings.toml")

# Get user home directory in a cross-platform way
__user_dir__ = Path(os.path.expanduser("~"))
Expand All @@ -38,11 +37,13 @@
USER_SETTINGS = rtoml.load(__settings_path__)
else:
# default to storing projects in user/__package_name__
USER_SETTINGS = {"recent_projects":[],
"last_project_parent":str(__user_dir__) # default initially to home...this will be where the 'New' folder dialog starts
}
USER_SETTINGS = {
"recent_projects": [],
"last_project_parent": str(
__user_dir__
), # default initially to home...this will be where the 'New' folder dialog starts
}


with open(__settings_path__, "a") as f:
rtoml.dump(USER_SETTINGS, f)

Expand All @@ -56,21 +57,20 @@

print(f"This is printing from: {__file__}")
print(f"Source code for this package is available at: {__repo_url__}")
print(
f"Log file associated with {__package_name__} is stored in {__log_dir__}"
)
print(f"Log file associated with {__package_name__} is stored in {__log_dir__}")


if __name__=="__main__":
if __name__ == "__main__":
import os
path = r'C:\Users\Mac Prible\AppData\Local\caliscope'
test_file = os.path.join(path, 'test.txt')
with open(test_file, 'w') as f:
f.write('Test')

path = r"C:\Users\Mac Prible\AppData\Local\caliscope"
test_file = os.path.join(path, "test.txt")
with open(test_file, "w") as f:
f.write("Test")
print(f"Test file created at: {test_file}")



import os
parent = r'C:\Users\Mac Prible\AppData\Local'

parent = r"C:\Users\Mac Prible\AppData\Local"
print(os.path.exists(parent))
print(os.listdir(parent))
print(os.listdir(parent))
2 changes: 1 addition & 1 deletion caliscope/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sys

from caliscope.gui.main_widget import launch_main
import caliscope.logger
from caliscope.gui.main_widget import launch_main

logger = caliscope.logger.get(__name__)

Expand Down
80 changes: 32 additions & 48 deletions caliscope/calibration/capture_volume/capture_volume.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
#%%
# %%

import caliscope.logger


from pathlib import Path
import pickle
from dataclasses import dataclass
import numpy as np
from pathlib import Path

import cv2
import numpy as np
from scipy.optimize import least_squares

import caliscope.logger
from caliscope.calibration.capture_volume.point_estimates import PointEstimates
from caliscope.calibration.charuco import Charuco
from caliscope.cameras.camera_array import CameraArray
from caliscope.calibration.capture_volume.set_origin_functions import (
get_board_origin_transform,
)
from caliscope.calibration.charuco import Charuco
from caliscope.cameras.camera_array import CameraArray

logger = caliscope.logger.get(__name__)

Expand All @@ -31,15 +30,12 @@ class CaptureVolume:

def __post__init__():
logger.info("Creating capture volume from estimated camera array and stereotriangulated points...")

def _save(self, directory: Path, descriptor: str = None):
if descriptor is None:
pkl_name = "capture_volume_stage_" + str(self.stage) + ".pkl"
else:

pkl_name = (
"capture_volume_stage_" + str(self.stage) + "_" + descriptor + ".pkl"
)
pkl_name = "capture_volume_stage_" + str(self.stage) + "_" + descriptor + ".pkl"
logger.info(f"Saving stage {str(self.stage)} capture volume to {directory}")
with open(Path(directory, pkl_name), "wb") as file:
pickle.dump(self, file)
Expand All @@ -56,31 +52,26 @@ def get_vectorized_params(self):

@property
def rmse(self):

if hasattr(self, "least_sq_result"):
rmse = rms_reproj_error(
self.least_sq_result.fun, self.point_estimates.camera_indices
)
rmse = rms_reproj_error(self.least_sq_result.fun, self.point_estimates.camera_indices)
else:
param_estimates = self.get_vectorized_params()
xy_reproj_error = xy_reprojection_error(param_estimates, self)
rmse = rms_reproj_error(
xy_reproj_error, self.point_estimates.camera_indices
)
rmse = rms_reproj_error(xy_reproj_error, self.point_estimates.camera_indices)

return rmse

def get_rmse_summary(self):
rmse_string = f"RMSE of Reprojection Overall: {round(self.rmse['overall'],2)}\n"
rmse_string+= " by camera:\n"
rmse_string += " by camera:\n"
for key, value in self.rmse.items():
if key == "overall":
pass
else:
rmse_string+=f" {key: >9}: {round(float(value),2)}\n"
rmse_string += f" {key: >9}: {round(float(value),2)}\n"

return rmse_string

def get_xy_reprojection_error(self):
vectorized_params = self.get_vectorized_params()
error = xy_reprojection_error(vectorized_params, self)
Expand All @@ -93,7 +84,7 @@ def optimize(self):
initial_param_estimate = self.get_vectorized_params()

# get a snapshot of where things are at the start
initial_xy_error = xy_reprojection_error(initial_param_estimate, self)
# initial_xy_error = xy_reprojection_error(initial_param_estimate, self)

# logger.info(
# f"Prior to bundle adjustment (stage {str(self.stage)}), RMSE is: {self.rmse}"
Expand All @@ -116,9 +107,7 @@ def optimize(self):
self.point_estimates.update_obj_xyz(self.least_sq_result.x)
self.stage += 1

logger.info(
f"Following bundle adjustment (stage {str(self.stage)}), RMSE is: {self.rmse['overall']}"
)
logger.info(f"Following bundle adjustment (stage {str(self.stage)}), RMSE is: {self.rmse['overall']}")

def get_xyz_points(self):
"""Get 3d positions arrived at by bundle adjustment"""
Expand All @@ -129,7 +118,6 @@ def get_xyz_points(self):
return xyz

def shift_origin(self, origin_shift_transform: np.ndarray):

# update 3d point estimates
xyz = self.point_estimates.obj
scale = np.expand_dims(np.ones(xyz.shape[0]), 1)
Expand All @@ -140,9 +128,7 @@ def shift_origin(self, origin_shift_transform: np.ndarray):

# update camera array
for port, camera_data in self.camera_array.cameras.items():
camera_data.transformation = np.matmul(
camera_data.transformation, origin_shift_transform
)
camera_data.transformation = np.matmul(camera_data.transformation, origin_shift_transform)

def set_origin_to_board(self, sync_index, charuco: Charuco):
"""
Expand All @@ -153,30 +139,29 @@ def set_origin_to_board(self, sync_index, charuco: Charuco):

logger.info(f"Capture volume origin set to board position at sync index {sync_index}")

origin_transform = get_board_origin_transform(
self.camera_array, self.point_estimates, sync_index, charuco
)
origin_transform = get_board_origin_transform(self.camera_array, self.point_estimates, sync_index, charuco)
self.shift_origin(origin_transform)


def xy_reprojection_error(current_param_estimates, capture_volume: CaptureVolume):
"""
current_param_estimates: the current iteration of the vector that was originally initialized for the x0 input of least squares
current_param_estimates:
the current iteration of the vector that was originally initialized for the x0 input of least squares
This function exists outside of the CaptureVolume class because the first argument must be the vector of parameters
that is being adjusted by the least_squares optimization.
"""
# Create one combined array primarily to make sure all calculations line up
## unpack the working estimates of the camera parameters (could be extr. or intr.)
camera_params = current_param_estimates[
: capture_volume.point_estimates.n_cameras * CAMERA_PARAM_COUNT
].reshape((capture_volume.point_estimates.n_cameras, CAMERA_PARAM_COUNT))
camera_params = current_param_estimates[: capture_volume.point_estimates.n_cameras * CAMERA_PARAM_COUNT].reshape(
(capture_volume.point_estimates.n_cameras, CAMERA_PARAM_COUNT)
)

## similarly unpack the 3d point location estimates
points_3d = current_param_estimates[
capture_volume.point_estimates.n_cameras * CAMERA_PARAM_COUNT :
].reshape((capture_volume.point_estimates.n_obj_points, 3))
points_3d = current_param_estimates[capture_volume.point_estimates.n_cameras * CAMERA_PARAM_COUNT :].reshape(
(capture_volume.point_estimates.n_obj_points, 3)
)

## create zero columns as placeholders for the reprojected 2d points
rows = capture_volume.point_estimates.camera_indices.shape[0]
Expand All @@ -195,7 +180,7 @@ def xy_reprojection_error(current_param_estimates, capture_volume: CaptureVolume
# iterate across cameras...while this injects a loop in the residual function
# it should scale linearly with the number of cameras...a tradeoff for stable
# and explicit calculations...

for port, cam in capture_volume.camera_array.cameras.items():
cam_points = np.where(capture_volume.point_estimates.camera_indices == port)
object_points = points_3d_and_2d[cam_points][:, 1:4]
Expand All @@ -209,9 +194,7 @@ def xy_reprojection_error(current_param_estimates, capture_volume: CaptureVolume
distortions = cam.distortions

# get the projection of the 2d points on the image plane; ignore the jacobian
cam_proj_points, _jac = cv2.projectPoints(
object_points.astype(np.float64), rvec, tvec, cam_matrix, distortions
)
cam_proj_points, _jac = cv2.projectPoints(object_points.astype(np.float64), rvec, tvec, cam_matrix, distortions)

points_3d_and_2d[cam_points, 6:8] = cam_proj_points[:, 0, :]

Expand Down Expand Up @@ -245,14 +228,15 @@ def rms_reproj_error(xy_reproj_error, camera_indices):
# logger.info(f"RMSE of reprojection is {rmse}")
return rmse


# def load_capture_volume(session_path:Path):
# config = get_config(session_directory)

# camera_array = get_camera_array(config)
# point_estimates = load_point_estimates(config)
# capture_volume = CaptureVolume(camera_array, point_estimates)

# capture_volume = CaptureVolume(camera_array, point_estimates)
# capture_volume.stage = config["capture_volume"]["stage"]
# # capture_volume.origin_sync_index = config["capture_volume"]["origin_sync_index"]

# return capture_volume
# return capture_volume
Loading

0 comments on commit 844e6ef

Please sign in to comment.