Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ py_bind/wheelhouse/*
build_artifacts
# conda smithy ci-skeleton end
*.whl
corres.0
linkage.0
prio.0
target_targets
5 changes: 5 additions & 0 deletions py_bind/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@

# Add the optv package directory to Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))

# Add the project root directory to Python path so that pyoptv can be imported
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
if project_root not in sys.path:
sys.path.insert(0, project_root)
1 change: 1 addition & 0 deletions py_bind/test/test_calibration_binding.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
# Importing the Calibration class, which provides methods to handle camera calibration data.
from optv.calibration import Calibration
import numpy, os, filecmp, shutil

Expand Down
123 changes: 123 additions & 0 deletions py_bind/test/test_pyoptv_calibration_binding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import unittest
# Importing the Calibration class, which provides methods to handle camera calibration data.
from pyoptv.calibration import Calibration
import numpy, os, filecmp, shutil

class Test_Calibration(unittest.TestCase):
def setUp(self):
self.input_ori_file_name = b"testing_fodder/calibration/cam1.tif.ori"
self.input_add_file_name = b"testing_fodder/calibration/cam2.tif.addpar"
self.output_directory = b"testing_fodder/calibration/testing_output/"

# create a temporary output directory (will be deleted by the end of test)
if not os.path.exists(self.output_directory):
os.makedirs(self.output_directory)

# create an instance of Calibration wrapper class
self.cal = Calibration()

def test_full_instantiate(self):
pos = numpy.r_[1., 3., 5.]
angs = numpy.r_[2., 4., 6.]
prim_point = pos * 3
rad_dist = pos * 4
decent = pos[:2] * 5
affine = decent * 1.5
glass = pos * 7

cal = Calibration(pos, angs, prim_point, rad_dist, decent, affine,
glass)

numpy.testing.assert_array_equal(pos, cal.get_pos())
numpy.testing.assert_array_equal(angs, cal.get_angles())
numpy.testing.assert_array_equal(prim_point, cal.get_primary_point())
numpy.testing.assert_array_equal(rad_dist, cal.get_radial_distortion())
numpy.testing.assert_array_equal(decent, cal.get_decentering())
numpy.testing.assert_array_equal(affine, cal.get_affine())
numpy.testing.assert_array_equal(glass, cal.get_glass_vec())

def test_Calibration_instantiation(self):
"""Filling a calibration object by reading ori files"""
self.output_ori_file_name = self.output_directory + b"output_ori"
self.output_add_file_name = self.output_directory + b"output_add"

# Using a round-trip test.
self.cal.from_file(self.input_ori_file_name, self.input_add_file_name)
self.cal.write(self.output_ori_file_name, self.output_add_file_name)

self.assertTrue(filecmp.cmp(self.input_ori_file_name, self.output_ori_file_name, 0))
self.assertTrue(filecmp.cmp(self.input_add_file_name, self.output_add_file_name, 0))

def test_set_pos(self):
"""Set exterior position, only for admissible values"""
# test set_pos() by passing a numpy array of 3 elements
new_np = numpy.array([111.1111, 222.2222, 333.3333])
self.cal.set_pos(new_np)

# test getting position and assert that position is equal to set position
numpy.testing.assert_array_equal(new_np, self.cal.get_pos())

# assert set_pos() raises ValueError exception when given more or less than 3 elements
self.assertRaises(ValueError, self.cal.set_pos, numpy.array([1, 2, 3, 4]))
self.assertRaises(ValueError, self.cal.set_pos, numpy.array([1, 2]))

def test_set_angles(self):
"""set angles correctly"""
dmatrix_before = self.cal.get_rotation_matrix() # dmatrix before setting angles
angles_np = numpy.array([0.1111, 0.2222, 0.3333])
self.cal.set_angles(angles_np)

dmatrix_after = self.cal.get_rotation_matrix() # dmatrix after setting angles
numpy.testing.assert_array_equal(self.cal.get_angles(), angles_np)

# assert dmatrix was recalculated (before vs after)
self.assertFalse(numpy.array_equal(dmatrix_before, dmatrix_after))

self.assertRaises(ValueError, self.cal.set_angles, numpy.array([1, 2, 3, 4]))
self.assertRaises(ValueError, self.cal.set_angles, numpy.array([1, 2]))

def tearDown(self):
# remove the testing output directory and its files
shutil.rmtree(self.output_directory)

def test_set_primary(self):
"""Set primary point (interior) position, only for admissible values"""
new_pp = numpy.array([111.1111, 222.2222, 333.3333])
self.cal.set_primary_point(new_pp)

numpy.testing.assert_array_equal(new_pp, self.cal.get_primary_point())
self.assertRaises(ValueError, self.cal.set_primary_point, numpy.ones(4))
self.assertRaises(ValueError, self.cal.set_primary_point, numpy.ones(2))

def test_set_radial(self):
"""Set radial distortion, only for admissible values"""
new_rd = numpy.array([111.1111, 222.2222, 333.3333])
self.cal.set_radial_distortion(new_rd)

numpy.testing.assert_array_equal(new_rd,
self.cal.get_radial_distortion())
self.assertRaises(ValueError, self.cal.set_radial_distortion,
numpy.ones(4))
self.assertRaises(ValueError, self.cal.set_radial_distortion,
numpy.ones(2))

def test_set_decentering(self):
"""Set radial distortion, only for admissible values"""
new_de = numpy.array([111.1111, 222.2222])
self.cal.set_decentering(new_de)

numpy.testing.assert_array_equal(new_de, self.cal.get_decentering())
self.assertRaises(ValueError, self.cal.set_decentering, numpy.ones(3))
self.assertRaises(ValueError, self.cal.set_decentering, numpy.ones(1))

def test_set_glass(self):
"""Set glass vector, only for admissible values"""
new_gv = numpy.array([1., 2., 3.])
self.cal.set_glass_vec(new_gv)

numpy.testing.assert_array_equal(new_gv, self.cal.get_glass_vec())
self.assertRaises(ValueError, self.cal.set_glass_vec, numpy.ones(2))
self.assertRaises(ValueError, self.cal.set_glass_vec, numpy.ones(1))

if __name__ == "__main__":
unittest.main()
8 changes: 8 additions & 0 deletions pyoptv/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include README.md
include LICENSE
include pyproject.toml
recursive-include pyoptv *.py
recursive-include tests *.py
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
recursive-exclude * .pytest_cache
49 changes: 49 additions & 0 deletions pyoptv/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.PHONY: help install install-dev test test-cov clean build upload-test upload format lint

help:
@echo "Available commands:"
@echo " install Install package"
@echo " install-dev Install package in development mode with dev dependencies"
@echo " test Run tests"
@echo " test-cov Run tests with coverage"
@echo " clean Clean build artifacts"
@echo " build Build package"
@echo " format Format code with black and isort"
@echo " lint Run linting with flake8"

install:
pip install .

install-dev:
pip install -e ".[dev]"

test:
pytest

test-cov:
pytest --cov=pyoptv --cov-report=html --cov-report=term

clean:
rm -rf build/
rm -rf dist/
rm -rf *.egg-info/
rm -rf .pytest_cache/
rm -rf htmlcov/
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete

build: clean
python -m build

upload-test: build
python -m twine upload --repository testpypi dist/*

upload: build
python -m twine upload dist/*

format:
black pyoptv tests
isort pyoptv tests

lint:
flake8 pyoptv tests
77 changes: 77 additions & 0 deletions pyoptv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# PyOptv - Pure Python OpenPTV

PyOptv is a pure Python implementation of OpenPTV (Open Source Particle Tracking Velocimetry), providing tools for 3D particle tracking velocimetry analysis.

## Features

- Camera calibration and orientation
- Image processing and particle detection
- 3D correspondence and tracking
- Ray tracing and stereoscopic reconstruction
- Pure Python implementation (no compiled dependencies)
- Optional Numba acceleration for performance-critical operations

## Installation

### From source

```bash
cd pyoptv
pip install -e .
```

### Development installation

```bash
cd pyoptv
pip install -e ".[dev]"
```

This will install the package in development mode with all development dependencies including testing tools.

## Usage

```python
import pyoptv
from pyoptv.calibration import Calibration
from pyoptv.tracking_frame_buf import FrameBuf

# Create a calibration object
cal = Calibration()

# Set camera parameters
cal.set_pos([100.0, 200.0, 300.0])
cal.set_angles([0.1, 0.2, 0.3])
```

## Testing

Run the test suite:

```bash
pytest
```

Run tests with coverage:

```bash
pytest --cov=pyoptv
```

## Dependencies

- NumPy >= 1.20.0
- SciPy >= 1.7.0
- Matplotlib >= 3.3.0

### Optional dependencies

- Numba >= 0.56.0 (for performance acceleration)

## License

This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0).

## Contributing

Please see the main OpenPTV repository for contribution guidelines.
Loading