diff --git a/.gitignore b/.gitignore index ec4828aa..5a89bdc4 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ py_bind/wheelhouse/* build_artifacts # conda smithy ci-skeleton end *.whl +corres.0 +linkage.0 +prio.0 +target_targets diff --git a/py_bind/test/conftest.py b/py_bind/test/conftest.py index 764bc3cd..da22f3a1 100644 --- a/py_bind/test/conftest.py +++ b/py_bind/test/conftest.py @@ -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) diff --git a/py_bind/test/test_calibration_binding.py b/py_bind/test/test_calibration_binding.py index 427ab1de..98392c05 100644 --- a/py_bind/test/test_calibration_binding.py +++ b/py_bind/test/test_calibration_binding.py @@ -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 diff --git a/py_bind/test/test_pyoptv_calibration_binding.py b/py_bind/test/test_pyoptv_calibration_binding.py new file mode 100644 index 00000000..af165001 --- /dev/null +++ b/py_bind/test/test_pyoptv_calibration_binding.py @@ -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() diff --git a/pyoptv/MANIFEST.in b/pyoptv/MANIFEST.in new file mode 100644 index 00000000..8e392fcc --- /dev/null +++ b/pyoptv/MANIFEST.in @@ -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 diff --git a/pyoptv/Makefile b/pyoptv/Makefile new file mode 100644 index 00000000..b5f3036f --- /dev/null +++ b/pyoptv/Makefile @@ -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 diff --git a/pyoptv/README.md b/pyoptv/README.md new file mode 100644 index 00000000..5ee312c3 --- /dev/null +++ b/pyoptv/README.md @@ -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. diff --git a/pyoptv/pyproject.toml b/pyoptv/pyproject.toml new file mode 100644 index 00000000..2d99a9f7 --- /dev/null +++ b/pyoptv/pyproject.toml @@ -0,0 +1,148 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyoptv" +version = "0.1.0" +description = "Pure Python implementation of OpenPTV - Open Source Particle Tracking Velocimetry" +authors = [ + {name = "OpenPTV Contributors", email = "openptv@googlegroups.com"} +] +readme = "README.md" +license = {text = "LGPL-3.0"} +requires-python = ">=3.8" +keywords = ["particle tracking", "velocimetry", "computer vision", "fluid mechanics", "PIV", "PTV"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Image Processing", + "Topic :: Scientific/Engineering :: Physics", +] + +dependencies = [ + "numpy>=1.19.0", + "scipy>=1.6.0", + "matplotlib>=3.3.0", +] + +[project.optional-dependencies] +# Optional dependencies for performance optimization +numba = ["numba>=0.50.0"] +# Development dependencies +dev = [ + "pytest>=6.0", + "pytest-cov>=2.10", + "black>=21.0", + "flake8>=3.8", + "mypy>=0.800", +] +# All optional dependencies +all = [ + "numba>=0.50.0", +] + +[project.urls] +Homepage = "https://github.com/OpenPTV/openptv" +Documentation = "https://openptv.readthedocs.io" +Repository = "https://github.com/OpenPTV/openptv.git" +"Bug Tracker" = "https://github.com/OpenPTV/openptv/issues" + +[tool.setuptools] +package-dir = {"" = "src"} +packages = ["pyoptv"] + +[tool.setuptools.package-data] +pyoptv = ["tests/testing_fodder/**/*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-v", + "--tb=short", + "--strict-markers", +] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks tests as integration tests", +] + +[tool.coverage.run] +source = ["pyoptv"] +omit = [ + "*/tests/*", + "*/test_*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] + +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["pyoptv"] + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +disallow_incomplete_defs = false +check_untyped_defs = true +disallow_untyped_decorators = false +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +[[tool.mypy.overrides]] +module = [ + "scipy.*", + "matplotlib.*", + "numba.*", +] +ignore_missing_imports = true \ No newline at end of file diff --git a/pyoptv/setup.py b/pyoptv/setup.py new file mode 100644 index 00000000..e7fe0a9f --- /dev/null +++ b/pyoptv/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +""" +Setup script for PyOptv - Pure Python OpenPTV implementation. + +This setup.py is provided for backward compatibility. +The package configuration is primarily defined in pyproject.toml. +""" + +from setuptools import setup + +if __name__ == "__main__": + setup() diff --git a/pyoptv/src/pyoptv.egg-info/PKG-INFO b/pyoptv/src/pyoptv.egg-info/PKG-INFO new file mode 100644 index 00000000..0a350312 --- /dev/null +++ b/pyoptv/src/pyoptv.egg-info/PKG-INFO @@ -0,0 +1,117 @@ +Metadata-Version: 2.4 +Name: pyoptv +Version: 0.1.0 +Summary: Pure Python implementation of OpenPTV - Open Source Particle Tracking Velocimetry +Author-email: OpenPTV Contributors +License: LGPL-3.0 +Project-URL: Homepage, https://github.com/OpenPTV/openptv +Project-URL: Documentation, https://openptv.readthedocs.io +Project-URL: Repository, https://github.com/OpenPTV/openptv.git +Project-URL: Bug Tracker, https://github.com/OpenPTV/openptv/issues +Keywords: particle tracking,velocimetry,computer vision,fluid mechanics,PIV,PTV +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Scientific/Engineering +Classifier: Topic :: Scientific/Engineering :: Image Processing +Classifier: Topic :: Scientific/Engineering :: Physics +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Requires-Dist: numpy>=1.19.0 +Requires-Dist: scipy>=1.6.0 +Requires-Dist: matplotlib>=3.3.0 +Provides-Extra: numba +Requires-Dist: numba>=0.50.0; extra == "numba" +Provides-Extra: dev +Requires-Dist: pytest>=6.0; extra == "dev" +Requires-Dist: pytest-cov>=2.10; extra == "dev" +Requires-Dist: black>=21.0; extra == "dev" +Requires-Dist: flake8>=3.8; extra == "dev" +Requires-Dist: mypy>=0.800; extra == "dev" +Provides-Extra: all +Requires-Dist: numba>=0.50.0; extra == "all" + +# 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. diff --git a/pyoptv/src/pyoptv.egg-info/SOURCES.txt b/pyoptv/src/pyoptv.egg-info/SOURCES.txt new file mode 100644 index 00000000..8a2ebb36 --- /dev/null +++ b/pyoptv/src/pyoptv.egg-info/SOURCES.txt @@ -0,0 +1,47 @@ +MANIFEST.in +README.md +pyproject.toml +setup.py +src/pyoptv/__init__.py +src/pyoptv/calibration.py +src/pyoptv/correspondences.py +src/pyoptv/epi.py +src/pyoptv/glass.py +src/pyoptv/image_processing.py +src/pyoptv/imgcoord.py +src/pyoptv/lsqadj.py +src/pyoptv/multimed.py +src/pyoptv/orientation.py +src/pyoptv/parameters.py +src/pyoptv/ray_tracing.py +src/pyoptv/segmentation.py +src/pyoptv/sortgrid.py +src/pyoptv/track.py +src/pyoptv/tracking_frame_buf.py +src/pyoptv/tracking_run.py +src/pyoptv/trafo.py +src/pyoptv/vec_utils.py +src/pyoptv.egg-info/PKG-INFO +src/pyoptv.egg-info/SOURCES.txt +src/pyoptv.egg-info/dependency_links.txt +src/pyoptv.egg-info/requires.txt +src/pyoptv.egg-info/top_level.txt +tests/conftest.py +tests/test_calibration.py +tests/test_correspondences.py +tests/test_epi.py +tests/test_glass.py +tests/test_image_processing.py +tests/test_imgcoord.py +tests/test_lsqadj.py +tests/test_multimed.py +tests/test_orientation.py +tests/test_parameters.py +tests/test_ray_tracing.py +tests/test_segmentation.py +tests/test_sortgrid.py +tests/test_track.py +tests/test_tracking_frame_buf.py +tests/test_tracking_run.py +tests/test_trafo.py +tests/test_vec_utils.py \ No newline at end of file diff --git a/pyoptv/src/pyoptv.egg-info/dependency_links.txt b/pyoptv/src/pyoptv.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/pyoptv/src/pyoptv.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pyoptv/src/pyoptv.egg-info/requires.txt b/pyoptv/src/pyoptv.egg-info/requires.txt new file mode 100644 index 00000000..7b378b23 --- /dev/null +++ b/pyoptv/src/pyoptv.egg-info/requires.txt @@ -0,0 +1,16 @@ +numpy>=1.19.0 +scipy>=1.6.0 +matplotlib>=3.3.0 + +[all] +numba>=0.50.0 + +[dev] +pytest>=6.0 +pytest-cov>=2.10 +black>=21.0 +flake8>=3.8 +mypy>=0.800 + +[numba] +numba>=0.50.0 diff --git a/pyoptv/src/pyoptv.egg-info/top_level.txt b/pyoptv/src/pyoptv.egg-info/top_level.txt new file mode 100644 index 00000000..e18001b1 --- /dev/null +++ b/pyoptv/src/pyoptv.egg-info/top_level.txt @@ -0,0 +1 @@ +pyoptv diff --git a/pyoptv/src/pyoptv/__init__.py b/pyoptv/src/pyoptv/__init__.py new file mode 100644 index 00000000..fd421a3f --- /dev/null +++ b/pyoptv/src/pyoptv/__init__.py @@ -0,0 +1,27 @@ +""" +PyOptv - Pure Python OpenPTV + +A pure Python implementation of OpenPTV (Open Source Particle Tracking Velocimetry). +""" + +__version__ = "0.1.0" +__author__ = "OpenPTV Contributors" +__email__ = "openptv@googlegroups.com" + +# Import main classes and functions +from .calibration import Calibration, Exterior, Interior, Glass, ap_52 +from .tracking_frame_buf import FrameBuffer, Target +from .parameters import SequencePar, TrackPar + +# Define what gets imported with "from pyoptv import *" +__all__ = [ + "Calibration", + "Exterior", + "Interior", + "Glass", + "ap_52", + "FrameBuffer", + "Target", + "SequencePar", + "TrackPar", +] diff --git a/pyoptv/src/pyoptv/calibration.py b/pyoptv/src/pyoptv/calibration.py new file mode 100644 index 00000000..e1ff24f2 --- /dev/null +++ b/pyoptv/src/pyoptv/calibration.py @@ -0,0 +1,468 @@ +# type: ignore +import numpy as np +from typing import Optional, Any + +class Calibration: + def __init__( + self, + ext_par: Optional["Exterior"] = None, + int_par: Optional["Interior"] = None, + glass_par: Optional["Glass"] = None, + added_par: Optional["ap_52"] = None, + ) -> None: + """ + Camera calibration class holding all camera parameters. + All arguments are optional arrays or parameter objects. + """ + # Initialize with parameter objects if provided, otherwise create new ones + if ext_par is not None: + self.ext_par = ext_par + else: + self.ext_par = Exterior() + + # Ensure the rotation matrix is initialized + self.ext_par.update_rotation_matrix() + + if int_par is not None: + self.int_par = int_par + else: + self.int_par = Interior() + + if glass_par is not None: + self.glass_par = glass_par + else: + self.glass_par = Glass() + + if added_par is not None: + self.added_par = added_par + else: + self.added_par = ap_52() + + self.mmlut = mmlut() + + + + def set_pos(self, pos: np.ndarray) -> None: + """ + Sets exterior position. + Parameter: pos - numpy array of 3 elements for x, y, z + """ + if len(pos) != 3: + raise ValueError( + "Illegal array argument " + + str(pos) + + " for x, y, z. Expected array/list of 3 numbers" + ) + self.ext_par.x0, self.ext_par.y0, self.ext_par.z0 = pos + + def set_angles(self, angs: np.ndarray) -> None: + """ + Sets angles (omega, phi, kappa) and recalculates Dmatrix accordingly + Parameter: angs - array of 3 elements. + """ + if len(angs) != 3: + raise ValueError( + "Illegal array argument " + + str(angs) + + " for omega, phi, kappa. Expected array/list of 3 numbers" + ) + self.ext_par.omega, self.ext_par.phi, self.ext_par.kappa = angs + self.ext_par.dm = self.rotation_matrix( + self.ext_par.omega, self.ext_par.phi, self.ext_par.kappa + ) + + def set_primary_point(self, prim_point: np.ndarray) -> None: + """ + Set the camera's primary point position (a.k.a. interior orientation). + + Arguments: + prim_point - a 3 element array holding the values of x and y shift + of point from sensor middle and sensor-point distance, in this + order. + """ + if len(prim_point) != 3: + raise ValueError("Expected a 3-element array") + self.int_par.xh, self.int_par.yh, self.int_par.cc = prim_point + + def set_radial_distortion(self, rad_dist: np.ndarray) -> None: + """ + Sets the parameters for the image radial distortion, where the x/y + coordinates are corrected by a polynomial in r = sqrt(x**2 + y**2): + p = k1*r**2 + k2*r**4 + k3*r**6 + + Arguments: + rad_dist - length-3 array, holding k_i. + """ + if len(rad_dist) != 3: + raise ValueError("Expected a 3-element array") + self.added_par.k1, self.added_par.k2, self.added_par.k3 = rad_dist + + def set_decentering(self, decent: np.ndarray) -> None: + """ + Sets the parameters of decentering distortion (a.k.a. p1, p2). + + Arguments: + decent - array, holding p_i + """ + if len(decent) != 2: + raise ValueError("Expected a 2-element array") + self.added_par.p1, self.added_par.p2 = decent + + def set_affine_trans(self, affine: np.ndarray) -> None: + """ + Sets the affine transform parameters (x-scale, shear) applied to the + image. + + Arguments: + affine - array, holding (x-scale, shear) in order. + """ + if len(affine) != 2: + raise ValueError("Expected a 2-element array") + self.added_par.scx, self.added_par.she = affine + + def set_glass_vec(self, glass: np.ndarray) -> None: + """ + Sets the glass vector: a vector from the origin to the glass, directed + normal to the glass. + + Arguments: + glass - a 3-element array, the glass vector. + """ + if len(glass) != 3: + raise ValueError("Expected a 3-element array") + self.glass_par.vec_x, self.glass_par.vec_y, self.glass_par.vec_z = glass + + @staticmethod + def rotation_matrix(omega: float, phi: float, kappa: float) -> np.ndarray: + # Use only the numpy implementation, as _rotation_matrix_numba does not exist + + cp = np.cos(phi) + sp = np.sin(phi) + co = np.cos(omega) + so = np.sin(omega) + ck = np.cos(kappa) + sk = np.sin(kappa) + + dm = np.zeros((3, 3)) + dm[0, 0] = cp * ck + dm[0, 1] = -cp * sk + dm[0, 2] = sp + dm[1, 0] = co * sk + so * sp * ck + dm[1, 1] = co * ck - so * sp * sk + dm[1, 2] = -so * cp + dm[2, 0] = so * sk - co * sp * ck + dm[2, 1] = so * ck + co * sp * sk + dm[2, 2] = co * cp + + return dm + + def write_ori(self, filename: str, add_file: Optional[str] = None) -> None: + with open(filename, "w") as f: + f.write( + f"{self.ext_par.x0:.8f} {self.ext_par.y0:.8f} {self.ext_par.z0:.8f}\n" + ) + f.write( + f"{self.ext_par.omega:.8f} {self.ext_par.phi:.8f} {self.ext_par.kappa:.8f}\n" + ) + for row in self.ext_par.dm: + f.write(f"{row[0]:.7f} {row[1]:.7f} {row[2]:.7f}\n") + f.write(f"{self.int_par.xh:.4f} {self.int_par.yh:.4f}\n") + f.write(f"{self.int_par.cc:.4f}\n") + f.write( + f"{self.glass_par.vec_x:.15f} {self.glass_par.vec_y:.15f} {self.glass_par.vec_z:.15f}\n" + ) + + if add_file: + with open(add_file, "w") as f: + f.write( + f"{self.added_par.k1:.8f} {self.added_par.k2:.8f} {self.added_par.k3:.8f} " + ) + f.write(f"{self.added_par.p1:.8f} {self.added_par.p2:.8f} ") + f.write(f"{self.added_par.scx:.8f} {self.added_par.she:.8f}") + + + @staticmethod + def compare_calib(c1: "Calibration", c2: "Calibration") -> bool: + return ( + Calibration.compare_exterior(c1.ext_par, c2.ext_par) + and Calibration.compare_interior(c1.int_par, c2.int_par) + and Calibration.compare_glass(c1.glass_par, c2.glass_par) + and Calibration.compare_addpar(c1.added_par, c2.added_par) + ) + + @staticmethod + def compare_exterior(e1: "Exterior", e2: "Exterior") -> bool: + return ( + np.allclose(e1.dm, e2.dm) + and e1.x0 == e2.x0 + and e1.y0 == e2.y0 + and e1.z0 == e2.z0 + and e1.omega == e2.omega + and e1.phi == e2.phi + and e1.kappa == e2.kappa + ) + + @staticmethod + def compare_interior(i1: "Interior", i2: "Interior") -> bool: + return i1.xh == i2.xh and i1.yh == i2.yh and i1.cc == i2.cc + + @staticmethod + def compare_glass(g1: "Glass", g2: "Glass") -> bool: + return g1.vec_x == g2.vec_x and g1.vec_y == g2.vec_y and g1.vec_z == g2.vec_z + + @staticmethod + def compare_addpar(a1: "ap_52", a2: "ap_52") -> bool: + return ( + a1.k1 == a2.k1 + and a1.k2 == a2.k2 + and a1.k3 == a2.k3 + and a1.p1 == a2.p1 + and a1.p2 == a2.p2 + and a1.scx == a2.scx + and a1.she == a2.she + ) + + @staticmethod + def read_calibration( + ori_file: str, + add_file: Optional[str] = None, + fallback_file: Optional[str] = None, + ) -> "Calibration": + return Calibration.read_ori(ori_file, add_file, fallback_file) + + def write_calibration(self, filename: str, add_file: Optional[str] = None) -> None: + self.write_ori(filename, add_file) + + def get_pos(self) -> np.ndarray: + """ + Returns numpy array of 3 elements representing exterior's x, y, z + """ + return np.array([self.ext_par.x0, self.ext_par.y0, self.ext_par.z0]) + + def get_angles(self) -> np.ndarray: + """ + Returns a numpy array of 3 elements representing omega, phi, kappa + """ + return np.array([self.ext_par.omega, self.ext_par.phi, self.ext_par.kappa]) + + def get_rotation_matrix(self) -> np.ndarray: + """ + Returns a 3x3 numpy array that represents Exterior's rotation matrix. + """ + return self.ext_par.dm.copy() + + def get_primary_point(self) -> np.ndarray: + """ + Returns the primary point position (a.k.a. interior orientation) as a 3 + element array holding the values of x and y shift of point from sensor + middle and sensor-point distance, in this order. + """ + return np.array([self.int_par.xh, self.int_par.yh, self.int_par.cc]) + + def get_radial_distortion(self) -> np.ndarray: + """ + Returns the radial distortion polynomial coefficients as a 3 element + array, from lowest power to highest. + """ + return np.array([self.added_par.k1, self.added_par.k2, self.added_par.k3]) + + def get_decentering(self) -> np.ndarray: + """ + Returns the decentering parameters as a 2 element array, (p_1, p_2). + """ + return np.array([self.added_par.p1, self.added_par.p2]) + + def get_affine(self) -> np.ndarray: + """ + Returns the affine transform parameters as a 2 element array, (scx, she). + """ + return np.array([self.added_par.scx, self.added_par.she]) + + def get_glass_vec(self) -> np.ndarray: + """ + Returns the glass vector, a 3-element array. + """ + return np.array( + [self.glass_par.vec_x, self.glass_par.vec_y, self.glass_par.vec_z] + ) + + def from_file( + self, + ori_file: str, + add_file: Optional[str] = None, + fallback_file: Optional[str] = None, + ) -> None: + """ + Populate calibration fields from .ori and .addpar files. + + Arguments: + ori_file - path to file containing exterior, interior and glass + parameters. + add_file - optional path to file containing distortion parameters. + fallback_file - optional path to file used in case ``add_file`` fails + to open. + """ + # Convert bytes to string if needed + if isinstance(ori_file, bytes): + ori_file = ori_file.decode("utf-8") + if isinstance(add_file, bytes): + add_file = add_file.decode("utf-8") + if isinstance(fallback_file, bytes): + fallback_file = fallback_file.decode("utf-8") + + cal = self.read_ori(ori_file, add_file, fallback_file) + self.ext_par = cal.ext_par + self.int_par = cal.int_par + self.glass_par = cal.glass_par + self.added_par = cal.added_par + + def write(self, filename: str, add_file: str) -> None: + """ + Write the calibration data to disk. Uses two output file, one for the + linear calibration part, and one for distortion parameters. + + Arguments: + filename - path to file containing exterior, interior and glass + parameters. + add_file - optional path to file containing distortion parameters. + """ + # Convert bytes to string if needed + if isinstance(filename, bytes): + filename = filename.decode("utf-8") + if isinstance(add_file, bytes): + add_file = add_file.decode("utf-8") + + self.write_ori(filename, add_file) + +def read_ori( + filename: str, + add_file: Optional[str] = None, + add_fallback: Optional[str] = None, +) -> Calibration: + import os + + if not os.path.exists(filename): + raise FileNotFoundError(f"File {os.path.abspath(filename)} does not exist.") + + with open(filename, "r") as f: + lines = f.readlines() + + # Remove empty lines and strip whitespace + lines = [line.strip() for line in lines if line.strip()] + + cal = Calibration() + + cal.ext_par.x0, cal.ext_par.y0, cal.ext_par.z0 = map(float, lines[0].split()) + cal.ext_par.omega, cal.ext_par.phi, cal.ext_par.kappa = map( + float, lines[1].split() + ) + cal.ext_par.dm = np.array( + [list(map(float, line.split())) for line in lines[2:5]] + ) + cal.int_par.xh, cal.int_par.yh = map(float, lines[5].split()) + cal.int_par.cc = float(lines[6]) + cal.glass_par.vec_x, cal.glass_par.vec_y, cal.glass_par.vec_z = map( + float, lines[7].split() + ) + + if add_file or add_fallback: + try: + with open(add_file, "r") as f: + add_lines = f.readlines() + except (FileNotFoundError, TypeError): + if add_fallback: + with open(add_fallback, "r") as f: + add_lines = f.readlines() + else: + # If no add file, use defaults + return cal + + ( + cal.added_par.k1, + cal.added_par.k2, + cal.added_par.k3, + cal.added_par.p1, + cal.added_par.p2, + cal.added_par.scx, + cal.added_par.she, + ) = map(float, add_lines[0].split()) + + return cal + + +class Exterior: + def __init__( + self, + x0: float = 0.0, + y0: float = 0.0, + z0: float = 0.0, + omega: float = 0.0, + phi: float = 0.0, + kappa: float = 0.0, + dm: Optional[np.ndarray] = None, + ) -> None: + self.x0: float = x0 + self.y0: float = y0 + self.z0: float = z0 + self.omega: float = omega + self.phi: float = phi + self.kappa: float = kappa + if dm is not None: + self.dm: np.ndarray = dm + else: + self.dm: np.ndarray = np.eye(3) + + def update_rotation_matrix(self) -> None: + """ + Updates the rotation matrix based on the current omega, phi, and kappa. + """ + self.dm = Calibration.rotation_matrix(self.omega, self.phi, self.kappa) + + +class Interior: + def __init__(self, xh: float = 0.0, yh: float = 0.0, cc: float = 0.0) -> None: + self.xh: float = xh + self.yh: float = yh + self.cc: float = cc + + +class Glass: + def __init__( + self, + vec_x: float = 0.0, + vec_y: float = 0.0, + vec_z: float = 1.0, + ) -> None: + self.vec_x: float = vec_x + self.vec_y: float = vec_y + self.vec_z: float = vec_z + + +class ap_52: + def __init__( + self, + k1: float = 0.0, + k2: float = 0.0, + k3: float = 0.0, + p1: float = 0.0, + p2: float = 0.0, + scx: float = 1.0, + she: float = 0.0, + ) -> None: + self.k1: float = k1 + self.k2: float = k2 + self.k3: float = k3 + self.p1: float = p1 + self.p2: float = p2 + self.scx: float = scx + self.she: float = she + + +class mmlut: + def __init__(self) -> None: + self.origin: np.ndarray = np.zeros(3) + self.nr: int = 0 + self.nz: int = 0 + self.rw: int = 0 + self.data: Any = None + diff --git a/pyoptv/src/pyoptv/constants.py b/pyoptv/src/pyoptv/constants.py new file mode 100644 index 00000000..1ebbc6c3 --- /dev/null +++ b/pyoptv/src/pyoptv/constants.py @@ -0,0 +1,17 @@ +STR_MAX_LEN: int = 255 +POSI: int = 4 +PREV_NONE: int = -1 +NEXT_NONE: int = -2 +PRIO_DEFAULT: int = 4 +CORRES_NONE: int = -999 +MAX_TARGETS: int = 20000 + +TR_UNUSED = -1 +PT_UNUSED = -999 +COORD_UNUSED = -1e10 # Added to represent unused coordinate values +TR_BUFSPACE = 4 +TR_MAX_CAMS = 4 +MAX_CANDS = 4 # max candidates, nearest neighbours +ADD_PART = 3 # search region 3 pix around a particle + +MAXCAND = 100 # max candidates in epipolar search \ No newline at end of file diff --git a/pyoptv/src/pyoptv/correspondences.py b/pyoptv/src/pyoptv/correspondences.py new file mode 100644 index 00000000..577ac1ee --- /dev/null +++ b/pyoptv/src/pyoptv/correspondences.py @@ -0,0 +1,389 @@ +import numpy as np +from typing import List, Any +from pyoptv.calibration import Calibration +from pyoptv.parameters import ControlPar, VolumePar +from pyoptv.tracking_frame_buf import Frame, Target +from scipy.optimize import minimize +import matplotlib.pyplot as plt +from .epi import Coord2D, epi_mm, find_candidate +from .constants import MAXCAND, PT_UNUSED + +nmax = 202400 + +class NTupel: + def __init__(self, indices: List[int] = None, corr: float = 0.0): + if indices is None: + self.p: List[int] = [] + else: + self.p: List[int] = indices + self.corr: float = corr + +class Correspond: + def __init__(self, p1: int = PT_UNUSED, n: int = 0, dist: np.ndarray = None, corr: np.ndarray = None, p2: np.ndarray = None): + self.p1: int = p1 # Add p1 attribute for compatibility with tests + self.n: int = n + self.p2: np.ndarray = np.zeros(MAXCAND, dtype=np.int32) if p2 is None else p2.astype(np.int32) + self.dist: np.ndarray = np.zeros(MAXCAND, dtype=np.float64) if dist is None else dist.astype(np.float64) + self.corr: np.ndarray = np.zeros(MAXCAND, dtype=np.float64) if corr is None else corr.astype(np.float64) + + def __repr__(self): + return f"Correspond(p1={self.p1}, n={self.n}, len(p2)={len(self.p2)})" + +def quicksort_con(con: List[Correspond]) -> None: + if len(con) > 0: + qs_con(con, 0, len(con) - 1) + +def qs_con(con, left, right): + i = left + j = right + xm = con[(left + right) // 2].corr + + while i <= j: + while con[i].corr > xm and i < right: + i += 1 + while xm > con[j].corr and j > left: + j -= 1 + + if i <= j: + con[i], con[j] = con[j], con[i] + i += 1 + j -= 1 + + if left < j: + qs_con(con, left, j) + if i < right: + qs_con(con, i, right) + +def quicksort_target_y(pix: List[Target]) -> None: + qs_target_y(pix, 0, len(pix) - 1) + +def qs_target_y(pix, left, right): + i = left + j = right + ym = pix[(left + right) // 2].y + + while i <= j: + while pix[i].y < ym and i < right: + i += 1 + while ym < pix[j].y and j > left: + j -= 1 + + if i <= j: + pix[i], pix[j] = pix[j], pix[i] + i += 1 + j -= 1 + + if left < j: + qs_target_y(pix, left, j) + if i < right: + qs_target_y(pix, i, right) + +def quicksort_coord2d_x(crd: List[Coord2D]) -> None: + qs_coord2d_x(crd, 0, len(crd) - 1) + +def qs_coord2d_x(crd, left, right): + i = left + j = right + xm = crd[(left + right) // 2].x + + while i <= j: + while crd[i].x < xm and i < right: + i += 1 + while xm < crd[j].x and j > left: + j -= 1 + + if i <= j: + crd[i], crd[j] = crd[j], crd[i] + i += 1 + j -= 1 + + if left < j: + qs_coord2d_x(crd, left, j) + if i < right: + qs_coord2d_x(crd, i, right) + +def safely_allocate_target_usage_marks(num_cams: int) -> np.ndarray: + try: + tusage = np.zeros((num_cams, nmax), dtype=np.int32) + return tusage + except MemoryError: + return None + +# def deallocate_target_usage_marks(tusage: np.ndarray) -> None: +# del tusage + +def safely_allocate_adjacency_lists(num_cams: int, target_counts: List[int]) -> List[List[List[Correspond]]] | None: + try: + lists = [[[] for _ in range(num_cams)] for _ in range(num_cams)] + for c1 in range(num_cams - 1): + for c2 in range(c1 + 1, num_cams): + lists[c1][c2] = [ + Correspond(n=0, p1=PT_UNUSED) + for _ in range(target_counts[c1]) + ] + return lists + except MemoryError: + return None + +# def deallocate_adjacency_lists(lists: List[List[List[Correspond]]]) -> None: +# del lists + +def four_camera_matching( + lists: List[List[List[Any]]], + num_targets: int, + corrmin: float, + scratch: List[NTupel], + scratch_size: int +) -> int: + matched = 0 + for i in range(num_targets): + p1 = lists[0][1][i].p1 + for j in range(lists[0][1][i].n): + for k in range(lists[0][2][i].n): + for l in range(lists[0][3][i].n): + p2 = lists[0][1][i].p2[j] + p3 = lists[0][2][i].p2[k] + p4 = lists[0][3][i].p2[l] + for m in range(lists[1][2][p2].n): + p31 = lists[1][2][p2].p2[m] + if p3 != p31: + continue + for n in range(lists[1][3][p2].n): + p41 = lists[1][3][p2].p2[n] + if p4 != p41: + continue + for o in range(lists[2][3][p3].n): + p42 = lists[2][3][p3].p2[o] + if p4 != p42: + continue + + corr = (lists[0][1][i].corr[j] + + lists[0][2][i].corr[k] + + lists[0][3][i].corr[l] + + lists[1][2][p2].corr[m] + + lists[1][3][p2].corr[n] + + lists[2][3][p3].corr[o]) / ( + lists[0][1][i].dist[j] + + lists[0][2][i].dist[k] + + lists[0][3][i].dist[l] + + lists[1][2][p2].dist[m] + + lists[1][3][p2].dist[n] + + lists[2][3][p3].dist[o]) + + if corr <= corrmin: + continue + + scratch[matched] = NTupel([p1, p2, p3, p4], corr) + matched += 1 + if matched == scratch_size: + print("Overflow in correspondences.") + return matched + return matched + +def three_camera_matching( + lists: List[List[List[Any]]], + num_targets: int, + num_targets_arr: List[int], + corrmin: float, + scratch: List[NTupel], + scratch_size: int, + tusage: np.ndarray +) -> int: + matched = 0 + num_cams = len(lists) + for i1 in range(num_cams - 2): + for i in range(num_targets_arr[i1]): + for i2 in range(i1 + 1, num_cams - 1): + p1 = lists[i1][i2][i].p1 + if p1 > nmax or tusage[i1][p1] > 0: + continue + + for j in range(lists[i1][i2][i].n): + p2 = lists[i1][i2][i].p2[j] + if p2 > nmax or tusage[i2][p2] > 0: + continue + + for i3 in range(i2 + 1, num_cams): + for k in range(lists[i1][i3][i].n): + p3 = lists[i1][i3][i].p2[k] + if p3 > nmax or tusage[i3][p3] > 0: + continue + + for m in range(lists[i2][i3][p2].n): + if p3 != lists[i2][i3][p2].p2[m]: + continue + + corr = (lists[i1][i2][i].corr[j] + + lists[i1][i3][i].corr[k] + + lists[i2][i3][p2].corr[m]) / ( + lists[i1][i2][i].dist[j] + + lists[i1][i3][i].dist[k] + + lists[i2][i3][p2].dist[m]) + + if corr <= corrmin: + continue + + scratch[matched] = NTupel([-2] * num_cams, corr) + scratch[matched].p[i1] = p1 + scratch[matched].p[i2] = p2 + scratch[matched].p[i3] = p3 + matched += 1 + if matched == scratch_size: + print("Overflow in correspondences.") + return matched + return matched + +def consistent_pair_matching( + lists: List[List[List[Any]]], + num_targets: int, + num_targets_arr: List[int], + corrmin: float, + scratch: List[NTupel], + scratch_size: int, + tusage: np.ndarray +) -> int: + matched = 0 + num_cams = len(lists) + for i1 in range(num_cams - 1): + for i2 in range(i1 + 1, num_cams): + for i in range(num_targets_arr[i1]): + p1 = lists[i1][i2][i].p1 + if p1 > nmax or tusage[i1][p1] > 0: + continue + + if lists[i1][i2][i].n != 1: + continue + + p2 = lists[i1][i2][i].p2[0] + if p2 > nmax or tusage[i2][p2] > 0: + continue + + corr = lists[i1][i2][i].corr[0] / lists[i1][i2][i].dist[0] + if corr <= corrmin: + continue + + scratch[matched] = NTupel([-2] * num_cams, corr) + scratch[matched].p[i1] = p1 + scratch[matched].p[i2] = p2 + matched += 1 + if matched == scratch_size: + print("Overflow in correspondences.") + return matched + return matched + +def match_pairs( + lists: List[List[List[Correspond]]], + corrected: List[List[Target]], + frm: Frame, + vpar: VolumePar, + cpar: ControlPar, + calib: List[Calibration] +) -> None: + for i1 in range(cpar.num_cams - 1): + for i2 in range(i1 + 1, cpar.num_cams): + for i in range(frm.num_targets[i1]): + if corrected[i1][i].x == -999: + continue + + xa12, ya12, xb12, yb12 = epi_mm(corrected[i1][i].x, corrected[i1][i].y, + calib[i1], calib[i2], cpar.mm, + vpar) + + lists[i1][i2][i].p1 = i + pt1 = corrected[i1][i].pnr + + count, cand = find_candidate(corrected[i2], frm.targets[i2], + frm.num_targets[i2], xa12, ya12, xb12, yb12, + frm.targets[i1][pt1].n, frm.targets[i1][pt1].nx, + frm.targets[i1][pt1].ny, frm.targets[i1][pt1].sumg, + vpar, cpar, calib[i2]) + + if count > MAXCAND: + count = MAXCAND + + for j in range(count): + lists[i1][i2][i].p2[j] = cand[j].pnr + lists[i1][i2][i].corr[j] = cand[j].corr + lists[i1][i2][i].dist[j] = cand[j].tol + lists[i1][i2][i].n = count + +def take_best_candidates( + src: List[NTupel], + dst: List[NTupel], + num_cams: int, + num: int, + tusage: np.ndarray +) -> int: + quicksort_con(src) + taken = 0 + + for cand in range(num): + has_used_target = False + for cam in range(num_cams): + tnum = src[cand].p[cam] + if tnum > -1 and tusage[cam][tnum] > 0: + has_used_target = True + break + + if has_used_target: + continue + + for cam in range(num_cams): + tnum = src[cand].p[cam] + if tnum > -1: + tusage[cam][tnum] += 1 + dst[taken] = src[cand] + taken += 1 + + return taken + +def correspondences( + frm: Frame, + corrected: List[List[Target]], + vpar: VolumePar, + cpar: ControlPar, + calib: List[Calibration] +) -> tuple[list[NTupel], list[int]]: + con0 = [NTupel([-1] * cpar.num_cams, 0.0) for _ in range(nmax)] + con = [NTupel([-1] * cpar.num_cams, 0.0) for _ in range(nmax)] + tim = safely_allocate_target_usage_marks(cpar.num_cams) + if tim is None: + print("out of memory") + return [], [] + + lists = safely_allocate_adjacency_lists(cpar.num_cams, frm.num_targets) + if lists is None: + print("list is not allocated") + # deallocate_target_usage_marks(tim) + return [], [] + + match_counts = [0] * 4 + match_pairs(lists, corrected, frm, vpar, cpar, calib) + + if cpar.num_cams == 4: + match0 = four_camera_matching(lists, frm.num_targets[0], vpar.corrmin, con0, 4 * nmax) + match_counts[0] = take_best_candidates(con0, con, cpar.num_cams, match0, tim) + match_counts[3] += match_counts[0] + + if (cpar.num_cams == 4 and cpar.allCam_flag == 0) or cpar.num_cams == 3: + match0 = three_camera_matching(lists, cpar.num_cams, frm.num_targets, vpar.corrmin, con0, 4 * nmax, tim) + match_counts[1] = take_best_candidates(con0, con[match_counts[3]:], cpar.num_cams, match0, tim) + match_counts[3] += match_counts[1] + + if cpar.num_cams > 1 and cpar.allCam_flag == 0: + match0 = consistent_pair_matching(lists, cpar.num_cams, frm.num_targets, vpar.corrmin, con0, 4 * nmax, tim) + match_counts[2] = take_best_candidates(con0, con[match_counts[3]:], cpar.num_cams, match0, tim) + match_counts[3] += match_counts[2] + + for i in range(match_counts[3]): + for j in range(cpar.num_cams): + if con[i].p[j] < 0: + continue + p1 = corrected[j][con[i].p[j]].pnr + if p1 > -1 and p1 < 1202590843: + frm.targets[j][p1].tnr = i + + # deallocate_adjacency_lists(lists) + # deallocate_target_usage_marks(tim) + + return con, match_counts diff --git a/pyoptv/src/pyoptv/epi.py b/pyoptv/src/pyoptv/epi.py new file mode 100644 index 00000000..b72cb051 --- /dev/null +++ b/pyoptv/src/pyoptv/epi.py @@ -0,0 +1,188 @@ +# type: ignore +import numpy as np +from typing import List, Tuple, Any +from .trafo import pixel_to_metric, dist_to_flat, metric_to_pixel, correct_brown_affin +from .imgcoord import flat_image_coord +from .ray_tracing import ray_tracing +from .parameters import ControlPar, VolumePar, MMNP +from .calibration import Calibration +from .epi import epi_mm, epi_mm_2D, find_candidate, Coord2D, Candidate +from .tracking_frame_buf import Target +from dataclasses import dataclass +from .multimed import move_along_ray + + +MAXCAND = 100 # Avoid circular import, match value from correspondences.py + + +@dataclass +class Candidate: + pnr: int = -1 + tol: float = np.nan + corr: float = np.nan + +@dataclass +class Coord2D: + pnr: int = -1 + x: float = np.nan + y: float = np.nan + + +def epi_mm( + xl: float, + yl: float, + cal1: Calibration, + cal2: Calibration, + mmp: MMNP, + vpar: VolumePar, +) -> Tuple[float, float, float, float]: + pos, v = ray_tracing(xl, yl, cal1, mmp) + Zmin = vpar.Zmin_lay[0] + (pos[0] - vpar.X_lay[0]) * (vpar.Zmin_lay[1] - vpar.Zmin_lay[0]) / ( + vpar.X_lay[1] - vpar.X_lay[0] + ) + Zmax = vpar.Zmax_lay[0] + (pos[0] - vpar.X_lay[0]) * (vpar.Zmax_lay[1] - vpar.Zmax_lay[0]) / ( + vpar.X_lay[1] - vpar.X_lay[0] + ) + xmin, ymin = flat_image_coord(move_along_ray(Zmin, pos, v), cal2, mmp) + xmax, ymax = flat_image_coord(move_along_ray(Zmax, pos, v), cal2, mmp) + return xmin, ymin, xmax, ymax + + +def epi_mm_2D( + xl: float, yl: float, cal1: Calibration, mmp: MMNP, vpar: VolumePar +) -> np.ndarray: + pos, v = ray_tracing(xl, yl, cal1, mmp) + + Zmin = vpar.Zmin_lay[0] + (pos[0] - vpar.X_lay[0]) * (vpar.Zmin_lay[1] - vpar.Zmin_lay[0]) / ( + vpar.X_lay[1] - vpar.X_lay[0] + ) + Zmax = vpar.Zmax_lay[0] + (pos[0] - vpar.X_lay[0]) * (vpar.Zmax_lay[1] - vpar.Zmax_lay[0]) / ( + vpar.X_lay[1] - vpar.X_lay[0] + ) + return move_along_ray(0.5 * (Zmin + Zmax), pos, v) + + +def find_candidate( + crd: List[Coord2D], + pix: List[Target], + num: int, + xa: float, + ya: float, + xb: float, + yb: float, + n: int, + nx: int, + ny: int, + sumg: float, + vpar: VolumePar, + cpar: ControlPar, + cal: Calibration, +) -> Tuple[int, List[Candidate]]: + tol_band_width = vpar.eps0 + xmin = -cpar.pix_x * cpar.imx / 2 - cal.int_par.xh + ymin = -cpar.pix_y * cpar.imy / 2 - cal.int_par.yh + xmax = cpar.pix_x * cpar.imx / 2 - cal.int_par.xh + ymax = cpar.pix_y * cpar.imy / 2 - cal.int_par.yh + xmin, ymin = correct_brown_affin(xmin, ymin, cal.added_par) + xmax, ymax = correct_brown_affin(xmax, ymax, cal.added_par) + + # Debug output for epipolar line and search window + print(f"find_candidate: xa={xa:.4f}, ya={ya:.4f}, xb={xb:.4f}, yb={yb:.4f}") + print(f"find_candidate: xmin={xmin:.4f}, xmax={xmax:.4f}, ymin={ymin:.4f}, ymax={ymax:.4f}") + print(f"find_candidate: tol_band_width={tol_band_width}") + print(f"find_candidate: num candidates in crd={num}") + for idx in range(min(num, 5)): + print(f" crd[{idx}]: pnr={crd[idx].pnr}, x={crd[idx].x:.4f}, y={crd[idx].y:.4f}") + + if xa == xb: + xb += 1e-10 + + m = (yb - ya) / (xb - xa) + b = ya - m * xa + + if xa > xb: + xa, xb = xb, xa + if ya > yb: + ya, yb = yb, ya + + if xb <= xmin or xa >= xmax or yb <= ymin or ya >= ymax: + # out-of-bounds epipolar strip: return -1 for semantic match to C implementation + return -1, [] + + j0 = num // 2 + dj = num // 4 + while dj > 1: + if crd[j0].x < (xa - tol_band_width): + j0 += dj + else: + j0 -= dj + dj //= 2 + + j0 -= 12 + if j0 < 0: + j0 = 0 + + candidates: List[Candidate] = [] + for j in range(j0, num): + if crd[j].x > xb + tol_band_width: + print(f"find_candidate: reached x>{xb + tol_band_width:.4f}, breaking candidate loop") + return len(candidates), candidates + + if crd[j].y <= ya - tol_band_width or crd[j].y >= yb + tol_band_width: + continue + if crd[j].x <= xa - tol_band_width or crd[j].x >= xb + tol_band_width: + continue + + d = abs((crd[j].y - m * crd[j].x - b) / np.sqrt(m * m + 1)) + if d >= tol_band_width: + continue + + p2 = crd[j].pnr + if p2 >= num: + print("find_candidate: pnr out of range:", p2) + return 0, [] + + qn = quality_ratio(n, pix[p2].n) + qnx = quality_ratio(nx, pix[p2].nx) + qny = quality_ratio(ny, pix[p2].ny) + qsumg = quality_ratio(sumg, pix[p2].sumg) + + if qn < vpar.cn or qnx < vpar.cnx or qny < vpar.cny or qsumg <= vpar.csumg: + continue + if len(candidates) >= MAXCAND: + print("find_candidate: More candidates than (maxcand):", len(candidates)) + return len(candidates), candidates + + corr = (4 * qsumg + 2 * qn + qnx + qny) * (sumg + pix[p2].sumg) + print(f"find_candidate: candidate j={j}, pnr={p2}, d={d:.6f}, corr={corr:.2f}") + candidates.append(Candidate(j, d, corr)) + + print(f"find_candidate: returning {len(candidates)} candidates") + return len(candidates), candidates + + +def quality_ratio(a: float, b: float) -> float: + return min(a, b) / max(a, b) + + +def epipolar_curve( + image_point: Tuple[float, float], + origin_cam: Calibration, + project_cam: Calibration, + num_points: int, + cparam: ControlPar, + vpar: VolumePar, +) -> np.ndarray: + img_pt = pixel_to_metric(image_point[0], image_point[1], cparam) + img_pt = dist_to_flat(img_pt[0], img_pt[1], origin_cam, 1e-5) + # Perform ray tracing to get origin and direction vectors + X, a = ray_tracing(img_pt[0], img_pt[1], origin_cam, cparam.mm) + line_points = np.empty((num_points, 2)) + Zs = np.linspace(vpar.Zmin_lay[0], vpar.Zmax_lay[0], num_points) + for pt_ix, Z in enumerate(Zs): + pos = X + (Z - X[2]) / a[2] * a # move_along_ray in C is just X + t*a + x, y = flat_image_coord(pos, project_cam, cparam.mm) + x, y = metric_to_pixel(x, y, cparam) + line_points[pt_ix, 0] = x + line_points[pt_ix, 1] = y + return line_points diff --git a/pyoptv/src/pyoptv/glass.py b/pyoptv/src/pyoptv/glass.py new file mode 100644 index 00000000..e69de29b diff --git a/pyoptv/src/pyoptv/image_processing.py b/pyoptv/src/pyoptv/image_processing.py new file mode 100644 index 00000000..e3d79565 --- /dev/null +++ b/pyoptv/src/pyoptv/image_processing.py @@ -0,0 +1,231 @@ +import numpy as np +from typing import Tuple +from scipy.ndimage import convolve +import matplotlib.pyplot as plt + +def filter_3(img: np.ndarray, filt: np.ndarray) -> np.ndarray: + """ + Perform a 3x3 filtering over an image. The first and last lines are not processed at all, the rest uses wrap-around on the image edges. Minimal brightness output in processed pixels is 8. + + Arguments: + img - original image as a 2D numpy array. + filt - the 3x3 matrix to apply to the image. + + Returns: + Filtered image as a 2D numpy array. + """ + sum_filt = np.sum(filt) + if sum_filt == 0: + return img + + img_lp = convolve(img, filt, mode='wrap') + img_lp = np.clip(img_lp / sum_filt, 8, 255).astype(np.uint8) + return img_lp + +def lowpass_3(img: np.ndarray) -> np.ndarray: + """ + Perform a 3x3 lowpass filtering over an image. + + Arguments: + img - original image as a 2D numpy array. + + Returns: + Filtered image as a 2D numpy array. + """ + filt = np.ones((3, 3)) + return filter_3(img, filt) +def fast_box_blur(filt_span: int, src: np.ndarray) -> np.ndarray: + """ + Perform a box blur of an image using a given kernel size. + + Arguments: + filt_span - how many pixels to take for the average on each side. The equivalent filter kernel is of side 2*filt_size + 1. + src - source image as a 2D numpy array. + + Returns: + Blurred image as a 2D numpy array. + """ + n = 2 * filt_span + 1 + nq = n * n + dest = np.zeros_like(src) + row_accum = np.zeros_like(src, dtype=np.int32) + col_accum = np.zeros(src.shape[1], dtype=np.int32) + + for i in range(src.shape[0]): + row_start = i * src.shape[1] + accum = src[i, 0] + row_accum[i, 0] = accum * n + + for j in range(1, filt_span + 1): + accum += src[i, j] + src[i, 2 * j] + row_accum[i, j] = accum * n // (2 * j + 1) + + for j in range(filt_span + 1, src.shape[1] - filt_span): + accum += src[i, j + filt_span] - src[i, j - filt_span - 1] + row_accum[i, j] = accum + + for j in range(src.shape[1] - filt_span, src.shape[1]): + accum -= src[i, 2 * (j - filt_span) - 1] + src[i, 2 * (j - filt_span)] + row_accum[i, j] = accum * n // (2 * (src.shape[1] - j) - 1) + + for j in range(src.shape[1]): + col_accum[j] = row_accum[0, j] + dest[0, j] = col_accum[j] // n + + for i in range(1, filt_span + 1): + for j in range(src.shape[1]): + col_accum[j] += row_accum[2 * i - 1, j] + row_accum[2 * i, j] + dest[i, j] = n * col_accum[j] // nq // (2 * i + 1) + + for i in range(filt_span + 1, src.shape[0] - filt_span): + for j in range(src.shape[1]): + col_accum[j] += row_accum[i + filt_span, j] - row_accum[i - filt_span - 1, j] + dest[i, j] = col_accum[j] // nq + + for i in range(src.shape[0] - filt_span, src.shape[0]): + for j in range(src.shape[1]): + col_accum[j] -= row_accum[2 * (i - filt_span) - 1, j] + row_accum[2 * (i - filt_span), j] + dest[i, j] = n * col_accum[j] // nq // (2 * (src.shape[0] - i) - 1) + + return dest + +def split(img: np.ndarray, half_selector: int) -> np.ndarray: + """ + Cram into the first half of a given image either its even or odd lines. Used with interlaced cameras, a mostly obsolete device. The lower half of the image is set to the number 2. + + Arguments: + img - the image to modify. Both input and output. + half_selector - 0 to do nothing, 1 to take odd rows, 2 for even rows + + Returns: + Modified image as a 2D numpy array. + """ + if half_selector == 0: + return img + + img_new = np.copy(img) + cond_offs = (half_selector % 2) * img.shape[1] + + for row in range(img.shape[0] // 2): + img_new[row, :] = img[2 * row + cond_offs // img.shape[1], :] + + img_new[img.shape[0] // 2:, :] = 2 + return img_new + +def subtract_img(img1: np.ndarray, img2: np.ndarray) -> np.ndarray: + """ + Subtract img2 from img1. + + Arguments: + img1, img2 - original images as 2D numpy arrays. + + Returns: + Resulting image as a 2D numpy array. + """ + return np.clip(img1 - img2, 0, None).astype(np.uint8) + +def subtract_mask(img: np.ndarray, img_mask: np.ndarray) -> np.ndarray: + """ + Compare img with img_mask and create a masked image img_new. Pixels that are equal to zero in the img_mask are overwritten with a default value (=0) in img_new. + + Arguments: + img - original image as a 2D numpy array. + img_mask - mask image as a 2D numpy array. + + Returns: + Resulting image as a 2D numpy array. + """ + img_new = np.copy(img) + img_new[img_mask == 0] = 0 + return img_new + +def copy_images(src: np.ndarray) -> np.ndarray: + """ + Copy one image into another. + + Arguments: + src - source image as a 2D numpy array. + + Returns: + Copied image as a 2D numpy array. + """ + return np.copy(src) + +def prepare_image(img: np.ndarray, dim_lp: int, filter_hp: int, filter_file: str, cpar: dict) -> np.ndarray: + """ + Perform the steps necessary for preparing an image to particle detection: an averaging (smoothing) filter on an image, optionally followed by additional user-defined filter. + + Arguments: + img - the source image to filter as a 2D numpy array. + dim_lp - half-width of lowpass filter. + filter_hp - flag for additional filtering of _hp. 1 for lowpass, 2 for general 3x3 filter given in parameter ``filter_file``. + filter_file - path to a text file containing the filter matrix to be used in case ``filter_hp == 2``. + cpar - image details such as size and image half for interlaced cases. + + Returns: + Filtered image as a 2D numpy array. + """ + img_lp = fast_box_blur(dim_lp, img) + img_hp = subtract_img(img, img_lp) + + if cpar['chfield'] == 1 or cpar['chfield'] == 2: + img_hp = split(img_hp, cpar['chfield']) + + if filter_hp == 1: + img_hp = lowpass_3(img_hp) + elif filter_hp == 2: + with open(filter_file, 'r') as fp: + filt = np.array([list(map(float, line.split())) for line in fp]) + img_hp = filter_3(img_hp, filt) + + return img_hp + +def threshold_image(img: np.ndarray, threshold: float) -> np.ndarray: + """ + Apply a binary threshold to the image. + + Arguments: + img - input image as a 2D numpy array. + threshold - threshold value. + + Returns: + Binary image as a 2D numpy array. + """ + return (img > threshold).astype(np.uint8) * 255 + +def find_local_maxima(img: np.ndarray) -> np.ndarray: + """ + Find local maxima in the image. + + Arguments: + img - input image as a 2D numpy array. + + Returns: + Image of the same size as img, with local maxima marked. + """ + from scipy.ndimage import maximum_filter, gaussian_filter + + # Smooth the image with a Gaussian filter + img_smoothed = gaussian_filter(img, sigma=1) + + # Find maxima using a maximum filter + img_maxima = maximum_filter(img_smoothed, size=3) == img_smoothed + + return img_maxima.astype(np.uint8) * 255 + +def label_connected_components(img: np.ndarray) -> Tuple[np.ndarray, int]: + """ + Label connected components in the image. + + Arguments: + img - input image as a 2D numpy array. + + Returns: + A tuple of (labeled_image, num_features), where labeled_image is the input image with connected components labeled, and num_features is the number of connected components. + """ + from scipy.ndimage import label + + # Label connected components + labeled_image, num_features = label(img) + + return labeled_image, num_features diff --git a/pyoptv/src/pyoptv/imgcoord.py b/pyoptv/src/pyoptv/imgcoord.py new file mode 100644 index 00000000..042933e3 --- /dev/null +++ b/pyoptv/src/pyoptv/imgcoord.py @@ -0,0 +1,84 @@ +import numpy as np +from typing import Tuple, Optional +from pyoptv.calibration import Calibration +from pyoptv.parameters import ControlPar, MMNP +from .multimed import trans_Cam_Point, multimed_nlay, back_trans_Point +from .trafo import flat_to_dist +from .vec_utils import vec_set + + +def flat_image_coord( + orig_pos: Tuple[float, float, float], + cal: Calibration, + mm: MMNP +) -> Tuple[float, float]: + """ + Calculates projection from coordinates in world space to metric coordinates in image space without distortions. + Args: + orig_pos: 3D position (X, Y, Z real space) + cal: Camera calibration parameters + mm: Layer thickness and refractive index parameters + Returns: + (x, y): metric coordinates of projection in the image space + """ + # Prepare temporary calibration and variables + cal_t = Calibration() + cal_t.mmlut = cal.mmlut + # Use the correct Python API for trans_Cam_Point + ex_t, pos_t, cross_p, cross_c = trans_Cam_Point( + cal.ext_par, mm, cal.glass_par, np.asarray(orig_pos) + ) + cal_t.ext_par = ex_t + X_t, Y_t = multimed_nlay(cal_t, mm, pos_t) + pos_t = vec_set(X_t, Y_t, pos_t[2] if hasattr(pos_t, '__getitem__') else pos_t.z) + pos = back_trans_Point(pos_t, mm, cal.glass_par, cross_p, cross_c) + + # Support both Vec3D and numpy array for pos + if hasattr(pos, 'x') and hasattr(pos, 'y') and hasattr(pos, 'z'): + dx = pos.x - cal.ext_par.x0 + dy = pos.y - cal.ext_par.y0 + dz = pos.z - cal.ext_par.z0 + else: + dx = pos[0] - cal.ext_par.x0 + dy = pos[1] - cal.ext_par.y0 + dz = pos[2] - cal.ext_par.z0 + # Avoid division by zero in denominator + deno = ( + cal.ext_par.dm[0][2] * dx + + cal.ext_par.dm[1][2] * dy + + cal.ext_par.dm[2][2] * dz + ) + if np.isclose(deno, 0.0): + return np.nan, np.nan + x = -cal.int_par.cc * ( + cal.ext_par.dm[0][0] * dx + + cal.ext_par.dm[1][0] * dy + + cal.ext_par.dm[2][0] * dz + ) / deno + y = -cal.int_par.cc * ( + cal.ext_par.dm[0][1] * dx + + cal.ext_par.dm[1][1] * dy + + cal.ext_par.dm[2][1] * dz + ) / deno + return x, y + + +def img_coord( + pos: Tuple[float, float, float], + cal: Calibration, + mm: MMNP +) -> Tuple[float, float]: + """ + Uses flat_image_coord to estimate metric coordinates in image space from the 3D position in the world and distorts it using the Brown distortion model. + Args: + pos: 3D position (X, Y, Z real space) + cal: Camera calibration parameters + mm: Layer thickness and refractive index parameters + Returns: + (x, y): metric distorted coordinates of projection in the image space + """ + x, y = flat_image_coord(pos, cal, mm) + x, y = flat_to_dist(x, y, cal) + return x, y + + diff --git a/pyoptv/src/pyoptv/lsqadj.py b/pyoptv/src/pyoptv/lsqadj.py new file mode 100644 index 00000000..cde49876 --- /dev/null +++ b/pyoptv/src/pyoptv/lsqadj.py @@ -0,0 +1,47 @@ +import numpy as np +from typing import Tuple +from scipy.linalg import inv + +def ata(a: np.ndarray, m: int, n: int, n_large: int) -> np.ndarray: + """ + Computes A^T * A for matrix a of shape (m, n_large). + Returns a matrix of shape (n, n_large). + """ + ata = np.zeros((n, n_large)) + for i in range(n): + for j in range(n): + for k in range(m): + ata[i, j] += a[k, i] * a[k, j] + return ata + +def atl(a: np.ndarray, l: np.ndarray, m: int, n: int, n_large: int) -> np.ndarray: + """ + Computes A^T * l for matrix a of shape (m, n_large) and vector l of shape (m,). + Returns a vector of shape (n,). + """ + u = np.zeros(n) + for i in range(n): + for k in range(m): + u[i] += a[k, i] * l[k] + return u + +def matinv(a: np.ndarray, n: int, n_large: int) -> np.ndarray: + """ + Returns the inverse of the top-left (n, n) submatrix of a. + """ + return inv(a[:n, :n]) + +def matmul( + a: np.ndarray, b: np.ndarray, m: int, n: int, k: int, m_large: int, n_large: int +) -> np.ndarray: + """ + Multiplies matrices a (m, n_large) and b (n_large, k). + Returns a matrix of shape (m, k). + """ + # Ensure dimensions passed to np.zeros are integers + c = np.zeros((int(m), int(k))) + for i in range(m): + for j in range(k): + for l in range(n): + c[i, j] += a[i, l] * b[l, j] + return c diff --git a/pyoptv/src/pyoptv/multimed.py b/pyoptv/src/pyoptv/multimed.py new file mode 100644 index 00000000..3845e809 --- /dev/null +++ b/pyoptv/src/pyoptv/multimed.py @@ -0,0 +1,249 @@ +import numpy as np +from typing import Tuple +from pyoptv.calibration import Calibration, Glass, Exterior +from pyoptv.parameters import ControlPar, MMNP, VolumePar + +def get_mmf_from_mmlut(cal: Calibration, pos: np.ndarray) -> float: + """ + Interpolates the multi-media factor (mmf) from the calibration's mmlut grid + for a given 3D position. + """ + rw = cal.mmlut.rw + temp = pos - cal.mmlut.origin + sz = temp[2] / rw + iz = int(sz) + sz -= iz + R = np.linalg.norm(temp[:2]) + sr = R / rw + ir = int(sr) + sr -= ir + nz = cal.mmlut.nz + nr = cal.mmlut.nr + if ir > nr or iz < 0 or iz > nz: + return 0 + v4 = [ + ir * nz + iz, + ir * nz + (iz + 1), + (ir + 1) * nz + iz, + (ir + 1) * nz + (iz + 1), + ] + for i in range(4): + if v4[i] < 0 or v4[i] > nr * nz: + return 0 + mmf = ( + cal.mmlut.data[v4[0]] * (1 - sr) * (1 - sz) + + cal.mmlut.data[v4[1]] * (1 - sr) * sz + + cal.mmlut.data[v4[2]] * sr * (1 - sz) + + cal.mmlut.data[v4[3]] * sr * sz + ) + return mmf + +def multimed_nlay(cal: Calibration, mm: MMNP, pos: np.ndarray) -> Tuple[float, float]: + """ + Applies multi-media correction for n-layer model. + Returns the corrected X, Y coordinates. + """ + radial_shift = multimed_r_nlay(cal, mm, pos) + Xq = cal.ext_par.x0 + (pos[0] - cal.ext_par.x0) * radial_shift + Yq = cal.ext_par.y0 + (pos[1] - cal.ext_par.y0) * radial_shift + return Xq, Yq + +def multimed_r_nlay(cal: Calibration, mm: MMNP, pos: np.ndarray) -> float: + """ + Computes the radial shift for a point in a multi-media, n-layer model. + Returns the scaling factor for the radial correction. + """ + if mm.n1 == 1 and mm.nlay == 1 and mm.n2[0] == 1 and mm.n3 == 1: + return 1.0 + if cal.mmlut.data is not None: + mmf = get_mmf_from_mmlut(cal, pos) + if mmf > 0: + return mmf + X, Y, Z = pos + zout = Z + sum(mm.d[1 : mm.nlay]) + r = np.linalg.norm([X - cal.ext_par.x0, Y - cal.ext_par.y0]) + rq = r + # Iteratively solve for the radial correction + for _ in range(40): + beta1 = np.arctan(rq / (cal.ext_par.z0 - Z)) + beta2 = [np.arcsin(np.sin(beta1) * mm.n1 / mm.n2[i]) for i in range(mm.nlay)] + beta3 = np.arcsin(np.sin(beta1) * mm.n1 / mm.n3) + rbeta = ( + (cal.ext_par.z0 - mm.d[0]) * np.tan(beta1) + - zout * np.tan(beta3) + + sum(mm.d[i] * np.tan(beta2[i]) for i in range(mm.nlay)) + ) + rdiff = r - rbeta + rq += rdiff + if abs(rdiff) < 0.001: + break + return rq / r if r != 0 else 1.0 + +def trans_Cam_Point( + ex: Exterior, mm: MMNP, gl: Glass, pos: np.ndarray +) -> Tuple[Exterior, np.ndarray, np.ndarray, np.ndarray]: + """ + Transforms a point from camera coordinates to glass coordinates, + considering the glass interface and multi-media parameters. + Returns the transformed exterior, transformed position, and crossing points. + """ + glass_dir = np.array([gl.vec_x, gl.vec_y, gl.vec_z]) + dist_o_glas = np.linalg.norm(glass_dir) + if np.isclose(dist_o_glas, 0.0): + # No glass, return original values + ex_t = Exterior() + ex_t.x0 = ex.x0 + ex_t.y0 = ex.y0 + ex_t.z0 = ex.z0 + return ex_t, pos, pos, pos + primary_pt = np.array([ex.x0, ex.y0, ex.z0]) + dist_cam_glas = ( + np.dot(primary_pt, glass_dir) / dist_o_glas - dist_o_glas - mm.d[0] + ) + dist_point_glas = np.dot(pos, glass_dir) / dist_o_glas - dist_o_glas + renorm_glass = glass_dir * (dist_cam_glas / dist_o_glas) + cross_c = primary_pt - renorm_glass + renorm_glass = glass_dir * (dist_point_glas / dist_o_glas) + cross_p = pos - renorm_glass + ex_t = Exterior() + ex_t.x0 = 0.0 + ex_t.y0 = 0.0 + ex_t.z0 = dist_cam_glas + mm.d[0] + renorm_glass = glass_dir * (mm.d[0] / dist_o_glas) + temp = cross_c - renorm_glass + pos_t = cross_p - temp + return ex_t, pos_t, cross_p, cross_c + +def back_trans_Point( + pos_t, mm: MMNP, G: Glass, cross_p, cross_c +) -> np.ndarray: + """ + Back-transforms a point from glass coordinates to camera coordinates. + Accepts pos_t as either a numpy array or a Vec3D. + """ + glass_dir = np.array([G.vec_x, G.vec_y, G.vec_z]) + nGl = np.linalg.norm(glass_dir) + if np.isclose(nGl, 0.0): + # Avoid division by zero if glass vector is zero (no glass) + if hasattr(pos_t, 'x') and hasattr(pos_t, 'y') and hasattr(pos_t, 'z'): + return np.array([pos_t.x, pos_t.y, pos_t.z]) + else: + return np.array([pos_t[0], pos_t[1], pos_t[2]]) + renorm_glass = glass_dir * (mm.d[0] / nGl) + after_glass = cross_c - renorm_glass + temp = cross_p - after_glass + nVe = np.linalg.norm(temp) + # Support both numpy array and Vec3D for pos_t + if hasattr(pos_t, 'x') and hasattr(pos_t, 'y') and hasattr(pos_t, 'z'): + pt0, pt2 = pos_t.x, pos_t.z + else: + pt0, pt2 = pos_t[0], pos_t[2] + renorm_glass = glass_dir * (-pt2 / nGl) + pos = after_glass - renorm_glass + if nVe > 0: + renorm_glass = temp * (-pt0 / nVe) + pos -= renorm_glass + return pos + +def move_along_ray(glob_Z: float, vertex: np.ndarray, direct: np.ndarray) -> np.ndarray: + """ + Moves a point along a ray to a specified Z coordinate. + """ + out = vertex + (glob_Z - vertex[2]) * direct / direct[2] + return out + +def init_mmlut(vpar: VolumePar, cpar: ControlPar, cal: Calibration) -> None: + """ + Initializes the multi-media lookup table (mmlut) for a given calibration. + """ + rw = 2.0 + cal_t = Calibration() + cal_t.ext_par = cal.ext_par + cal_t.int_par = cal.int_par + cal_t.glass_par = cal.glass_par + cal_t.added_par = cal.added_par + cal_t.mmlut = cal.mmlut + Zmin = min(vpar.Zmin_lay) + Zmax = max(vpar.Zmax_lay) + Zmin -= Zmin % rw + Zmax += rw - Zmax % rw + Zmin_t = Zmin + Zmax_t = Zmax + Rmax = 0 + # Compute the bounding box in Z and R + for x, y in [ + (0, 0), + (cpar.imx, 0), + (0, cpar.imy), + (cpar.imx, cpar.imy), + ]: + x -= cal.int_par.xh + y -= cal.int_par.yh + x, y = correct_brown_affin(x, y, cal.added_par) + pos, a = ray_tracing(x, y, cal, cpar.mm) + xyz = move_along_ray(Zmin, pos, a) + ex_t, xyz_t, cross_p, cross_c = trans_Cam_Point( + cal.ext_par, cpar.mm, cal.glass_par, xyz + ) + Zmin_t = min(Zmin_t, xyz_t[2]) + Zmax_t = max(Zmax_t, xyz_t[2]) + Rmax = max(Rmax, np.linalg.norm([xyz_t[0] - ex_t.x0, xyz_t[1] - ex_t.y0])) + xyz = move_along_ray(Zmax, pos, a) + ex_t, xyz_t, cross_p, cross_c = trans_Cam_Point( + cal.ext_par, cpar.mm, cal.glass_par, xyz + ) + Zmin_t = min(Zmin_t, xyz_t[2]) + Zmax_t = max(Zmax_t, xyz_t[2]) + Rmax = max(Rmax, np.linalg.norm([xyz_t[0] - ex_t.x0, xyz_t[1] - ex_t.y0])) + Rmax += rw - Rmax % rw + nr = int(Rmax / rw + 1) + nz = int((Zmax_t - Zmin_t) / rw + 1) + cal.mmlut.origin = np.array([cal_t.ext_par.x0, cal_t.ext_par.y0, Zmin_t]) + cal.mmlut.nr = nr + cal.mmlut.nz = nz + cal.mmlut.rw = rw + data = np.zeros((nr, nz)) + # Fill the lookup table with radial correction factors + for i in range(nr): + for j in range(nz): + xyz = np.array([i * rw + cal_t.ext_par.x0, cal_t.ext_par.y0, Zmin_t + j * rw]) + data[i, j] = multimed_r_nlay(cal_t, cpar.mm, xyz) + cal.mmlut.data = data + +def volumedimension( + xmax: float, + xmin: float, + ymax: float, + ymin: float, + zmax: float, + zmin: float, + vpar: VolumePar, + cpar: ControlPar, + cal: list, +) -> None: + """ + Computes the bounding box of the measurement volume in 3D space. + Updates xmax, xmin, ymax, ymin, zmax, zmin in place. + """ + Zmin = min(vpar.Zmin_lay) + Zmax = max(vpar.Zmax_lay) + zmin, zmax = Zmin, Zmax + for i_cam in range(cpar.num_cams): + for x, y in [ + (0, 0), + (cpar.imx, 0), + (0, cpar.imy), + (cpar.imx, cpar.imy), + ]: + x -= cal[i_cam].int_par.xh + y -= cal[i_cam].int_par.yh + x, y = correct_brown_affin(x, y, cal[i_cam].added_par) + pos, a = ray_tracing(x, y, cal[i_cam], cpar.mm) + X = pos[0] + (Zmin - pos[2]) * a[0] / a[2] + Y = pos[1] + (Zmin - pos[2]) * a[1] / a[2] + xmax, xmin = max(xmax, X), min(xmin, X) + ymax, ymin = max(ymax, Y), min(ymin, Y) + X = pos[0] + (Zmax - pos[2]) * a[0] / a[2] + Y = pos[1] + (Zmax - pos[2]) * a[1] / a[2] + xmax, xmin = max(xmax, X), min(xmin, X) + ymax, ymin = max(ymax, Y), min(ymin, Y) diff --git a/pyoptv/src/pyoptv/orientation.py b/pyoptv/src/pyoptv/orientation.py new file mode 100644 index 00000000..fc34063a --- /dev/null +++ b/pyoptv/src/pyoptv/orientation.py @@ -0,0 +1,399 @@ +# type: ignore +import numpy as np +from typing import List, Tuple, Optional +from scipy.optimize import least_squares +import matplotlib.pyplot as plt +from pyoptv.calibration import Calibration, Exterior +from pyoptv.parameters import ControlPar +from pyoptv.trafo import pixel_to_metric, correct_brown_affin +from pyoptv.imgcoord import img_coord +from pyoptv.ray_tracing import ray_tracing +from pyoptv.sortgrid import read_calblock + +COORD_UNUSED = -1e10 +IDT = 10 +NPAR = 19 +NUM_ITER = 80 +POS_INF = 1E20 +CONVERGENCE = 0.00001 + +class OrientPar: + """ + Parameter flags for orientation adjustment. + Each flag controls whether a parameter is included in the adjustment. + """ + def __init__( + self, + useflag: int = 0, + ccflag: int = 0, + xhflag: int = 0, + yhflag: int = 0, + k1flag: int = 0, + k2flag: int = 0, + k3flag: int = 0, + p1flag: int = 0, + p2flag: int = 0, + scxflag: int = 0, + sheflag: int = 0, + interfflag: int = 0, + ): + self.useflag = useflag + self.ccflag = ccflag + self.xhflag = xhflag + self.yhflag = yhflag + self.k1flag = k1flag + self.k2flag = k2flag + self.k3flag = k3flag + self.p1flag = p1flag + self.p2flag = p2flag + self.scxflag = scxflag + self.sheflag = sheflag + self.interfflag = interfflag + +def skew_midpoint( + vert1: np.ndarray, direct1: np.ndarray, vert2: np.ndarray, direct2: np.ndarray +) -> Tuple[float, np.ndarray]: + """ + Computes the shortest distance and midpoint between two skew lines in 3D. + """ + sp_diff = vert2 - vert1 + perp_both = np.cross(direct1, direct2) + scale = np.dot(perp_both, perp_both) + temp = np.cross(sp_diff, direct2) + on1 = vert1 + np.dot(perp_both, temp) / scale * direct1 + temp = np.cross(sp_diff, direct1) + on2 = vert2 + np.dot(perp_both, temp) / scale * direct2 + res = (on1 + on2) / 2 + return np.linalg.norm(on1 - on2), res + +def point_position( + targets: np.ndarray, + num_cams: int, + multimed_pars, + cals: List[Calibration], +) -> Tuple[float, np.ndarray]: + """ + Computes the average intersection point and mean distance between rays from multiple cameras. + """ + num_used_pairs = 0 + dtot = 0.0 + point_tot = np.zeros(3) + vertices = np.zeros((num_cams, 3)) + directs = np.zeros((num_cams, 3)) + for cam in range(num_cams): + if targets[cam, 0] != COORD_UNUSED: + vertices[cam], directs[cam] = ray_tracing( + targets[cam, 0], targets[cam, 1], cals[cam], multimed_pars + ) + for cam in range(num_cams): + if targets[cam, 0] == COORD_UNUSED: + continue + for pair in range(cam + 1, num_cams): + if targets[pair, 0] == COORD_UNUSED: + continue + num_used_pairs += 1 + dist, point = skew_midpoint( + vertices[cam], directs[cam], vertices[pair], directs[pair] + ) + dtot += dist + point_tot += point + res = point_tot / num_used_pairs + return dtot / num_used_pairs, res + +def weighted_dumbbell_precision( + targets: np.ndarray, + num_targs: int, + num_cams: int, + multimed_pars, + cals: List[Calibration], + db_length: float, + db_weight: float, +) -> float: + """ + Computes a weighted precision metric for a dumbbell calibration object. + """ + dtot = 0.0 + len_err_tot = 0.0 + res = np.zeros((2, 3)) + for pt in range(num_targs): + res_current = res[pt % 2] + dist, res_current = point_position(targets[pt], num_cams, multimed_pars, cals) + dtot += dist + if pt % 2 == 1: + dist = np.linalg.norm(res[0] - res[1]) + len_err_tot += 1 - (db_length / dist if dist > db_length else dist / db_length) + return dtot / num_targs + db_weight * len_err_tot / (0.5 * num_targs) + +def num_deriv_exterior( + cal: Calibration, + cpar: ControlPar, + dpos: float, + dang: float, + pos: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray]: + """ + Numerically computes derivatives of image coordinates with respect to exterior orientation parameters. + NOTE: This function mutates cal.ext_par fields in-place for finite differencing. + """ + x_ders = np.zeros(6) + y_ders = np.zeros(6) + vars = [ + cal.ext_par.x0, + cal.ext_par.y0, + cal.ext_par.z0, + cal.ext_par.omega, + cal.ext_par.phi, + cal.ext_par.kappa, + ] + xs, ys = img_coord(pos, cal, cpar.mm) + for pd in range(6): + step = dang if pd > 2 else dpos + vars[pd] += step + if pd > 2: + cal.ext_par.dm = Calibration.rotation_matrix( + cal.ext_par.omega, cal.ext_par.phi, cal.ext_par.kappa + ) + xpd, ypd = img_coord(pos, cal, cpar.mm) + x_ders[pd] = (xpd - xs) / step + y_ders[pd] = (ypd - ys) / step + vars[pd] -= step + cal.ext_par.dm = Calibration.rotation_matrix( + cal.ext_par.omega, cal.ext_par.phi, cal.ext_par.kappa + ) + return x_ders, y_ders + +def orient( + cal_in: Calibration, + cpar: ControlPar, + nfix: int, + fix: np.ndarray, + pix: List, + flags: OrientPar, + sigmabeta: np.ndarray, +) -> Optional[np.ndarray]: + """ + Performs iterative orientation adjustment for camera calibration. + Updates cal_in in-place if converged. + Returns residuals if converged, else None. + NOTE: This function mutates cal_in and expects pix to have .pnr attribute. + """ + cal = cal_in.copy() + maxsize = nfix * 2 + IDT + P = np.ones(maxsize) + y = np.zeros(maxsize) + X = np.zeros((maxsize, NPAR)) + ident = [cal.int_par.cc, cal.int_par.xh, cal.int_par.yh, cal.added_par.k1, cal.added_par.k2, cal.added_par.k3, cal.added_par.p1, cal.added_par.p2, cal.added_par.scx, cal.added_par.she] + safety_x, safety_y, safety_z = cal.glass_par.vec_x, cal.glass_par.vec_y, cal.glass_par.vec_z + glass_dir = np.array([cal.glass_par.vec_x, cal.glass_par.vec_y, cal.glass_par.vec_z]) + nGl = np.linalg.norm(glass_dir) + e1 = np.array([2 * cal.glass_par.vec_z - 3 * cal.glass_par.vec_x, 3 * cal.glass_par.vec_x - 1 * cal.glass_par.vec_z, 1 * cal.glass_par.vec_y - 2 * cal.glass_par.vec_y]) + e1 /= np.linalg.norm(e1) + e2 = np.cross(e1, glass_dir) + al, be, ga = 0, 0, 0 + itnum = 0 + stopflag = 0 + while stopflag == 0 and itnum < NUM_ITER: + itnum += 1 + n = 0 + for i in range(nfix): + if pix[i].pnr != i: + continue + if flags.useflag == 1 and i % 2 == 0: + continue + if flags.useflag == 2 and i % 2 != 0: + continue + if flags.useflag == 3 and i % 3 == 0: + continue + xc, yc = pixel_to_metric(pix[i].x, pix[i].y, cpar) + xc, yc = correct_brown_affin(xc, yc, cal.added_par) + xp, yp = img_coord(fix[i], cal, cpar.mm) + r = np.sqrt(xp ** 2 + yp ** 2) + X[n, 7] = cal.added_par.scx + X[n + 1, 7] = np.sin(cal.added_par.she) + X[n, 8] = 0 + X[n + 1, 8] = 1 + X[n, 9] = cal.added_par.scx * xp * r ** 2 + X[n + 1, 9] = yp * r ** 2 + X[n, 10] = cal.added_par.scx * xp * r ** 4 + X[n + 1, 10] = yp * r ** 4 + X[n, 11] = cal.added_par.scx * xp * r ** 6 + X[n + 1, 11] = yp * r ** 6 + X[n, 12] = cal.added_par.scx * (2 * xp ** 2 + r ** 2) + X[n + 1, 12] = 2 * xp * yp + X[n, 13] = 2 * cal.added_par.scx * xp * yp + X[n + 1, 13] = 2 * yp ** 2 + r ** 2 + qq = cal.added_par.k1 * r ** 2 + cal.added_par.k2 * r ** 4 + cal.added_par.k3 * r ** 6 + 1 + X[n, 14] = xp * qq + cal.added_par.p1 * (r ** 2 + 2 * xp ** 2) + 2 * cal.added_par.p2 * xp * yp + X[n + 1, 14] = 0 + X[n, 15] = -np.cos(cal.added_par.she) * yp + X[n + 1, 15] = -np.sin(cal.added_par.she) * yp + x_ders, y_ders = num_deriv_exterior(cal, cpar, 0.00001, 0.0000001, fix[i]) + X[n, :6] = x_ders + X[n + 1, :6] = y_ders + cal.int_par.cc += 0.00001 + xp_d, yp_d = img_coord(fix[i], cal, cpar.mm) + X[n, 6] = (xp_d - xp) / 0.00001 + X[n + 1, 6] = (yp_d - yp) / 0.00001 + cal.int_par.cc -= 0.00001 + al += 0.00001 + cal.glass_par.vec_x += e1[0] * nGl * al + cal.glass_par.vec_y += e1[1] * nGl * al + cal.glass_par.vec_z += e1[2] * nGl * al + xp_d, yp_d = img_coord(fix[i], cal, cpar.mm) + X[n, 16] = (xp_d - xp) / 0.00001 + X[n + 1, 16] = (yp_d - yp) / 0.00001 + al -= 0.00001 + cal.glass_par.vec_x = safety_x + cal.glass_par.vec_y = safety_y + cal.glass_par.vec_z = safety_z + be += 0.00001 + cal.glass_par.vec_x += e2[0] * nGl * be + cal.glass_par.vec_y += e2[1] * nGl * be + cal.glass_par.vec_z += e2[2] * nGl * be + xp_d, yp_d = img_coord(fix[i], cal, cpar.mm) + X[n, 17] = (xp_d - xp) / 0.00001 + X[n + 1, 17] = (yp_d - yp) / 0.00001 + be -= 0.00001 + cal.glass_par.vec_x = safety_x + cal.glass_par.vec_y = safety_y + cal.glass_par.vec_z = safety_z + ga += 0.00001 + cal.glass_par.vec_x += cal.glass_par.vec_x * nGl * ga + cal.glass_par.vec_y += cal.glass_par.vec_y * nGl * ga + cal.glass_par.vec_z += cal.glass_par.vec_z * nGl * ga + xp_d, yp_d = img_coord(fix[i], cal, cpar.mm) + X[n, 18] = (xp_d - xp) / 0.00001 + X[n + 1, 18] = (yp_d - yp) / 0.00001 + ga -= 0.00001 + cal.glass_par.vec_x = safety_x + cal.glass_par.vec_y = safety_y + cal.glass_par.vec_z = safety_z + y[n] = xc - xp + y[n + 1] = yc - yp + n += 2 + n_obs = n + for i in range(IDT): + X[n_obs + i, 6 + i] = 1 + y[n_obs:n_obs + IDT] = ident - np.array([cal.int_par.cc, cal.int_par.xh, cal.int_par.yh, cal.added_par.k1, cal.added_par.k2, cal.added_par.k3, cal.added_par.p1, cal.added_par.p2, cal.added_par.scx, cal.added_par.she]) + P[n_obs:n_obs + IDT] = [POS_INF if not flag else 1 for flag in [flags.ccflag, flags.xhflag, flags.yhflag, flags.k1flag, flags.k2flag, flags.k3flag, flags.p1flag, flags.p2flag, flags.scxflag, flags.sheflag]] + n_obs += IDT + sumP = np.sum(P[:n_obs]) + p = np.sqrt(P[:n_obs]) + Xh = X[:n_obs] * p[:, np.newaxis] + yh = y[:n_obs] * p + XPX = np.linalg.inv(Xh.T @ Xh) + XPy = Xh.T @ yh + beta = XPX @ XPy + stopflag = np.all(np.abs(beta[:16]) <= CONVERGENCE) + cal.ext_par.x0 += beta[0] + cal.ext_par.y0 += beta[1] + cal.ext_par.z0 += beta[2] + cal.ext_par.omega += beta[3] + cal.ext_par.phi += beta[4] + cal.ext_par.kappa += beta[5] + cal.int_par.cc += beta[6] + cal.int_par.xh += beta[7] + cal.int_par.yh += beta[8] + cal.added_par.k1 += beta[9] + cal.added_par.k2 += beta[10] + cal.added_par.k3 += beta[11] + cal.added_par.p1 += beta[12] + cal.added_par.p2 += beta[13] + cal.added_par.scx += beta[14] + cal.added_par.she += beta[15] + if flags.interfflag: + cal.glass_par.vec_x += e1[0] * nGl * beta[16] + cal.glass_par.vec_y += e1[1] * nGl * beta[16] + cal.glass_par.vec_z += e1[2] * nGl * beta[16] + cal.glass_par.vec_x += e2[0] * nGl * beta[17] + cal.glass_par.vec_y += e2[1] * nGl * beta[17] + cal.glass_par.vec_z += e2[2] * nGl * beta[17] + resi = X @ beta - y + omega = np.sum(resi[:n_obs] ** 2 * P[:n_obs]) + sigmabeta[:NPAR] = np.sqrt(np.diag(XPX) * omega / (n_obs - 16)) + sigmabeta[NPAR] = np.sqrt(omega / (n_obs - 16)) + if stopflag: + cal_in.update(cal) + return resi + else: + return None + +def raw_orient( + cal: Calibration, + cpar: ControlPar, + nfix: int, + fix: np.ndarray, + pix: List, +) -> bool: + """ + Performs a raw orientation adjustment (no distortion parameters). + Returns True if converged, else False. + """ + X = np.zeros((10, 6)) + y = np.zeros(10) + cal.added_par.k1 = 0 + cal.added_par.k2 = 0 + cal.added_par.k3 = 0 + cal.added_par.p1 = 0 + cal.added_par.p2 = 0 + cal.added_par.scx = 1 + cal.added_par.she = 0 + itnum = 0 + stopflag = 0 + while stopflag == 0 and itnum < 20: + itnum += 1 + n = 0 + for i in range(nfix): + xc, yc = pixel_to_metric(pix[i].x, pix[i].y, cpar) + xp, yp = img_coord(fix[i], cal, cpar.mm) + x_ders, y_ders = num_deriv_exterior(cal, cpar, 0.0001, 0.0001, fix[i]) + X[n, :6] = x_ders + X[n + 1, :6] = y_ders + y[n] = xc - xp + y[n + 1] = yc - yp + n += 2 + XPX = np.linalg.inv(X[:n].T @ X[:n]) + XPy = X[:n].T @ y[:n] + beta = XPX @ XPy + stopflag = np.all(np.abs(beta) <= 0.1) + cal.ext_par.x0 += beta[0] + cal.ext_par.y0 += beta[1] + cal.ext_par.z0 += beta[2] + cal.ext_par.omega += beta[3] + cal.ext_par.phi += beta[4] + cal.ext_par.kappa += beta[5] + if stopflag: + rotation_matrix(cal.ext_par) + return stopflag + +def read_man_ori_fix( + fix4: np.ndarray, + calblock_filename: str, + man_ori_filename: str, + cam: int, +) -> int: + """ + Reads four manually oriented calibration points from file and fills fix4. + Returns the number of matches found. + """ + with open(man_ori_filename, "r") as fpp: + for _ in range(cam): + fpp.readline() + nr = list(map(int, fpp.readline().split())) + fix = read_calblock(calblock_filename) + num_match = 0 + for pnr, point in enumerate(fix): + if pnr in nr: + fix4[nr.index(pnr)] = point + num_match += 1 + if num_match >= 4: + break + return num_match + +def read_orient_par(filename: str) -> OrientPar: + """ + Reads orientation parameter flags from a file and returns an OrientPar instance. + """ + with open(filename, "r") as file: + params = list(map(int, file.read().split())) + return OrientPar(*params) diff --git a/pyoptv/src/pyoptv/parameters.py b/pyoptv/src/pyoptv/parameters.py new file mode 100644 index 00000000..95f419bc --- /dev/null +++ b/pyoptv/src/pyoptv/parameters.py @@ -0,0 +1,302 @@ +# type: ignore +import numpy as np +from typing import List + +SEQ_FNAME_MAX_LEN: int = 240 + +class SequencePar: + num_cams: int + img_base_name: List[str] + first: int + last: int + def __init__(self, num_cams: int) -> None: + self.num_cams = num_cams + self.img_base_name = [""] * num_cams + self.first = 0 + self.last = 0 + +def read_sequence_par(filename, num_cams): + ret = SequencePar(num_cams) + with open(filename, "r") as par_file: + for cam in range(num_cams): + ret.img_base_name[cam] = par_file.readline().strip() + ret.first = int(par_file.readline().strip()) + ret.last = int(par_file.readline().strip()) + return ret + +def new_sequence_par(num_cams): + return SequencePar(num_cams) + +def free_sequence_par(sp): + del sp + +def compare_sequence_par(sp1, sp2): + if sp1.first != sp2.first or sp1.last != sp2.last or sp1.num_cams != sp2.num_cams: + return False + for cam in range(sp1.num_cams): + if sp1.img_base_name[cam] != sp2.img_base_name[cam]: + return False + return True + +class TrackPar: + dacc: float + dangle: float + dvxmax: float + dvxmin: float + dvymax: float + dvymin: float + dvzmax: float + dvzmin: float + dsumg: int + dn: int + dnx: int + dny: int + add: int + def __init__(self) -> None: + self.dacc = 0.0 + self.dangle = 0.0 + self.dvxmax = 0.0 + self.dvxmin = 0.0 + self.dvymax = 0.0 + self.dvymin = 0.0 + self.dvzmax = 0.0 + self.dvzmin = 0.0 + self.dsumg = 0 + self.dn = 0 + self.dnx = 0 + self.dny = 0 + self.add = 0 + +def read_track_par(filename): + ret = TrackPar() + with open(filename, "r") as fpp: + ret.dvxmin = float(fpp.readline().strip()) + ret.dvxmax = float(fpp.readline().strip()) + ret.dvymin = float(fpp.readline().strip()) + ret.dvymax = float(fpp.readline().strip()) + ret.dvzmin = float(fpp.readline().strip()) + ret.dvzmax = float(fpp.readline().strip()) + ret.dangle = float(fpp.readline().strip()) + ret.dacc = float(fpp.readline().strip()) + ret.add = int(fpp.readline().strip()) + return ret + +def compare_track_par(t1, t2): + return (t1.dvxmin == t2.dvxmin and t1.dvxmax == t2.dvxmax and + t1.dvymin == t2.dvymin and t1.dvymax == t2.dvymax and + t1.dvzmin == t2.dvzmin and t1.dvzmax == t2.dvzmax and + t1.dacc == t2.dacc and t1.dangle == t2.dangle and + t1.dsumg == t2.dsumg and t1.dn == t2.dn and + t1.dnx == t2.dnx and t1.dny == t2.dny and t1.add == t2.add) + +class VolumePar: + def __init__( + self, + X_lay: List[float] = None, + Zmin_lay: List[float] = None, + Zmax_lay: List[float] = None, + cnx: float = 0.3, + cny: float = 0.3, + cn: float = 0.01, + csumg: float = 0.01, + corrmin: float = 33.0, + eps0: float = 1.0, + ) -> None: + self.X_lay = X_lay if X_lay is not None else [-100.0, 100.0] + self.Zmin_lay = Zmin_lay if Zmin_lay is not None else [-100.0, -100.0] + self.Zmax_lay = Zmax_lay if Zmax_lay is not None else [100.0, 100.0] + self.cnx = cnx + self.cny = cny + self.cn = cn + self.csumg = csumg + self.corrmin = corrmin + self.eps0 = eps0 + +def read_volume_par(filename): + ret = VolumePar() + with open(filename, "r") as fpp: + ret.X_lay[0] = float(fpp.readline().strip()) + ret.Zmin_lay[0] = float(fpp.readline().strip()) + ret.Zmax_lay[0] = float(fpp.readline().strip()) + ret.X_lay[1] = float(fpp.readline().strip()) + ret.Zmin_lay[1] = float(fpp.readline().strip()) + ret.Zmax_lay[1] = float(fpp.readline().strip()) + ret.cnx = float(fpp.readline().strip()) + ret.cny = float(fpp.readline().strip()) + ret.cn = float(fpp.readline().strip()) + ret.csumg = float(fpp.readline().strip()) + ret.corrmin = float(fpp.readline().strip()) + ret.eps0 = float(fpp.readline().strip()) + return ret + +def compare_volume_par(v1, v2): + return ( + v1.X_lay == v2.X_lay and + v1.Zmin_lay == v2.Zmin_lay and + v1.Zmax_lay == v2.Zmax_lay and + v1.cnx == v2.cnx and + v1.cny == v2.cny and + v1.cn == v2.cn and + v1.csumg == v2.csumg and + v1.corrmin == v2.corrmin and + v1.eps0 == v2.eps0 + ) + +class MMNP: + nlay: int + n1: float + d: List[float] + n2: List[float] + n3: float + + def __init__( + self, + nlay: int = 1, + n1: float = 1.0, + d: List[float] = None, + n2: List[float] = None, + n3: float = 1.0, + ) -> None: + self.nlay = nlay + self.n1 = n1 + self.n2 = n2 if n2 is not None else [1.0, 1.0, 1.0] + self.d = d if d is not None else [0.0, 0.0, 0.0] + self.n3 = n3 + +class ControlPar: + num_cams: int + img_base_name: List[str] + cal_img_base_name: List[str] + hp_flag: int + allCam_flag: int + tiff_flag: int + imx: int + imy: int + pix_x: float + pix_y: float + chfield: int + mm: MMNP + + def __init__(self, num_cams: int = 1) -> None: + self.num_cams = num_cams + self.img_base_name = [""] * num_cams + self.cal_img_base_name = [""] * num_cams + self.hp_flag = 0 + self.allCam_flag = 0 + self.tiff_flag = 0 + self.imx = 256 # pix + self.imy = 256 # pix + self.pix_x = 0.01 + self.pix_y = 0.01 + self.chfield = 0 + self.mm = MMNP() + + def set_image_size(self, size): + """Set image size (imx, imy) from a (width, height) tuple.""" + self.imx, self.imy = size + + def set_pixel_size(self, pix_size): + """Set pixel size (pix_x, pix_y) from a (pix_x, pix_y) tuple.""" + self.pix_x, self.pix_y = pix_size + +def read_control_par(filename: str) -> ControlPar: + with open(filename, "r") as par_file: + num_cams = int(par_file.readline().strip()) + ret = ControlPar(num_cams) + for cam in range(num_cams): + ret.img_base_name[cam] = par_file.readline().strip() + ret.cal_img_base_name[cam] = par_file.readline().strip() + ret.hp_flag = int(par_file.readline().strip()) + ret.allCam_flag = int(par_file.readline().strip()) + ret.tiff_flag = int(par_file.readline().strip()) + ret.imx = int(par_file.readline().strip()) + ret.imy = int(par_file.readline().strip()) + ret.pix_x = float(par_file.readline().strip()) + ret.pix_y = float(par_file.readline().strip()) + ret.chfield = int(par_file.readline().strip()) + ret.mm.n1 = float(par_file.readline().strip()) + ret.mm.n2[0] = float(par_file.readline().strip()) + ret.mm.n3 = float(par_file.readline().strip()) + ret.mm.d[0] = float(par_file.readline().strip()) + ret.mm.nlay = 1 + return ret + +def free_control_par(cp: ControlPar) -> None: + del cp + +def compare_control_par(c1: ControlPar, c2: ControlPar) -> bool: + if c1.num_cams != c2.num_cams: + return False + for cam in range(c1.num_cams): + if c1.img_base_name[cam] != c2.img_base_name[cam]: + return False + if c1.cal_img_base_name[cam] != c2.cal_img_base_name[cam]: + return False + if (c1.hp_flag != c2.hp_flag or c1.allCam_flag != c2.allCam_flag or + c1.tiff_flag != c2.tiff_flag or c1.imx != c2.imx or + c1.imy != c2.imy or c1.pix_x != c2.pix_x or c1.pix_y != c2.pix_y or + c1.chfield != c2.chfield): + return False + return compare_mm_np(c1.mm, c2.mm) + +def compare_mm_np(mm_np1: MMNP, mm_np2: MMNP) -> bool: + if mm_np1.n2[0] != mm_np2.n2[0] or mm_np1.d[0] != mm_np2.d[0]: + return False + if mm_np1.nlay != mm_np2.nlay or mm_np1.n1 != mm_np2.n1 or mm_np1.n3 != mm_np2.n3: + return False + return True + +class TargetPar: + discont: int + gvthres: List[int] + nnmin: int + nnmax: int + nxmin: int + nxmax: int + nymin: int + nymax: int + sumg_min: int + cr_sz: int + def __init__(self) -> None: + self.discont = 0 + self.gvthres = [0, 0, 0, 0] + self.nnmin = 0 + self.nnmax = 0 + self.nxmin = 0 + self.nxmax = 0 + self.nymin = 0 + self.nymax = 0 + self.sumg_min = 0 + self.cr_sz = 0 + +def read_target_par(filename: str = "target.par") -> TargetPar: + ret = TargetPar() + with open(filename, "r") as file: + ret.gvthres[0] = int(file.readline().strip()) + ret.gvthres[1] = int(file.readline().strip()) + ret.gvthres[2] = int(file.readline().strip()) + ret.gvthres[3] = int(file.readline().strip()) + ret.discont = int(file.readline().strip()) + ret.nnmin, ret.nnmax = map(int, file.readline().strip().split()) + ret.nxmin, ret.nxmax = map(int, file.readline().strip().split()) + ret.nymin, ret.nymax = map(int, file.readline().strip().split()) + ret.sumg_min = int(file.readline().strip()) + ret.cr_sz = int(file.readline().strip()) + return ret + +def compare_target_par(targ1: TargetPar, targ2: TargetPar) -> bool: + return (targ1.discont == targ2.discont and + targ1.gvthres[0] == targ2.gvthres[0] and + targ1.gvthres[1] == targ2.gvthres[1] and + targ1.gvthres[2] == targ2.gvthres[2] and + targ1.gvthres[3] == targ2.gvthres[3] and + targ1.nnmin == targ2.nnmin and targ1.nnmax == targ2.nnmax and + targ1.nxmin == targ2.nxmin and targ1.nxmax == targ2.nxmax and + targ1.nymin == targ2.nymin and targ1.nymax == targ2.nymax and + targ1.sumg_min == targ2.sumg_min and targ1.cr_sz == targ2.cr_sz) + +def write_target_par(targ: TargetPar, filename: str) -> None: + with open(filename, "w") as file: + file.write(f"{targ.gvthres[0]}\n{targ.gvthres[1]}\n{targ.gvthres[2]}\n{targ.gvthres[3]}\n") + file.write(f"{targ.discont}\n{targ.nnmin} {targ.nnmax}\n{targ.nxmin} {targ.nxmax}\n") + file.write(f"{targ.nymin} {targ.nymax}\n{targ.sumg_min}\n{targ.cr_sz}\n") diff --git a/pyoptv/src/pyoptv/ray_tracing.py b/pyoptv/src/pyoptv/ray_tracing.py new file mode 100644 index 00000000..93f4fa48 --- /dev/null +++ b/pyoptv/src/pyoptv/ray_tracing.py @@ -0,0 +1,70 @@ +import numpy as np +from .vec_utils import vec_set, vec_subt, vec_add, vec_scalar_mul, vec_norm, vec_dot, unit_vector +from pyoptv.calibration import Calibration +from pyoptv.parameters import MMNP +from .lsqadj import matmul + +def ray_tracing(x: float, y: float, cal: Calibration, mm: MMNP) -> tuple[np.ndarray, np.ndarray]: + """ + Translated implementation of ray tracing from C to Python. + Args: + x: X coordinate in image space + y: Y coordinate in image space + cal: Camera calibration parameters + mm: Multi-media parameters (thickness and refractive indices) + + Returns: + X: Intersection point in the glass + out: Direction vector after passing through the glass + """ + + + # Initial ray direction in global coordinate system + tmp1 = vec_set(x, y, -1 * cal.int_par.cc) + tmp1 = unit_vector(tmp1) + # Reshape tmp1 to 2D array for matmul + tmp1 = tmp1.reshape(-1, 1) + start_dir = matmul(cal.ext_par.dm, tmp1, 3, 3, 1, 3, 3).flatten() + + primary_point = vec_set(cal.ext_par.x0, cal.ext_par.y0, cal.ext_par.z0) + + glass_dir = vec_set(cal.glass_par.vec_x, cal.glass_par.vec_y, cal.glass_par.vec_z) + glass_dir = unit_vector(glass_dir) + c = vec_norm(tmp1) + mm.d[0] + + dist_cam_glass = vec_dot(glass_dir, primary_point) - c + d1 = -dist_cam_glass / vec_dot(glass_dir, start_dir) + tmp1 = vec_scalar_mul(start_dir, d1) + Xb = vec_add(primary_point, tmp1) + + n = vec_dot(start_dir, glass_dir) + tmp1 = vec_scalar_mul(glass_dir, n) + tmp2 = vec_subt(start_dir, tmp1) + bp = unit_vector(tmp2) + + # Align Snell's law computation with C implementation + p = np.sqrt(1 - n * n) * mm.n1 / mm.n2[0] + n = -1*np.sqrt(1 - p * p) + + tmp1 = vec_scalar_mul(bp, p) + tmp2 = vec_scalar_mul(glass_dir, n) + a2 = vec_add(tmp1, tmp2) + + d2 = mm.d[0] / abs(vec_dot(glass_dir, a2)) + + tmp1 = vec_scalar_mul(a2, d2) + X = vec_add(Xb, tmp1) + + n = vec_dot(a2, glass_dir) + tmp2 = vec_subt(a2, tmp2) + bp = unit_vector(tmp2) + + p = np.sqrt(1 - n * n) + p = p * mm.n2[0] / mm.n3 + n = -np.sqrt(1 - p * p) + + tmp1 = vec_scalar_mul(bp, p) + tmp2 = vec_scalar_mul(glass_dir, n) + out = vec_add(tmp1, tmp2) + + return X, out diff --git a/pyoptv/src/pyoptv/segmentation.py b/pyoptv/src/pyoptv/segmentation.py new file mode 100644 index 00000000..45c06597 --- /dev/null +++ b/pyoptv/src/pyoptv/segmentation.py @@ -0,0 +1,372 @@ +import numpy as np +from typing import List +from scipy.ndimage import label +import matplotlib.pyplot as plt +from pyoptv.tracking_frame_buf import Target +from pyoptv.parameters import ControlPar, TargetPar + + +class Peak: + def __init__( + self, + pos: int = 0, + status: int = 0, + xmin: int = 0, + xmax: int = 0, + ymin: int = 0, + ymax: int = 0, + n: int = 0, + sumg: int = 0, + x: float = 0.0, + y: float = 0.0, + unr: int = 0, + touch: list = None, + n_touch: int = 0, + ): + self.pos = pos + self.status = status + self.xmin = xmin + self.xmax = xmax + self.ymin = ymin + self.ymax = ymax + self.n = n + self.sumg = sumg + self.x = x + self.y = y + self.unr = unr + self.touch = touch if touch is not None else [0, 0, 0, 0] + self.n_touch = n_touch + + +def targ_rec( + img: np.ndarray, + targ_par: TargetPar, + xmin: int, + xmax: int, + ymin: int, + ymax: int, + cpar: ControlPar, + num_cam: int, +) -> List[Target]: + imx, imy = cpar.imx, cpar.imy + thres = targ_par.gvthres[num_cam] + disco = targ_par.discont + img0 = np.copy(img) + img0 = np.pad(img0, ((1, 1), (1, 1)), mode="constant", constant_values=0) + xmin, xmax = max(1, xmin), min(imx - 1, xmax) + ymin, ymax = max(1, ymin), min(imy - 1, ymax) + targets: List[Target] = [] + + for i in range(ymin, ymax): + for j in range(xmin, xmax): + gv = img0[i, j] + if gv > thres and all( + gv >= img0[i + di, j + dj] + for di in [-1, 0, 1] + for dj in [-1, 0, 1] + if di != 0 or dj != 0 + ): + yn, xn = i, j + sumg = gv + img0[i, j] = 0 + xa, xb, ya, yb = xn, xn, yn, yn + gv -= thres + x, y = xn * gv, yn * gv + numpix = 1 + waitlist = [(j, i)] + + while waitlist: + gvref = img[waitlist[0][1], waitlist[0][0]] + x4 = [ + waitlist[0][0] - 1, + waitlist[0][0] + 1, + waitlist[0][0], + waitlist[0][0], + ] + y4 = [ + waitlist[0][1], + waitlist[0][1], + waitlist[0][1] - 1, + waitlist[0][1] + 1, + ] + + for xn, yn in zip(x4, y4): + if not ( + xmin - 1 < xn < xmax + 1 and ymin - 1 < yn < ymax + 1 + ): + continue + gv = img0[yn, xn] + if ( + gv > thres + and gv <= gvref + disco + and all( + gvref + disco + >= img[yn + di, xn + dj] + for di, dj in [(-1, 0), (1, 0), (0, -1), (0, 1)] + ) + ): + sumg += gv + img0[yn, xn] = 0 + xa, xb, ya, yb = ( + min(xa, xn), + max(xb, xn), + min(ya, yn), + max(yb, yn), + ) + waitlist.append((xn, yn)) + x += xn * (gv - thres) + y += yn * (gv - thres) + numpix += 1 + + waitlist.pop(0) + + if ( + xa == xmin - 1 + or ya == ymin - 1 + or xb == xmax + 1 + or yb == ymax + 1 + ): + continue + + nx, ny = xb - xa + 1, yb - ya + 1 + if ( + targ_par.nnmin <= numpix <= targ_par.nnmax + and targ_par.nxmin <= nx <= targ_par.nxmax + and targ_par.nymin <= ny <= targ_par.nymax + and sumg > targ_par.sumg_min + ): + sumg -= numpix * thres + x_c = x / sumg + 0.5 + y_c = y / sumg + 0.5 + targets.append( + Target( + pnr=len(targets), + x=x_c, + y=y_c, + n=numpix, + nx=nx, + ny=ny, + sumg=sumg, + tnr=-1, # Use -1 as default tnr, matching C struct default + ) + ) + + if not targets: + targets.append( + Target( + pnr=1, + x=1, + y=1, + n=1, + nx=1, + ny=1, + sumg=1, + tnr=-1, # Use -1 as default tnr + ) + ) + + return targets + + +def peak_fit( + img: np.ndarray, + targ_par: TargetPar, + xmin: int, + xmax: int, + ymin: int, + ymax: int, + cpar: ControlPar, + num_cam: int, +) -> List[Target]: + imx, imy = cpar.imx, cpar.imy + thres = targ_par.gvthres[num_cam] + disco = targ_par.discont + label_img = np.zeros((imx, imy), dtype=np.int16) + peaks: List[Peak] = [] + n_peaks = 0 + + for i in range(ymin, ymax - 1): + for j in range(xmin, xmax): + n = i * imx + j + gv = img[i, j] + if gv <= thres or label_img[i, j] != 0: + continue + if all( + gv >= img[i + di, j + dj] + for di in [-1, 0, 1] + for dj in [-1, 0, 1] + if di != 0 or dj != 0 + ): + n_peaks += 1 + label_img[i, j] = n_peaks + peaks.append( + Peak( + pos=n, + status=1, + xmin=j, + xmax=j, + ymin=i, + ymax=i, + unr=0, + n=0, + sumg=0, + x=0.0, + y=0.0, + n_touch=0, + touch=[0, 0, 0, 0], + ) + ) + waitlist = [(j, i)] + + while waitlist: + gvref = img[waitlist[0][1], waitlist[0][0]] + x8 = [ + waitlist[0][0] - 1, + waitlist[0][0] + 1, + waitlist[0][0], + waitlist[0][0], + ] + y8 = [ + waitlist[0][1], + waitlist[0][1], + waitlist[0][1] - 1, + waitlist[0][1] + 1, + ] + + for xn, yn in zip(x8, y8): + if ( + not (0 <= xn < imx and 0 <= yn < imy) + or label_img[yn, xn] != 0 + ): + continue + gv = img[yn, xn] + if ( + gv > thres + and gv <= gvref + disco + and all( + gvref + disco + >= img[yn + di, xn + dj] + for di, dj in [(-1, 0), (1, 0), (0, -1), (0, 1)] + ) + ): + label_img[yn, xn] = n_peaks + waitlist.append((xn, yn)) + + waitlist.pop(0) + + for i in range(ymin, ymax): + for j in range(xmin, xmax): + n = i * imx + j + if label_img[i, j] > 0: + pnr = label_img[i, j] + gv = img[i, j] + peak = peaks[pnr - 1] + peak.n += 1 + peak.sumg += gv + peak.x += j * gv + peak.y += i * gv + peak.xmin, peak.xmax = min(peak.xmin, j), max(peak.xmax, j) + peak.ymin, peak.ymax = min(peak.ymin, i), max(peak.ymax, i) + + for di, dj in [ + (-1, -1), + (-1, 0), + (-1, 1), + (0, -1), + (0, 1), + (1, -1), + (1, 0), + (1, 1), + ]: + if 0 <= i + di < imy and 0 <= j + dj < imx: + check_touch(peak, pnr, label_img[i + di, j + dj]) + + for i in range(n_peaks): + if peaks[i].n_touch == 0 or peaks[i].unr != 0: + continue + x1, y1 = peaks[i].x / peaks[i].sumg, peaks[i].y / peaks[i].sumg + gv1 = img[peaks[i].pos // imx, peaks[i].pos % imx] + + for j in range(peaks[i].n_touch): + p2 = peaks[i].touch[j] - 1 + if p2 >= n_peaks or p2 < 0 or peaks[p2].unr != 0: + continue + x2, y2 = peaks[p2].x / peaks[p2].sumg, peaks[p2].y / peaks[p2].sumg + gv2 = img[peaks[p2].pos // imx, peaks[p2].pos % imx] + s12 = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + unify = ( + 1 + if s12 < 2.0 + else all( + img[ + int(y1 + l * (y2 - y1) / s12), + int(x1 + l * (x2 - x1) / s12), + ] + + disco + >= gv1 + l * (gv2 - gv1) / s12 + for l in range(1, int(s12)) + ) + ) + + if unify: + peaks[i].unr = p2 + peaks[p2].x += peaks[i].x + peaks[p2].y += peaks[i].y + peaks[p2].sumg += peaks[i].sumg + peaks[p2].n += peaks[i].n + peaks[p2].xmin, peaks[p2].xmax = min( + peaks[p2].xmin, peaks[i].xmin + ), max(peaks[p2].xmax, peaks[i].xmax) + peaks[p2].ymin, peaks[p2].ymax = min( + peaks[p2].ymin, peaks[i].ymin + ), max(peaks[p2].ymax, peaks[i].ymax) + + targets: List[Target] = [] + for i in range(n_peaks): + if ( + peaks[i].xmin == xmin + and (xmax - xmin) > 32 + or peaks[i].ymin == ymin + and (xmax - xmin) > 32 + or peaks[i].xmax == xmax - 1 + and (xmax - xmin) > 32 + or peaks[i].ymax == ymax - 1 + and (xmax - xmin) > 32 + ): + continue + if ( + peaks[i].unr == 0 + and peaks[i].sumg > targ_par.sumg_min + and targ_par.nxmin + <= peaks[i].xmax - peaks[i].xmin + 1 + <= targ_par.nxmax + and targ_par.nymin + <= peaks[i].ymax - peaks[i].ymin + 1 + <= targ_par.nymax + and targ_par.nnmin <= peaks[i].n <= targ_par.nnmax + ): + sumg = peaks[i].sumg + x, y = 0.5 + peaks[i].x / sumg, 0.5 + peaks[i].y / sumg + targets.append( + Target( + x=x, + y=y, + sumg=sumg, + n=peaks[i].n, + nx=peaks[i].xmax - peaks[i].xmin + 1, + ny=peaks[i].ymax - peaks[i].ymin + 1, + tnr=-1, + pnr=len(targets), + ) + ) + + return targets + + +def check_touch(peak: Peak, p1: int, p2: int) -> None: + if p2 == 0 or p2 == p1: + return + if p2 not in peak.touch[: peak.n_touch]: + peak.touch[peak.n_touch] = p2 + peak.n_touch = min(peak.n_touch + 1, 3) + diff --git a/pyoptv/src/pyoptv/sortgrid.py b/pyoptv/src/pyoptv/sortgrid.py new file mode 100644 index 00000000..e835a31d --- /dev/null +++ b/pyoptv/src/pyoptv/sortgrid.py @@ -0,0 +1,93 @@ +import numpy as np +from typing import Tuple, List +from pyoptv.calibration import Calibration +from pyoptv.parameters import ControlPar, MMNP +from pyoptv.tracking_frame_buf import Target +from pyoptv.imgcoord import img_coord +from pyoptv.trafo import metric_to_pixel + + + + +def sortgrid( + cal: Calibration, + cpar: ControlPar, + nfix: int, + fix: np.ndarray, + num: int, + eps: float, + pix: List[Target] +) -> List[Target]: + """ + Literal translation of liboptv's sortgrid.c: + For each calibration point in fix, project to image, find nearest detected target in pix within eps, + assign all fields, set pnr to i if found, else pnr=-999. + """ + # Pre-allocate output array + sorted_pix: List[Target] = [Target(-999, 0, 0, 0, 0, 0, 0, -1) for _ in range(nfix)] + for i in range(nfix): + # Project calibration point to image coordinates + xp, yp = img_coord(fix[i], cal, cpar.mm) + calib_point = metric_to_pixel(xp, yp, cpar) + # If projected point is not touching the image border + if (calib_point[0] > -eps and calib_point[1] > -eps and + calib_point[0] < cpar.imx + eps and calib_point[1] < cpar.imy + eps): + # Find the nearest target point + j = nearest_neighbour_pix(pix, num, calib_point[0], calib_point[1], eps) + if j != -999: + # Assign all fields from pix[j], but set pnr to i + sorted_pix[i] = Target( + pnr=i, + x=pix[j].x, + y=pix[j].y, + n=pix[j].n, + nx=pix[j].nx, + ny=pix[j].ny, + sumg=pix[j].sumg, + tnr=pix[j].tnr + ) + # else: already set to unused + # else: already set to unused + return sorted_pix + +def nearest_neighbour_pix( + pix: List[Target], + num: int, + x: float, + y: float, + eps: float +) -> int: + xmin, xmax = x - eps, x + eps + ymin, ymax = y - eps, y + eps + dmin, pnr = 1e20, -999 + + for j in range(num): + if ymin < pix[j].y < ymax and xmin < pix[j].x < xmax: + d = np.sqrt((x - pix[j].x) ** 2 + (y - pix[j].y) ** 2) + if d < dmin: + dmin, pnr = d, j + + return pnr + +def read_sortgrid_par(filename: str) -> int: + try: + with open(filename, 'r') as f: + eps = int(f.readline().strip()) + return eps + except Exception as e: + print(f"Error reading sortgrid parameter from {filename}: {e}") + return 0 + +def read_calblock(filename: str) -> Tuple[np.ndarray, int]: + try: + with open(filename, 'r') as f: + lines = f.readlines() + num_points = len(lines) + fix = np.empty(num_points, dtype=np.object) + for i, line in enumerate(lines): + parts = line.split() + fix[i] = {'x': float(parts[1]), 'y': float(parts[2]), 'z': float(parts[3])} + return fix, num_points + except Exception as e: + print(f"Can't open calibration block file: {filename}: {e}") + return None, 0 diff --git a/pyoptv/src/pyoptv/tests/test_epi_candidates.py b/pyoptv/src/pyoptv/tests/test_epi_candidates.py new file mode 100644 index 00000000..5a42e00d --- /dev/null +++ b/pyoptv/src/pyoptv/tests/test_epi_candidates.py @@ -0,0 +1,55 @@ +import pytest +from pyoptv.epi import find_candidate, Coord2D +from pyoptv.parameters import ControlPar, VolumePar, MMNP +from pyoptv.calibration import Calibration +from pyoptv.trafo import pixel_to_metric +def make_dummy_list(num, xmin, xmax, ymin, ymax): + # generate a sorted list of Coord2D points along x with y between bounds + pts = [] + step = (xmax - xmin) / (num - 1) + for i in range(num): + x = xmin + i * step + y = (ymin + ymax) / 2 + pt = Coord2D(pnr=i, x=x, y=y) + pts.append(pt) + return pts + +@pytest.fixture +def dummy_params(): + # minimal ControlPar and VolumePar to call find_candidate + cpar = ControlPar() + cpar.pix_x = 1.0; cpar.pix_y = 1.0; cpar.imx = 100; cpar.imy = 100 + vpar = VolumePar(); vpar.eps0 = 0.1; vpar.cn = vpar.cnx = vpar.cny = 0; vpar.csumg = -1 + calib = Calibration() + calib.int_par.xh = calib.int_par.yh = 0 + return cpar, vpar, calib + + +def test_out_of_bounds_returns_negative(dummy_params): + cpar, vpar, calib = dummy_params + coords = make_dummy_list(10, 0, 1, 0, 1) + pix = [None] * 10 # pix list unused if out-of-bounds + # set xa/xb so epipolar line lies entirely left of sensor + xa, ya, xb, yb = -200, -200, -150, -150 + count, cands = find_candidate(coords, pix, len(coords), xa, ya, xb, yb, 0,0,0,0, vpar, cpar, calib) + assert count == -1 + assert cands == [] + +@pytest.mark.parametrize("xa,xb,expected", [ + (0, 10, 10), (5, 15, 6), (10, 20, 0) +]) +def test_in_bounds_candidate_counts(dummy_params, xa, xb, expected): + cpar, vpar, calib = dummy_params + ymin, ymax = 0, 0 + coords = make_dummy_list(20, 0, 20, ymin, ymax) + # simple horizontal epipolar: y=0 + count, cands = find_candidate(coords, pix=[None]*20, num=20, + xa=xa, ya=0, xb=xb, yb=0, + n=1, nx=1, ny=1, sumg=1, + vpar=vpar, cpar=cpar, cal=calib) + # expected candidates are those with x in [xa, xb] + assert count == expected + assert len(cands) == expected + # check pnr indices correspond to list positions + for cand in cands: + assert xa - vpar.eps0 < coords[cand.pnr].x < xb + vpar.eps0 diff --git a/pyoptv/src/pyoptv/track.py b/pyoptv/src/pyoptv/track.py new file mode 100644 index 00000000..66c567fa --- /dev/null +++ b/pyoptv/src/pyoptv/track.py @@ -0,0 +1,702 @@ +import numpy as np +from typing import List, Optional, Sequence, Any +from .vec_utils import ( + Vec2D, Vec3D, vec_subt, vec_dot, vec_norm, vec_diff_norm, vec_scalar_mul, vec_set, vec_copy +) +from .tracking_frame_buf import ( + PathInfo, Corres, PREV_NONE, NEXT_NONE, PRIO_DEFAULT, Target, + fb_next, fb_prev, fb_write_frame_from_start, fb_read_frame_at_end, Frame, FrameBuffer +) +from .trafo import pixel_to_metric, metric_to_pixel, dist_to_flat +from .imgcoord import img_coord +from .orientation import point_position +from .tracking_run import TrackingRun +from .parameters import ControlPar, TrackPar +from .calibration import Calibration +from .constants import ( + TR_UNUSED, PT_UNUSED, COORD_UNUSED, ADD_PART, CORRES_NONE, MAX_CANDS, MAX_TARGETS +) + + +class FoundPix: + """Represents a found pixel candidate for tracking correspondence. + + Attributes: + ftnr: Feature/track number. + freq: Frequency of appearance across cameras. + whichcam: List indicating which cameras detected this candidate. + """ + ftnr: int + freq: int + whichcam: List[int] + + def __init__( + self, ftnr: int = TR_UNUSED, freq: int = 0, whichcam: Optional[List[int]] = None + ) -> None: + self.ftnr = ftnr + self.freq = freq + self.whichcam = [0, 0, 0, 0] if whichcam is None else whichcam + + +def register_link_candidate(path_info: PathInfo, fitness: float, cand: int) -> None: + """Register a candidate link in the path info structure.""" + path_info.decis[path_info.inlist] = fitness + path_info.linkdecis[path_info.inlist] = cand + path_info.inlist += 1 + + +def make_v2(num_cams: int) -> List[Vec2D]: + """Create a list of Vec2D initialized to zero for each camera.""" + return [Vec2D(0, 0) for _ in range(num_cams)] + + +def make_philf(num_cams: int) -> List[List[int]]: + """Create a 2D list for candidate indices, initialized to PT_UNUSED.""" + return [[PT_UNUSED for _ in range(MAX_CANDS)] for _ in range(num_cams)] + + +def reset_foundpix_array(arr: List[FoundPix], arr_len: int, num_cams: int) -> None: + """Reset an array of FoundPix objects to default values.""" + for i in range(arr_len): + arr[i].ftnr = TR_UNUSED + arr[i].freq = 0 + for cam in range(num_cams): + arr[i].whichcam[cam] = 0 + + +def copy_foundpix_array( + dest: List[FoundPix], src: List[FoundPix], arr_len: int, num_cams: int +) -> None: + """Copy an array of FoundPix objects from src to dest.""" + for i in range(arr_len): + dest[i].ftnr = src[i].ftnr + dest[i].freq = src[i].freq + for cam in range(num_cams): + dest[i].whichcam[cam] = src[i].whichcam[cam] + + +def register_closest_neighbs( + targets: List[Target], + num_targets: int, + cam: int, + cent_x: float, + cent_y: float, + dl: float, + dr: float, + du: float, + dd: float, + reg: List[FoundPix], + cpar: ControlPar, +) -> None: + """Register the closest neighbor candidates for a given camera and search region.""" + all_cands = np.zeros(MAX_CANDS, dtype=np.int32) + cand = candsearch_in_pix( + targets, num_targets, cent_x, cent_y, dl, dr, du, dd, all_cands, cpar + ) + for cand in range(MAX_CANDS): + if all_cands[cand] == -999: + reg[cand].ftnr = TR_UNUSED + else: + reg[cand].whichcam[cam] = 1 + reg[cand].ftnr = targets[all_cands[cand]].tnr + + +def search_volume_center_moving(prev_pos: Vec3D, curr_pos: Vec3D) -> Vec3D: + """Predict the next position in 3D by linear extrapolation.""" + output = vec_scalar_mul(curr_pos, 2) + return vec_subt(output, prev_pos) + + +def predict(prev_pos: Vec2D, curr_pos: Vec2D) -> Vec2D: + """Predict the next 2D position by linear extrapolation.""" + return Vec2D(2 * curr_pos.x - prev_pos.x, 2 * curr_pos.y - prev_pos.y) + + +def pos3d_in_bounds(pos: Vec3D, bounds: TrackPar) -> bool: + """Check if a 3D position is within the specified tracking bounds.""" + return ( + bounds.dvxmin < pos.x < bounds.dvxmax + and bounds.dvymin < pos.y < bounds.dvymax + and bounds.dvzmin < pos.z < bounds.dvzmax + ) + + +def angle_acc(start: Vec3D, pred: Vec3D, cand: Vec3D) -> tuple: + """Compute the angle and acceleration between predicted and candidate positions.""" + v0 = vec_subt(pred, start) + v1 = vec_subt(cand, start) + acc = vec_diff_norm(v0, v1) + if v0.x == -v1.x and v0.y == -v1.y and v0.z == -v1.z: + angle = 200.0 + elif v0.x == v1.x and v0.y == v1.y and v0.z == v1.z: + angle = 0.0 + else: + angle = (200.0 / np.pi) * np.arccos( + vec_dot(v0, v1) / vec_norm(v0) / vec_norm(v1) + ) + return angle, acc + + +def candsearch_in_pix( + next: List[Target], + num_targets: int, + cent_x: float, + cent_y: float, + dl: float, + dr: float, + du: float, + dd: float, + p: Any, + cpar: ControlPar, +) -> int: + """Search for up to four nearest candidate targets in a region.""" + j0 = num_targets // 2 + dj = num_targets // 4 + while dj > 1: + if next[j0].y < cent_y - du: + j0 += dj + else: + j0 -= dj + dj //= 2 + j0 -= 12 + if j0 < 0: + j0 = 0 + counter = 0 + p1 = p2 = p3 = p4 = PT_UNUSED + d1 = d2 = d3 = d4 = 1e20 + for j in range(j0, num_targets): + if next[j].tnr != TR_UNUSED: + if next[j].y > cent_y + dd: + break + if cent_x - dl < next[j].x < cent_x + dr and cent_y - du < next[j].y < cent_y + dd: + d = np.sqrt((cent_x - next[j].x) ** 2 + (cent_y - next[j].y) ** 2) + if d < d1: + p4, p3, p2, p1 = p3, p2, p1, j + d4, d3, d2, d1 = d3, d2, d1, d + elif d1 < d < d2: + p4, p3, p2 = p3, p2, j + d4, d3, d2 = d3, d2, d + elif d2 < d < d3: + p4, p3 = p3, j + d4, d3 = d3, d + elif d3 < d < d4: + p4 = j + d4 = d + p[0], p[1], p[2], p[3] = p1, p2, p3, p4 + for j in range(4): + if p[j] != PT_UNUSED: + counter += 1 + return counter + + +def candsearch_in_pix_rest( + next: List[Target], + num_targets: int, + cent_x: float, + cent_y: float, + dl: float, + dr: float, + du: float, + dd: float, + p: List[int], + cpar: ControlPar, +) -> int: + """Search for the nearest unused candidate target in a region. + + Args: + next: List of Target objects to search. + num_targets: Number of targets in the list. + cent_x: Center x coordinate of the search region. + cent_y: Center y coordinate of the search region. + dl, dr, du, dd: Search region bounds (left, right, up, down). + p: Output array (list or numpy array) to store found candidate indices. + cpar: ControlPar object with camera parameters. + + Returns: + The number of found candidates (0 or 1). + """ + # p is an output array (list or numpy array) where the index of the found candidate is stored. + j0 = num_targets // 2 + dj = num_targets // 4 + while dj > 1: + if next[j0].y < cent_y - du: + j0 += dj + else: + j0 -= dj + dj //= 2 + j0 -= 12 + if j0 < 0: + j0 = 0 + counter = 0 + dmin = 1e20 + p[0] = PT_UNUSED + for j in range(j0, num_targets): + if next[j].tnr == TR_UNUSED: + if next[j].y > cent_y + dd: + break + if cent_x - dl < next[j].x < cent_x + dr and cent_y - du < next[j].y < cent_y + dd: + d = np.sqrt((cent_x - next[j].x) ** 2 + (cent_y - next[j].y) ** 2) + if d < dmin: + dmin = d + p[0] = j + if p[0] != PT_UNUSED: + counter += 1 + return counter + + +def searchquader( + point: Vec3D, + xr: np.ndarray, + xl: np.ndarray, + yd: np.ndarray, + yu: np.ndarray, + tpar: TrackPar, + cpar: ControlPar, + cal: List[Calibration], +) -> None: + """Project a 3D cuboid (search volume) to image space and compute search bounds for each camera.""" + mins = vec_set(tpar.dvxmin, tpar.dvymin, tpar.dvzmin) + maxes = vec_set(tpar.dvxmax, tpar.dvymax, tpar.dvzmax) + quader = [vec_copy(point) for _ in range(8)] + for pt in range(8): + for dim in range(3): + if pt & (1 << dim): + quader[pt][dim] += maxes[dim] + else: + quader[pt][dim] += mins[dim] + for i in range(cpar.num_cams): + xr[i] = 0 + xl[i] = cpar.imx + yd[i] = 0 + yu[i] = cpar.imy + center = point_to_pixel(point, cal[i], cpar) + for pt in range(8): + corner = point_to_pixel(quader[pt], cal[i], cpar) + if corner.x < xl[i]: + xl[i] = corner.x + if corner.y < yu[i]: + yu[i] = corner.y + if corner.x > xr[i]: + xr[i] = corner.x + if corner.y > yd[i]: + yd[i] = corner.y + if xl[i] < 0: + xl[i] = 0 + if yu[i] < 0: + yu[i] = 0 + if xr[i] > cpar.imx: + xr[i] = cpar.imx + if yd[i] > cpar.imy: + yd[i] = cpar.imy + xr[i] -= center.x + xl[i] = center.x - xl[i] + yd[i] -= center.y + yu[i] = center.y - yu[i] + + +def sort_candidates_by_freq(item: List[FoundPix], num_cams: int) -> int: + """Sort candidate found pixels by frequency and return the number of unique candidates.""" + different = 0 + for i in range(num_cams * MAX_CANDS): + for j in range(num_cams): + for m in range(MAX_CANDS): + if item[i].ftnr == item[4 * j + m].ftnr: + item[i].whichcam[j] = 1 + for i in range(num_cams * MAX_CANDS): + for j in range(num_cams): + if item[i].whichcam[j] == 1 and item[i].ftnr != TR_UNUSED: + item[i].freq += 1 + for i in range(1, num_cams * MAX_CANDS): + for j in range(num_cams * MAX_CANDS - 1, i - 1, -1): + if item[j - 1].freq < item[j].freq: + item[j - 1], item[j] = item[j], item[j - 1] + for i in range(num_cams * MAX_CANDS): + for j in range(i + 1, num_cams * MAX_CANDS): + if item[i].ftnr == item[j].ftnr or item[j].freq < 2: + item[j].freq = 0 + item[j].ftnr = TR_UNUSED + for i in range(1, num_cams * MAX_CANDS): + for j in range(num_cams * MAX_CANDS - 1, i - 1, -1): + if item[j - 1].freq < item[j].freq: + item[j - 1], item[j] = item[j], item[j - 1] + for i in range(num_cams * MAX_CANDS): + if item[i].freq != 0: + different += 1 + return different + + +def sort(arr: List[Any]) -> List[Any]: + """Sort an array in place using bubble sort.""" + flag = 0 + while True: + flag = 0 + for i in range(len(arr) - 1): + if arr[i] > arr[i + 1]: + arr[i], arr[i + 1] = arr[i + 1], arr[i] + flag = 1 + if flag == 0: + break + + +def point_to_pixel(point: Vec3D, cal: Calibration, cpar: ControlPar) -> Vec2D: + """Project a 3D point to 2D pixel coordinates for a given camera.""" + x, y = img_coord(point, cal, cpar.mm) + return metric_to_pixel(x, y, cpar) + + +def sorted_candidates_in_volume( + center: Vec3D, + center_proj: List[Vec2D], + frm: Frame, + run: TrackingRun, +) -> Optional[List[FoundPix]]: + """Find and sort candidate targets in a 3D search volume across all cameras.""" + num_cams = frm.num_cams + points: List[FoundPix] = [FoundPix(TR_UNUSED, 0, [0] * num_cams) for _ in range(num_cams * MAX_CANDS)] + reset_foundpix_array(points, num_cams * MAX_CANDS, num_cams) + right = np.zeros(num_cams) + left = np.zeros(num_cams) + down = np.zeros(num_cams) + up = np.zeros(num_cams) + searchquader(center, right, left, down, up, run.tpar, run.cpar, run.cal) + for cam in range(num_cams): + register_closest_neighbs( + frm.targets[cam], + frm.num_targets[cam], + cam, + center_proj[cam].x, + center_proj[cam].y, + left[cam], + right[cam], + up[cam], + down[cam], + points[cam * MAX_CANDS :], + run.cpar, + ) + num_cands = sort_candidates_by_freq(points, num_cams) + if num_cands > 0: + return points[:num_cands] + else: + return None + + +def assess_new_position( + pos: Vec3D, + targ_pos: List[Vec2D], + cand_inds: np.ndarray, + frm: Frame, + run: TrackingRun, +) -> int: + """Assess the new 3D position by searching for corresponding 2D targets in all cameras. + + Args: + pos: The 3D position to assess. + targ_pos: List to store the corresponding 2D positions for each camera. + cand_inds: List of candidate indices for each camera. + frm: The current frame buffer containing target information (expected type: TrackingFrameBuf). + run: The tracking run configuration. + + Returns: + The number of valid cameras where a target was found. + """ + left = right = up = down = ADD_PART + for cam in range(run.cpar.num_cams): + targ_pos[cam] = Vec2D(COORD_UNUSED, COORD_UNUSED) + for cam in range(run.cpar.num_cams): + pixel = point_to_pixel(pos, run.cal[cam], run.cpar) + num_cands = candsearch_in_pix_rest( + frm.targets[cam], + frm.num_targets[cam], + pixel.x, + pixel.y, + left, + right, + up, + down, + cand_inds[cam], + run.cpar, + ) + if num_cands > 0: + _ix = cand_inds[cam][0] + targ_pos[cam] = Vec2D(frm.targets[cam][_ix].x, frm.targets[cam][_ix].y) + valid_cams = 0 + for cam in range(run.cpar.num_cams): + if targ_pos[cam].x != COORD_UNUSED and targ_pos[cam].y != COORD_UNUSED: + targ_pos[cam] = pixel_to_metric( + targ_pos[cam].x, targ_pos[cam].y, run.cpar + ) + targ_pos[cam] = dist_to_flat( + targ_pos[cam].x, targ_pos[cam].y, run.cal[cam], run.flatten_tol + ) + valid_cams += 1 + return valid_cams + + +def add_particle(frm: Frame, pos: Vec3D, cand_inds: np.ndarray) -> None: + """Add a new particle to the frame at the specified position.""" + num_parts = frm.num_parts + ref_path_inf = PathInfo(pos, PREV_NONE, NEXT_NONE, 0, np.zeros(MAX_CANDS), np.zeros(MAX_CANDS), 0) + frm.path_info.append(ref_path_inf) + ref_corres = Corres(num_parts, [CORRES_NONE] * frm.num_cams) + frm.correspond.append(ref_corres) + for cam in range(frm.num_cams): + if cand_inds[cam][0] != PT_UNUSED: + _ix = cand_inds[cam][0] + frm.targets[cam][_ix].tnr = num_parts + ref_corres.p[cam] = _ix + frm.num_parts += 1 + + +def trackcorr_c_loop(run_info: TrackingRun, step: int) -> None: + """Main tracking loop for updating particle correspondences between frames.""" + fb = run_info.fb + cal = run_info.cal + tpar = run_info.tpar + vpar = run_info.vpar + cpar = run_info.cpar + curr_targets = fb.buf[1].targets + orig_parts = fb.buf[1].num_parts + v2 = make_v2(fb.num_cams) + philf = make_philf(fb.num_cams) + for h in range(orig_parts): + curr_path_inf = fb.buf[1].path_info[h] + curr_corres = fb.buf[1].correspond[h] + curr_path_inf.inlist = 0 + X = [Vec3D(0, 0, 0) for _ in range(6)] + X[1] = curr_path_inf.x + if curr_path_inf.prev >= 0: + ref_path_inf = fb.buf[0].path_info[curr_path_inf.prev] + X[0] = ref_path_inf.x + X[2] = search_volume_center_moving(ref_path_inf.x, curr_path_inf.x) + v1 = [point_to_pixel(X[2], cal[j], cpar) for j in range(fb.num_cams)] + else: + X[2] = X[1] + v1 = [point_to_pixel(X[2], cal[j], cpar) if curr_corres.p[j] == CORRES_NONE else Vec2D(curr_targets[j][curr_corres.p[j]].x, curr_targets[j][curr_corres.p[j]].y) for j in range(fb.num_cams)] + w = sorted_candidates_in_volume(X[2], v1, fb.buf[2], run_info) + if w is None: + continue + mm = 0 + while w[mm].ftnr != TR_UNUSED: + ref_path_inf = fb.buf[2].path_info[w[mm].ftnr] + X[3] = ref_path_inf.x + if curr_path_inf.prev >= 0: + X[5] = Vec3D(0.5 * (5.0 * X[3].x - 4.0 * X[1].x + X[0].x), 0.5 * (5.0 * X[3].y - 4.0 * X[1].y + X[0].y), 0.5 * (5.0 * X[3].z - 4.0 * X[1].z + X[0].z)) + else: + X[5] = search_volume_center_moving(X[1], X[3]) + v1 = [point_to_pixel(X[5], cal[j], cpar) for j in range(fb.num_cams)] + wn = sorted_candidates_in_volume(X[5], v1, fb.buf[3], run_info) + if wn is not None: + kk = 0 + while wn[kk].ftnr != TR_UNUSED: + ref_path_inf = fb.buf[3].path_info[wn[kk].ftnr] + X[4] = ref_path_inf.x + diff_pos = vec_subt(X[4], X[3]) + if pos3d_in_bounds(diff_pos, tpar): + angle1, acc1 = angle_acc(X[3], X[4], X[5]) + if curr_path_inf.prev >= 0: + angle0, acc0 = angle_acc(X[1], X[2], X[3]) + else: + acc0, angle0 = acc1, angle1 + acc = (acc0 + acc1) / 2 + angle = (angle0 + angle1) / 2 + quali = wn[kk].freq + w[mm].freq + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + dl = (vec_diff_norm(X[1], X[3]) + vec_diff_norm(X[4], X[3])) / 2 + rr = (dl / run_info.lmax + acc / tpar.dacc + angle / tpar.dangle) / quali + register_link_candidate(curr_path_inf, rr, w[mm].ftnr) + kk += 1 + quali = assess_new_position(X[5], v2, philf, fb.buf[3], run_info) + if quali >= 2: + in_volume = 0 + dl = point_position(v2, cpar.num_cams, cpar.mm, cal, X[4]) + if vpar.X_lay[0] < X[4].x < vpar.X_lay[1] and run_info.ymin < X[4].y < run_info.ymax and vpar.Zmin_lay[0] < X[4].z < vpar.Zmax_lay[1]: + in_volume = 1 + diff_pos = vec_subt(X[3], X[4]) + if in_volume == 1 and pos3d_in_bounds(diff_pos, tpar): + angle, acc = angle_acc(X[3], X[4], X[5]) + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + dl = (vec_diff_norm(X[1], X[3]) + vec_diff_norm(X[4], X[3])) / 2 + rr = (dl / run_info.lmax + acc / tpar.dacc + angle / tpar.dangle) / (quali + w[mm].freq) + register_link_candidate(curr_path_inf, rr, w[mm].ftnr) + if tpar.add: + add_particle(fb.buf[3], X[4], philf) + num_added += 1 + if curr_path_inf.inlist == 0 and curr_path_inf.prev >= 0: + diff_pos = vec_subt(X[3], X[1]) + if pos3d_in_bounds(diff_pos, tpar): + angle, acc = angle_acc(X[1], X[2], X[3]) + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + quali = w[mm].freq + dl = (vec_diff_norm(X[1], X[3]) + vec_diff_norm(X[0], X[1])) / 2 + rr = (dl / run_info.lmax + acc / tpar.dacc + angle / tpar.dangle) / quali + register_link_candidate(curr_path_inf, rr, w[mm].ftnr) + mm += 1 + if tpar.add and curr_path_inf.inlist == 0 and curr_path_inf.prev >= 0: + quali = assess_new_position(X[2], v2, philf, fb.buf[2], run_info) + if quali >= 2: + X[3] = X[2] + in_volume = 0 + dl = point_position(v2, fb.num_cams, cpar.mm, cal, X[3]) + if vpar.X_lay[0] < X[3].x < vpar.X_lay[1] and run_info.ymin < X[3].y < run_info.ymax and vpar.Zmin_lay[0] < X[3].z < vpar.Zmax_lay[1]: + in_volume = 1 + diff_pos = vec_subt(X[2], X[3]) + if in_volume == 1 and pos3d_in_bounds(diff_pos, tpar): + angle, acc = angle_acc(X[1], X[2], X[3]) + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + dl = (vec_diff_norm(X[1], X[3]) + vec_diff_norm(X[0], X[1])) / 2 + rr = (dl / run_info.lmax + acc / tpar.dacc + angle / tpar.dangle) / quali + register_link_candidate(curr_path_inf, rr, fb.buf[2].num_parts) + add_particle(fb.buf[2], X[3], philf) + num_added += 1 + for h in range(fb.buf[1].num_parts): + curr_path_inf = fb.buf[1].path_info[h] + if curr_path_inf.inlist > 0: + sort(curr_path_inf.inlist, curr_path_inf.decis, curr_path_inf.linkdecis) + curr_path_inf.finaldecis = curr_path_inf.decis[0] + curr_path_inf.next = curr_path_inf.linkdecis[0] + count1 = 0 + for h in range(fb.buf[1].num_parts): + curr_path_inf = fb.buf[1].path_info[h] + if curr_path_inf.inlist > 0: + ref_path_inf = fb.buf[2].path_info[curr_path_inf.next] + if ref_path_inf.prev == PREV_NONE: + ref_path_inf.prev = h + else: + if fb.buf[1].path_info[ref_path_inf.prev].finaldecis > curr_path_inf.finaldecis: + fb.buf[1].path_info[ref_path_inf.prev].next = NEXT_NONE + ref_path_inf.prev = h + else: + curr_path_inf.next = NEXT_NONE + if curr_path_inf.next != NEXT_NONE: + count1 += 1 + print(f"step: {step}, curr: {fb.buf[1].num_parts}, next: {fb.buf[2].num_parts}, links: {count1}, lost: {fb.buf[1].num_parts - count1}, add: {num_added}") + run_info.npart += fb.buf[1].num_parts + run_info.nlinks += count1 + fb_next(fb) + fb_write_frame_from_start(fb, step) + if step < run_info.seq_par.last - 2: + fb_read_frame_at_end(fb, step + 3, 0) + +def trackcorr_c_finish(run_info: TrackingRun, step: int) -> None: + range_ = run_info.seq_par.last - run_info.seq_par.first + npart = run_info.npart / range_ + nlinks = run_info.nlinks / range_ + print(f"Average over sequence, particles: {npart:.1f}, links: {nlinks:.1f}, lost: {npart - nlinks:.1f}") + fb_next(run_info.fb) + fb_write_frame_from_start(run_info.fb, step) + +def trackback_c(run_info: TrackingRun) -> int: + seq_par = run_info.seq_par + tpar = run_info.tpar + vpar = run_info.vpar + cpar = run_info.cpar + fb = run_info.fb + cal = run_info.cal + v2 = make_v2(fb.num_cams) + philf = make_philf(fb.num_cams) + for step in range(seq_par.last, seq_par.last - 4, -1): + fb_read_frame_at_end(fb, step, 1) + fb_next(fb) + fb_prev(fb) + npart = nlinks = 0 + for step in range(seq_par.last - 1, seq_par.first, -1): + v2 = make_v2(fb.num_cams) + philf = make_philf(fb.num_cams) + for h in range(fb.buf[1].num_parts): + curr_path_inf = fb.buf[1].path_info[h] + if curr_path_inf.next < 0 or curr_path_inf.prev != -1: + continue + X = [Vec3D(0, 0, 0) for _ in range(6)] + curr_path_inf.inlist = 0 + X[1] = curr_path_inf.x + ref_path_inf = fb.buf[0].path_info[curr_path_inf.next] + X[0] = ref_path_inf.x + X[2] = search_volume_center_moving(ref_path_inf.x, curr_path_inf.x) + n = [point_to_pixel(X[2], cal[j], cpar) for j in range(fb.num_cams)] + w = sorted_candidates_in_volume(X[2], n, fb.buf[2], run_info) + if w is not None: + i = 0 + while w[i].ftnr != TR_UNUSED: + ref_path_inf = fb.buf[2].path_info[w[i].ftnr] + X[3] = ref_path_inf.x + diff_pos = vec_subt(X[1], X[3]) + if pos3d_in_bounds(diff_pos, tpar): + angle, acc = angle_acc(X[1], X[2], X[3]) + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + dl = (vec_diff_norm(X[1], X[3]) + vec_diff_norm(X[0], X[1])) / 2 + quali = w[i].freq + rr = (dl / run_info.lmax + acc / tpar.dacc + angle / tpar.dangle) / quali + register_link_candidate(curr_path_inf, rr, w[i].ftnr) + i += 1 + if tpar.add and curr_path_inf.inlist == 0: + quali = assess_new_position(X[2], v2, philf, fb.buf[2], run_info) + if quali >= 2: + in_volume = 0 + point_position(v2, fb.num_cams, cpar.mm, cal, X[3]) + if vpar.X_lay[0] < X[3].x < vpar.X_lay[1] and run_info.ymin < X[3].y < run_info.ymax and vpar.Zmin_lay[0] < X[3].z < vpar.Zmax_lay[1]: + in_volume = 1 + diff_pos = vec_subt(X[1], X[3]) + if in_volume == 1 and pos3d_in_bounds(diff_pos, tpar): + angle, acc = angle_acc(X[1], X[2], X[3]) + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + dl = (vec_diff_norm(X[1], X[3]) + vec_diff_norm(X[0], X[1])) / 2 + rr = (dl / run_info.lmax + acc / tpar.dacc + angle / tpar.dangle) / quali + register_link_candidate(curr_path_inf, rr, fb.buf[2].num_parts) + add_particle(fb.buf[2], X[3], philf) + for h in range(fb.buf[1].num_parts): + curr_path_inf = fb.buf[1].path_info[h] + if curr_path_inf.inlist > 0: + sort(curr_path_inf.inlist, curr_path_inf.decis, curr_path_inf.linkdecis) + count1 = num_added = 0 + for h in range(fb.buf[1].num_parts): + curr_path_inf = fb.buf[1].path_info[h] + if curr_path_inf.inlist > 0: + ref_path_inf = fb.buf[2].path_info[curr_path_inf.linkdecis[0]] + if ref_path_inf.prev == PREV_NONE and ref_path_inf.next == NEXT_NONE: + curr_path_inf.finaldecis = curr_path_inf.decis[0] + curr_path_inf.prev = curr_path_inf.linkdecis[0] + fb.buf[2].path_info[curr_path_inf.prev].next = h + num_added += 1 + if ref_path_inf.prev != PREV_NONE and ref_path_inf.next == NEXT_NONE: + X[0] = fb.buf[0].path_info[curr_path_inf.next].x + X[1] = curr_path_inf.x + X[3] = ref_path_inf.x + X[4] = fb.buf[3].path_info[ref_path_inf.prev].x + X[5] = Vec3D(0.5 * (5.0 * X[3].x - 4.0 * X[1].x + X[0].x), 0.5 * (5.0 * X[3].y - 4.0 * X[1].y + X[0].y), 0.5 * (5.0 * X[3].z - 4.0 * X[1].z + X[0].z)) + angle, acc = angle_acc(X[3], X[4], X[5]) + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + curr_path_inf.finaldecis = curr_path_inf.decis[0] + curr_path_inf.prev = curr_path_inf.linkdecis[0] + count1 = num_added = 0 + for h in range(fb.buf[1].num_parts): + curr_path_inf = fb.buf[1].path_info[h] + if curr_path_inf.inlist > 0: + ref_path_inf = fb.buf[2].path_info[curr_path_inf.linkdecis[0]] + if ref_path_inf.prev == PREV_NONE and ref_path_inf.next == NEXT_NONE: + curr_path_inf.finaldecis = curr_path_inf.decis[0] + curr_path_inf.prev = curr_path_inf.linkdecis[0] + fb.buf[2].path_info[curr_path_inf.prev].next = h + num_added += 1 + if ref_path_inf.prev != PREV_NONE and ref_path_inf.next == NEXT_NONE: + X[0] = fb.buf[0].path_info[curr_path_inf.next].x + X[1] = curr_path_inf.x + X[3] = ref_path_inf.x + X[4] = fb.buf[3].path_info[ref_path_inf.prev].x + X[5] = Vec3D(0.5 * (5.0 * X[3].x - 4.0 * X[1].x + X[0].x), 0.5 * (5.0 * X[3].y - 4.0 * X[1].y + X[0].y), 0.5 * (5.0 * X[3].z - 4.0 * X[1].z + X[0].z)) + angle, acc = angle_acc(X[3], X[4], X[5]) + if (acc < tpar.dacc and angle < tpar.dangle) or (acc < tpar.dacc / 10): + curr_path_inf.finaldecis = curr_path_inf.decis[0] + curr_path_inf.prev = curr_path_inf.linkdecis[0] + print(f"step: {step}, curr: {fb.buf[1].num_parts}, next: {fb.buf[2].num_parts}, links: {count1}, lost: {fb.buf[1].num_parts - count1}, add: {num_added}") + npart += fb.buf[1].num_parts + nlinks += count1 + fb_next(fb) + fb_write_frame_from_start(fb, step) + if step > seq_par.first + 2: + fb_read_frame_at_end(fb, step - 3, 1) + npart /= (seq_par.last - seq_par.first - 1) + nlinks /= (seq_par.last - seq_par.first - 1) + print(f"Average over sequence, particles: {npart:.1f}, links: {nlinks:.1f}, lost: {npart - nlinks:.1f}") + fb_next(fb) + fb_write_frame_from_start(fb, step) + return nlinks diff --git a/pyoptv/src/pyoptv/tracking_frame_buf.py b/pyoptv/src/pyoptv/tracking_frame_buf.py new file mode 100644 index 00000000..96fc6d40 --- /dev/null +++ b/pyoptv/src/pyoptv/tracking_frame_buf.py @@ -0,0 +1,424 @@ +# type: ignore +import numpy as np +from typing import Any, List +from .constants import MAX_TARGETS +from .constants import POSI, PREV_NONE, NEXT_NONE, PRIO_DEFAULT, MAX_TARGETS + +class Target: + """ + Represents a detected target (particle) in an image. + """ + pnr: int + x: float + y: float + n: int + nx: int + ny: int + sumg: float + tnr: int + + def __init__( + self, + pnr: int = -1, + x: float = 0.0, + y: float = 0.0, + n: int = 0, + nx: int = 0, + ny: int = 0, + sumg: float = 0.0, + tnr: int = -1 + ) -> None: + self.pnr = pnr + self.x = x + self.y = y + self.n = n + self.nx = nx + self.ny = ny + self.sumg = sumg + self.tnr = tnr + +class Corres: + """ + Correspondence information for a 3D particle across cameras. + """ + nr: int + p: List[int] + + def __init__(self, nr: int, p: List[int]) -> None: + self.nr = nr + self.p = p + +class PathInfo: + """ + Path information for a tracked particle. + """ + prev: int + next: int + prio: int + finaldecis: float + inlist: int + x: np.ndarray + decis: np.ndarray + linkdecis: np.ndarray + + def __init__(self, prev: int, next: int, prio: int, finaldecis: float, inlist: int, x: np.ndarray, decis: np.ndarray, linkdecis: np.ndarray) -> None: + self.prev = prev + self.next = next + self.prio = prio + self.finaldecis = finaldecis + self.inlist = inlist + self.x = x + self.decis = decis + self.linkdecis = linkdecis + + def register_link_candidate(self, fitness: float, cand: int) -> None: + """ + Register a candidate link in the path info structure. + """ + self.decis[self.inlist] = fitness + self.linkdecis[self.inlist] = cand + self.inlist += 1 + + def reset_links(self) -> None: + """ + Reset the link information for this path. + """ + self.prev = PREV_NONE + self.next = NEXT_NONE + self.prio = PRIO_DEFAULT + +class Frame: + """ + Represents a frame in the tracking buffer, holding all targets, correspondences, and path info. + """ + num_cams: int + max_targets: int + num_parts: int + path_info: List[PathInfo] + correspond: List[Corres] + targets: List[List[Target]] + num_targets: List[int] + + def __init__(self, num_cams: int, max_targets: int = None) -> None: + if max_targets is None: + max_targets = MAX_TARGETS + self.num_cams = num_cams + self.max_targets = max_targets + self.num_parts = 0 + self.path_info = [PathInfo(PREV_NONE, NEXT_NONE, PRIO_DEFAULT, 1000000.0, 0, np.zeros(3), np.zeros(POSI), np.zeros(POSI)) for _ in range(max_targets)] + self.correspond = [Corres(-1, [-1, -1, -1, -1]) for _ in range(max_targets)] + self.targets = [[Target(-1, 0, 0, 0, 0, 0, 0, -1) for _ in range(max_targets)] for _ in range(num_cams)] + self.num_targets = [0] * num_cams + + +def compare_targets(t1: Target, t2: Target) -> bool: + return ( + t1.pnr == t2.pnr and t1.x == t2.x and t1.y == t2.y and + t1.n == t2.n and t1.nx == t2.nx and t1.ny == t2.ny and + t1.sumg == t2.sumg and t1.tnr == t2.tnr + ) + +def read_targets(file_base: str, frame_num: int) -> List[Target]: + filein = f"{file_base}{frame_num:04d}_targets" if frame_num > 0 else f"{file_base}_targets" + try: + with open(filein, "r") as f: + num_targets = int(f.readline().strip()) + buffer = [] + for _ in range(num_targets): + data = list(map(float, f.readline().strip().split())) + buffer.append(Target(int(data[0]), data[1], data[2], int(data[3]), int(data[4]), int(data[5]), int(data[6]), int(data[7]))) + return buffer + except Exception as e: + print(f"Error reading file {filein}: {e}") + return -1 + +def write_targets(buffer: List[Target], num_targets: int, file_base: str, frame_num: int) -> int: + fileout = f"{file_base}{frame_num:04d}_targets" if frame_num > 0 else f"{file_base}_targets" + try: + with open(fileout, "w") as f: + f.write(f"{num_targets}\n") + for t in buffer: + f.write(f"{t.pnr} {t.x:.4f} {t.y:.4f} {t.n} {t.nx} {t.ny} {t.sumg} {t.tnr}\n") + return 1 + except Exception as e: + print(f"Error writing file {fileout}: {e}") + return 0 +def compare_corres(c1: Corres, c2: Corres) -> bool: + return (c1.nr == c2.nr and c1.p[0] == c2.p[0] and + c1.p[1] == c2.p[1] and c1.p[2] == c2.p[2] and + c1.p[3] == c2.p[3]) +def compare_path_info(p1: PathInfo, p2: PathInfo) -> bool: + if not (p1.prev == p2.prev and p1.next == p2.next and + p1.prio == p2.prio and p1.finaldecis == p2.finaldecis and + p1.inlist == p2.inlist and np.array_equal(p1.x, p2.x)): + return False + for iter in range(POSI): + if p1.decis[iter] != p2.decis[iter] or p1.linkdecis[iter] != p2.linkdecis[iter]: + return False + return True +def read_path_frame( + cor_buf: List[Corres], + path_buf: List[PathInfo], + corres_file_base: str, + linkage_file_base: str, + prio_file_base: str, + frame_num: int +) -> int: + """ + Read a path frame from disk into correspondence and path buffers. + """ + try: + with open(f"{corres_file_base}.{frame_num}", "r") as filein: + num_points = int(filein.readline().strip()) + if linkage_file_base: + with open(f"{linkage_file_base}.{frame_num}", "r") as linkagein: + linkagein.readline() + if prio_file_base: + with open(f"{prio_file_base}.{frame_num}", "r") as prioin: + prioin.readline() + for i in range(num_points): + if linkage_file_base: + linkage_data = list(map(float, linkagein.readline().strip().split())) + path_buf[i].prev = int(linkage_data[0]) + path_buf[i].next = int(linkage_data[1]) + else: + path_buf[i].prev = -1 + path_buf[i].next = -2 + if prio_file_base: + prio_data = list(map(float, prioin.readline().strip().split())) + path_buf[i].prio = int(prio_data[5]) + else: + path_buf[i].prio = 4 + path_buf[i].inlist = 0 + path_buf[i].finaldecis = 1000000.0 + path_buf[i].decis = np.zeros(POSI) + path_buf[i].linkdecis = np.full(POSI, -999) + cor_data = list(map(float, filein.readline().strip().split())) + path_buf[i].x = np.array(cor_data[1:4]) + cor_buf[i].p = list(map(int, cor_data[4:8])) + cor_buf[i].nr = i + return num_points + except Exception as e: + print(f"Error reading path frame: {e}") + return -1 + +def write_path_frame( + cor_buf: List[Corres], + path_buf: List[PathInfo], + num_parts: int, + corres_file_base: str, + linkage_file_base: str, + prio_file_base: str, + frame_num: int +) -> int: + """ + Write a path frame to disk from correspondence and path buffers. + """ + try: + with open(f"{corres_file_base}.{frame_num}", "w") as corres_file: + corres_file.write(f"{num_parts}\n") + if linkage_file_base: + with open(f"{linkage_file_base}.{frame_num}", "w") as linkage_file: + linkage_file.write(f"{num_parts}\n") + if prio_file_base: + with open(f"{prio_file_base}.{frame_num}", "w") as prio_file: + prio_file.write(f"{num_parts}\n") + for i in range(num_parts): + if linkage_file_base: + linkage_file.write(f"{path_buf[i].prev} {path_buf[i].next} {path_buf[i].x[0]:.3f} {path_buf[i].x[1]:.3f} {path_buf[i].x[2]:.3f}\n") + corres_file.write(f"{i + 1} {path_buf[i].x[0]:.3f} {path_buf[i].x[1]:.3f} {path_buf[i].x[2]:.3f} {cor_buf[i].p[0]} {cor_buf[i].p[1]} {cor_buf[i].p[2]} {cor_buf[i].p[3]}\n") + if prio_file_base: + prio_file.write(f"{path_buf[i].prev} {path_buf[i].next} {path_buf[i].x[0]:.3f} {path_buf[i].x[1]:.3f} {path_buf[i].x[2]:.3f} {path_buf[i].prio}\n") + return 1 + except Exception as e: + print(f"Error writing path frame: {e}") + return 0 + +def frame_init(new_frame: Frame, num_cams: int, max_targets: int) -> None: + """ + Initialize a Frame object with the given number of cameras and targets. + """ + new_frame.path_info = [PathInfo(PREV_NONE, NEXT_NONE, PRIO_DEFAULT, 1000000.0, 0, np.zeros(3), np.zeros(POSI), np.zeros(POSI)) for _ in range(max_targets)] + new_frame.correspond = [Corres(-1, [-1, -1, -1, -1]) for _ in range(max_targets)] + new_frame.targets = [[Target(-1, 0, 0, 0, 0, 0, 0, -1) for _ in range(max_targets)] for _ in range(num_cams)] + new_frame.num_targets = [0] * num_cams + new_frame.num_cams = num_cams + new_frame.max_targets = max_targets + new_frame.num_parts = 0 + +def free_frame(frame: Frame) -> None: + """ + Free the memory associated with a Frame object. + """ + frame.path_info = None + frame.correspond = None + frame.num_targets = None + frame.targets = None + +def read_frame( + frame: Frame, + corres_file_base: str, + linkage_file_base: str, + prio_file_base: str, + target_file_base: List[str], + frame_num: int +) -> int: + """ + Read a frame from disk into a Frame object. + """ + frame.num_parts = read_path_frame(frame.correspond, frame.path_info, corres_file_base, linkage_file_base, prio_file_base, frame_num) + if frame.num_parts == -1: + return 0 + for cam in range(frame.num_cams): + frame.num_targets[cam] = read_targets(target_file_base[cam], frame_num) + if frame.num_targets[cam] == -1: + return 0 + return 1 + +def write_frame( + self: Frame, + corres_file_base: str, + linkage_file_base: str, + prio_file_base: str, + target_file_base: List[str], + frame_num: int +) -> int: + """ + Write a Frame object to disk. + """ + status = write_path_frame(self.correspond, self.path_info, self.num_parts, corres_file_base, linkage_file_base, prio_file_base, frame_num) + if status == 0: + return 0 + for cam in range(self.num_cams): + status = write_targets(self.targets[cam], self.num_targets[cam], target_file_base[cam], frame_num) + if status == 0: + return 0 + return 1 + +class FrameBufferBase: + """ + Base class for frame buffer objects, implements ring buffer logic. + """ + def __init__(self, buf_len: int, num_cams: int, max_targets: int) -> None: + self.buf_len = buf_len + self.num_cams = num_cams + self._ring_vec = [Frame(num_cams, max_targets) for _ in range(buf_len * 2)] + self.buf = self._ring_vec[:buf_len] + self._vptr = None + + def free(self) -> None: + """ + Free the frame buffer. + """ + self._vptr.free(self) + + def read_frame_at_end(self, frame_num: int, read_links: bool) -> int: + """ + Read a frame at the end of the buffer. + """ + return self._vptr.read_frame_at_end(self, frame_num, read_links) + + def write_frame_from_start(self, frame_num: int) -> int: + """ + Write a frame from the start of the buffer. + """ + return self._vptr.write_frame_from_start(self, frame_num) + +def fb_base_init(new_buf: FrameBufferBase, buf_len: int, num_cams: int, max_targets: int) -> None: + """ + Initialize a FrameBufferBase object. + """ + new_buf.buf_len = buf_len + new_buf.num_cams = num_cams + new_buf._ring_vec = [Frame(num_cams, max_targets) for _ in range(buf_len * 2)] + new_buf.buf = new_buf._ring_vec[:buf_len] + new_buf._vptr = None + +def fb_base_free(fb: FrameBufferBase) -> None: + """ + Free the memory associated with a FrameBufferBase object. + """ + fb.buf = fb._ring_vec[:fb.buf_len] + for frame in fb.buf: + free_frame(frame) + fb.buf = None + fb._ring_vec = None + fb._vptr = None + +class FrameBuffer(FrameBufferBase): + """ + Frame buffer for tracking, with file I/O support. + """ + def __init__( + self, + buf_len: int, + num_cams: int, + max_targets: int, + corres_file_base: str, + linkage_file_base: str, + prio_file_base: str, + target_file_base: List[str] + ) -> None: + super().__init__(buf_len, num_cams, max_targets) + self.corres_file_base = corres_file_base + self.linkage_file_base = linkage_file_base + self.prio_file_base = prio_file_base + self.target_file_base = target_file_base + self._vptr = self + + def free(self) -> None: + """ + Free the frame buffer. + """ + fb_base_free(self) + + def read_frame_at_end(self, frame_num: int, read_links: bool) -> int: + """ + Read a frame at the end of the buffer. + """ + if read_links: + return read_frame(self.buf[-1], self.corres_file_base, self.linkage_file_base, self.prio_file_base, self.target_file_base, frame_num) + else: + return read_frame(self.buf[-1], self.corres_file_base, None, None, self.target_file_base, frame_num) + + def write_frame_from_start(self, frame_num: int) -> int: + """ + Write a frame from the start of the buffer. + """ + return write_frame(self.buf[0], self.corres_file_base, self.linkage_file_base, self.prio_file_base, self.target_file_base, frame_num) + + def fb_next(self) -> None: + """ + Advance the buffer to the next frame. + """ + self.buf = self.buf[1:] + self.buf[:1] + + def fb_prev(self) -> None: + """ + Move the buffer to the previous frame. + """ + self.buf = self.buf[-1:] + self.buf[:-1] + +def fb_write_frame_from_start(fb: FrameBuffer, frame_num: int) -> int: + """ + Write a frame from the start of the buffer using the FrameBuffer interface. + """ + return fb.write_frame_from_start(frame_num) + +def fb_read_frame_at_end(fb: FrameBuffer, frame_num: int, read_links: bool) -> int: + """ + Read a frame at the end of the buffer using the FrameBuffer interface. + """ + return fb.read_frame_at_end(frame_num, read_links) + +def fb_next(fb: FrameBuffer) -> None: + """ + Advance the buffer to the next frame (C API compatibility). + """ + fb.fb_next() + +def fb_prev(fb: FrameBuffer) -> None: + """ + Move the buffer to the previous frame (C API compatibility). + """ + fb.fb_prev() diff --git a/pyoptv/src/pyoptv/tracking_run.py b/pyoptv/src/pyoptv/tracking_run.py new file mode 100644 index 00000000..1cb1c9e2 --- /dev/null +++ b/pyoptv/src/pyoptv/tracking_run.py @@ -0,0 +1,109 @@ +# type: ignore +import numpy as np +from typing import List, Optional, Tuple +from .tracking_frame_buf import FrameBuffer +from .parameters import ( + SequencePar, TrackPar, VolumePar, ControlPar, + read_control_par, read_sequence_par, read_track_par, read_volume_par +) +from .calibration import Calibration + +class TrackingRun: + seq_par: SequencePar + tpar: TrackPar + vpar: VolumePar + cpar: ControlPar + cal: List[object] + flatten_tol: float + fb: FrameBuffer + lmax: float + ymax: float + ymin: float + npart: int + nlinks: int + + def __init__( + self, + seq_par: SequencePar, + tpar: TrackPar, + vpar: VolumePar, + cpar: ControlPar, + cal: List[Calibration], + buf_len: int, + max_targets: int, + corres_file_base: str, + linkage_file_base: str, + prio_file_base: str, + flatten_tol: float, + ) -> None: + self.seq_par = seq_par + self.tpar = tpar + self.vpar = vpar + self.cpar = cpar + self.cal = cal + self.flatten_tol = flatten_tol + self.fb = FrameBuffer( + buf_len, + cpar.num_cams, + max_targets, + corres_file_base, + linkage_file_base, + prio_file_base, + seq_par.img_base_name, + ) + self.lmax = np.linalg.norm([ + tpar.dvxmin - tpar.dvxmax, + tpar.dvymin - tpar.dvymax, + tpar.dvzmin - tpar.dvzmax, + ]) + self.ymax, self.ymin = self.volumedimension( + vpar.X_lay[1], vpar.X_lay[0], vpar.Zmax_lay[1], vpar.Zmin_lay[0], vpar, cpar, cal + ) + self.npart = 0 + self.nlinks = 0 + + def volumedimension( + self, + X_lay1: float, + X_lay0: float, + Zmax_lay1: float, + Zmin_lay0: float, + vpar: VolumePar, + cpar: ControlPar, + cal: List[object], + ) -> Tuple[float, float]: + # Placeholder for the actual implementation + return 0, 0 + + @staticmethod + def tr_new_legacy( + seq_par_fname: str, + tpar_fname: str, + vpar_fname: str, + cpar_fname: str, + cal: List[object], + ) -> 'TrackingRun': + cpar = read_control_par(cpar_fname) + seq_par = read_sequence_par(seq_par_fname, cpar.num_cams) + return TrackingRun( + seq_par, + read_track_par(tpar_fname), + read_volume_par(vpar_fname), + cpar, + cal, + 4, + 20000, + "res/rt_is", + "res/ptv_is", + "res/added", + 10000, + ) + + def tr_free(self) -> None: + del self.fb + del self.seq_par.img_base_name + del self.seq_par + del self.tpar + del self.vpar + del self.cpar + diff --git a/pyoptv/src/pyoptv/trafo.py b/pyoptv/src/pyoptv/trafo.py new file mode 100644 index 00000000..07d1b00e --- /dev/null +++ b/pyoptv/src/pyoptv/trafo.py @@ -0,0 +1,248 @@ +import numpy as np +from typing import Tuple, Any +from .parameters import ControlPar +from .calibration import ap_52, Calibration + + +MAX_ITER = 50 +DAMPING = 0.5 +TOL = 1e-8 + + +def old_pixel_to_metric( + x_pixel: float, + y_pixel: float, + im_size_x: float, + im_size_y: float, + pix_size_x: float, + pix_size_y: float, + y_remap_mode: int, +) -> Tuple[float, float]: + """ + Convert pixel coordinates to metric coordinates using legacy formula. + """ + if y_remap_mode == 1: + y_pixel = 2.0 * y_pixel + 1.0 + elif y_remap_mode == 2: + y_pixel *= 2.0 + + x_metric = (x_pixel - im_size_x / 2.0) * pix_size_x + y_metric = (im_size_y / 2.0 - y_pixel) * pix_size_y + + return x_metric, y_metric + + +def pixel_to_metric( + x_pixel: float, y_pixel: float, cpar: ControlPar +) -> Tuple[float, float]: + """ + Convert pixel coordinates to metric coordinates using camera parameters. + """ + return old_pixel_to_metric( + x_pixel, + y_pixel, + cpar.imx, + cpar.imy, + cpar.pix_x, + cpar.pix_y, + cpar.chfield, + ) + + +def old_metric_to_pixel( + x_metric: float, + y_metric: float, + im_size_x: float, + im_size_y: float, + pix_size_x: float, + pix_size_y: float, + y_remap_mode: int, +) -> Tuple[float, float]: + """ + Convert metric coordinates to pixel coordinates using legacy formula. + """ + x_pixel = (x_metric / pix_size_x) + (im_size_x / 2.0) + y_pixel = (im_size_y / 2.0) - (y_metric / pix_size_y) + + if y_remap_mode == 1: + y_pixel = (y_pixel - 1.0) / 2.0 + elif y_remap_mode == 2: + y_pixel /= 2.0 + + return x_pixel, y_pixel + + +def metric_to_pixel( + x_metric: float, y_metric: float, cpar: ControlPar +) -> Tuple[float, float]: + """ + Convert metric coordinates to pixel coordinates using camera parameters. + """ + return old_metric_to_pixel( + x_metric, + y_metric, + cpar.imx, + cpar.imy, + cpar.pix_x, + cpar.pix_y, + cpar.chfield, + ) + + +def distort_brown_affin(x: float, y: float, ap: ap_52) -> Tuple[float, float]: + """ + Apply Brown distortion and affine transformation to coordinates. + Args: + x: x coordinate in flat (undistorted) space + y: y coordinate in flat (undistorted) space + ap: ap_52 object containing distortion parameters + Returns: + Tuple[float, float]: distorted x and y coordinates + """ + r = np.sqrt(x * x + y * y) + if r < 1e-10: + return 0.0, 0.0 + + r2 = r * r + r4 = r2 * r2 + r6 = r4 * r2 + radial_factor = 1.0 + ap.k1 * r2 + ap.k2 * r4 + ap.k3 * r6 + + x_dist = x * radial_factor + ap.p1 * (r2 + 2 * x * x) + 2 * ap.p2 * x * y + y_dist = y * radial_factor + ap.p2 * (r2 + 2 * y * y) + 2 * ap.p1 * x * y + + sin_she = np.sin(ap.she) + cos_she = np.cos(ap.she) + + x1 = ap.scx * (x_dist - sin_she * y_dist) + y1 = ap.scx * cos_she * y_dist + + return x1, y1 + + +def correct_brown_affin(x: float, y: float, ap: ap_52) -> Tuple[float, float]: + """ + Corrects the distortion using the Brown model with affine transformation. + Args: + x: x coordinate in distorted space + y: y coordinate in distorted space + ap: ap_52 object containing the distortion parameters + Returns: + Tuple[float, float]: corrected x and y coordinates in flat (undistorted) space + """ + + + sin_she = np.sin(ap.she) + cos_she = np.cos(ap.she) + inv_scx = 1.0 / ap.scx + + xq = x * inv_scx + yq = y * inv_scx / cos_she + xq += yq * sin_she + + + + for _ in range(MAX_ITER): + xq_old = xq + yq_old = yq + + xt, yt = distort_brown_affin(xq, yq, ap) + + dx = (x - xt) * inv_scx + dy = (y - yt) * inv_scx + + xq += dx * DAMPING + yq += dy * DAMPING + + change = np.sqrt((xq - xq_old) ** 2 + (yq - yq_old) ** 2) + pos_magnitude = np.sqrt(xq * xq + yq * yq) + if pos_magnitude > 1e-10 and change / pos_magnitude < TOL: + break + + return xq, yq + + +def correct_brown_affine_exact( + x: float, y: float, ap: ap_52, tol: float +) -> Tuple[float, float]: + """ + Iteratively corrects Brown distortion and affine transformation to a given tolerance. + Args: + x: x coordinate in distorted space + y: y coordinate in distorted space + ap: ap_52 object containing the distortion parameters + tol: tolerance for convergence + Returns: + Tuple[float, float]: corrected x and y coordinates in flat (undistorted) space + """ + r_init = np.sqrt(x * x + y * y) + if r_init < 1e-10: + return 0.0, 0.0 + + sin_she = np.sin(ap.she) + cos_she = np.cos(ap.she) + inv_scx = 1.0 / ap.scx + + xq = (x + y * sin_she) * inv_scx + yq = y / cos_she + + + for _ in range(MAX_ITER): + r2 = xq * xq + yq * yq + r4 = r2 * r2 + r6 = r4 * r2 + + radial_factor = ap.k1 * r2 + ap.k2 * r4 + ap.k3 * r6 + + dx = xq * radial_factor + ap.p1 * (r2 + 2 * xq * xq) + 2 * ap.p2 * xq * yq + dy = yq * radial_factor + ap.p2 * (r2 + 2 * yq * yq) + 2 * ap.p1 * xq * yq + + xq_new = (x + y * sin_she) * inv_scx - dx + yq_new = y / cos_she - dy + + dx_change = xq_new - xq + dy_change = yq_new - yq + + xq += DAMPING * dx_change + yq += DAMPING * dy_change + + if np.sqrt(dx_change * dx_change + dy_change * dy_change) < tol: + break + + return xq, yq + + +def flat_to_dist(flat_x: float, flat_y: float, cal: Calibration) -> Tuple[float, float]: + """ + Convert flat (undistorted) coordinates to distorted coordinates using calibration. + Args: + flat_x: x coordinate in flat space + flat_y: y coordinate in flat space + cal: Calibration object + Returns: + Tuple[float, float]: distorted x and y coordinates + """ + flat_x += cal.int_par.xh + flat_y += cal.int_par.yh + + return distort_brown_affin(flat_x, flat_y, cal.added_par) + + +def dist_to_flat( + dist_x: float, dist_y: float, cal: Calibration, tol: float +) -> Tuple[float, float]: + """ + Convert distorted coordinates to flat (undistorted) coordinates using calibration. + Args: + dist_x: x coordinate in distorted space + dist_y: y coordinate in distorted space + cal: Calibration object + tol: tolerance for convergence + Returns: + Tuple[float, float]: flat (undistorted) x and y coordinates + """ + flat_x, flat_y = correct_brown_affine_exact(dist_x, dist_y, cal.added_par, tol) + flat_x -= cal.int_par.xh + flat_y -= cal.int_par.yh + + return flat_x, flat_y diff --git a/pyoptv/src/pyoptv/vec_utils.py b/pyoptv/src/pyoptv/vec_utils.py new file mode 100644 index 00000000..f5c9b9f3 --- /dev/null +++ b/pyoptv/src/pyoptv/vec_utils.py @@ -0,0 +1,66 @@ +import numpy as np +from typing import Union + +class Vec2D(np.ndarray): + def __new__(cls, input_array=None): + obj = np.asarray(input_array if input_array is not None else [np.nan, np.nan], dtype=float).view(cls) + if obj.shape != (2,): + raise ValueError("Vec2D must be of shape (2,)") + return obj + +class Vec3D(np.ndarray): + def __new__(cls, input_array=None): + obj = np.asarray(input_array if input_array is not None else [np.nan, np.nan, np.nan], dtype=float).view(cls) + if obj.shape != (3,): + raise ValueError("Vec3D must be of shape (3,)") + return obj + +EMPTY_CELL: float = np.nan + +def is_empty(val: float) -> bool: + return np.isnan(val) + +def norm(x: float, y: float, z: float) -> float: + return np.sqrt(x * x + y * y + z * z) + +def vec_init() -> Vec3D: + return np.full(3, EMPTY_CELL, dtype=float) + +def vec_set(x: float, y: float, z: float) -> Vec3D: + return np.array([x, y, z], dtype=float) + +def vec_copy(src: Vec3D) -> Vec3D: + return np.copy(src) + +def vec_subt(a: Vec3D, b: Vec3D) -> Vec3D: + return a - b + +def vec_add(a: Vec3D, b: Vec3D) -> Vec3D: + return a + b + +def vec_scalar_mul(a: Vec3D, scalar: float) -> Vec3D: + return a * scalar + +def vec_norm(a: Vec3D) -> float: + return np.linalg.norm(a) + +def vec_diff_norm(a: Vec3D, b: Vec3D) -> float: + return np.linalg.norm(a - b) + +def vec_dot(a: Vec3D, b: Vec3D) -> float: + return float(np.dot(a, b)) + +def vec_cross(a: Vec3D, b: Vec3D) -> Vec3D: + return np.cross(a, b) + +def vec_cmp(a: Vec3D, b: Vec3D) -> bool: + return np.array_equal(a, b) + +def vec_approx_cmp(a: Vec3D, b: Vec3D, tol: float) -> bool: + return np.allclose(a, b, atol=tol) + +def unit_vector(a: Vec3D) -> Vec3D: + n = np.linalg.norm(a) + if n == 0: + n = 1.0 + return a / n diff --git a/pyoptv/tests/conftest.py b/pyoptv/tests/conftest.py new file mode 100644 index 00000000..aa69fd16 --- /dev/null +++ b/pyoptv/tests/conftest.py @@ -0,0 +1,42 @@ +import pytest +import sys +import os +from pathlib import Path + +def find_project_root(): + """Find the project root by looking for pyproject.toml.""" + current = Path(__file__).parent + while current != current.parent: + if (current / "pyproject.toml").exists(): + return current + current = current.parent + return Path(__file__).parent.parent + +# Add project src directory to path for development testing +project_root = find_project_root() +src_path = project_root / "src" +if src_path.exists() and str(src_path) not in sys.path: + sys.path.insert(0, str(src_path)) + +@pytest.fixture +def test_data_dir(): + """Return the path to test data directory.""" + # Try multiple locations for testing_fodder + candidates = [ + Path(__file__).parent / "testing_fodder", + project_root / "testing_fodder", + project_root / "py_bind" / "testing_fodder", + project_root.parent / "py_bind" / "testing_fodder" + ] + + for candidate in candidates: + if candidate.exists(): + return candidate + + # If none found, return the expected location + return Path(__file__).parent / "testing_fodder" + +@pytest.fixture +def calibration_data_dir(test_data_dir): + """Return the calibration test data directory.""" + return test_data_dir / "calibration" diff --git a/pyoptv/tests/gen_track_data.py b/pyoptv/tests/gen_track_data.py new file mode 100644 index 00000000..291869c9 --- /dev/null +++ b/pyoptv/tests/gen_track_data.py @@ -0,0 +1,53 @@ +""" +Generate a 5-frame trajectory that is pretty degenerates so is good for +testing. It starts from (0,0,0) and moves in a straight line on the x axis, +at a slow velocity. +""" + +import numpy as np +from pyoptv.calibration import Calibration +from pyoptv.parameters import ControlParams +from pyoptv.imgcoord import image_coordinates +from pyoptv.transforms import convert_arr_metric_to_pixel + +num_cams = 3 +num_frames = 5 +velocity = 0.01 + +part_traject = np.zeros((num_frames,3)) +part_traject[:,0] = np.r_[:num_frames]*velocity + +# Find targets on each camera. +cpar = ControlParams(3) +cpar.read_control_par("testing_fodder/track/parameters/control_newpart.par") + +targs = [] +for cam in xrange(num_cams): + cal = Calibration() + cal.from_file( + "testing_fodder/cal/sym_cam%d.tif.ori" % (cam + 1), + "testing_fodder/cal/cam1.tif.addpar") + targs.append(convert_arr_metric_to_pixel(image_coordinates( + part_traject, cal, cpar.get_multimedia_params()), cpar)) + +for frame in xrange(num_frames): + # write 3D positions: + with open("testing_fodder/track/res_orig/particles.%d" % (frame + 1), "w") as outfile: + # Note correspondence to the single target in each frame. + outfile.writelines([ + str(1) + "\n", + "{:5d}{:10.3f}{:10.3f}{:10.3f}{:5d}{:5d}{:5d}{:5d}\n".format( + 1, part_traject[frame,0], part_traject[frame,1], + part_traject[frame,1], 0, 0, 0, 0)]) + + # write associated targets from all cameras: + for cam in xrange(num_cams): + with open("testing_fodder/track/newpart/cam%d.%04d_targets" \ + % (cam + 1, frame + 1), "w") as outfile: + outfile.writelines([ + str(1) + "\n", + "{:5d}{:10.3f}{:10.3f}{:5d}{:5d}{:5d}{:10d}{:5d}\n".format( + 0, targs[cam][frame, 0], targs[cam][frame, 1], + 100, 10, 10, 10000, 0)]) + +# That's all, folks! diff --git a/pyoptv/tests/test_basic_epipolar.py b/pyoptv/tests/test_basic_epipolar.py new file mode 100644 index 00000000..ac7f655d --- /dev/null +++ b/pyoptv/tests/test_basic_epipolar.py @@ -0,0 +1,103 @@ +import pytest +import numpy as np +from pyoptv.epi import epipolar_curve, find_candidate +from pyoptv.parameters import ControlPar, VolumePar +from pyoptv.calibration import Calibration +from pyoptv.trafo import metric_to_pixel +from dataclasses import dataclass + +@dataclass +class Coord2D: + pnr: int + x: float + y: float + +@pytest.fixture +def basic_calibration(): + cal1 = Calibration() + cal2 = Calibration() + + # Camera 1 at (100, 0, 1000), looking towards origin, rotated -atan(100/1000) around Y + cal1.ext_par.x0, cal1.ext_par.y0, cal1.ext_par.z0 = 100.0, 0.0, 1000.0 + cal1.ext_par.omega = 0.0 # rotation around X + cal1.ext_par.phi = -np.arctan2(100.0, 1000.0) # rotation around Y towards origin + cal1.ext_par.kappa = 0.0 # rotation around Z + + # Camera 2 at (-100, 0, 1000), looking towards origin, rotated +atan(100/1000) around Y + cal2.ext_par.x0, cal2.ext_par.y0, cal2.ext_par.z0 = -100.0, 0.0, 1000.0 + cal2.ext_par.omega = 0.0 + cal2.ext_par.phi = np.arctan2(100.0, 1000.0) # rotation around Y towards origin + cal2.ext_par.kappa = 0.0 + + cal1.int_par.cc = 1.0 + cal2.int_par.cc = 1.0 + + # Set a valid glass vector for both cameras + cal1.set_glass_vec(np.array([0.0, 0.0, 1.0])) + cal2.set_glass_vec(np.array([0.0, 0.0, 1.0])) + + return cal1, cal2 + +@pytest.fixture +def basic_parameters(): + cpar = ControlPar(num_cams=2) + cpar.set_image_size((256, 256)) + cpar.set_pixel_size((0.01, 0.01)) + + vpar = VolumePar() + vpar.X_lay = [-10.0, 10.0] + vpar.Zmin_lay = [-10, -10] + vpar.Zmax_lay = [10, 10] + + return cpar, vpar + +def test_epipolar_curve_basic(basic_calibration, basic_parameters): + cal1, cal2 = basic_calibration + cpar, vpar = basic_parameters + + # Known point in camera 1 + point_cam1 = (0.0, 0.0) + + # Compute epipolar curve in camera 2 + curve = epipolar_curve(point_cam1, cal1, cal2, 5, cpar, vpar) + + # Analytical solution for the epipolar curve + expected_curve = np.array([ + [0.0, -0.1], + [0.0, -0.05], + [0.0, 0.0], + [0.0, 0.05], + [0.0, 0.1], + ]) + + assert np.allclose(curve, expected_curve, atol=1e-6), "Epipolar curve does not match expected values" + +def test_find_candidate_basic(basic_calibration, basic_parameters): + cal1, cal2 = basic_calibration + cpar, vpar = basic_parameters + + # Known point in camera 1 + point_cam1 = (0.0, 0.0) + + # Compute epipolar curve in camera 2 + curve = epipolar_curve(point_cam1, cal1, cal2, 5, cpar, vpar) + + # Known candidates in camera 2 + candidates = [ + (0.0, -0.1), + (0.0, -0.05), + (0.0, 0.0), + (0.0, 0.05), + (0.0, 0.1), + ] + + # Convert candidates to Coord2D objects + metric_candidates = [Coord2D(pnr=i, x=x, y=y) for i, (x, y) in enumerate(candidates)] + + # Run find_candidate + count, found_candidates = find_candidate(metric_candidates, [], len(metric_candidates), + curve[0][0], curve[0][1], curve[-1][0], curve[-1][1], + 1, 1, 1, 1, vpar, cpar, cal2) + + assert count == len(candidates), "Number of candidates found does not match expected" + assert np.allclose(found_candidates, candidates, atol=1e-6), "Candidates do not match expected values" diff --git a/pyoptv/tests/test_calibration.py b/pyoptv/tests/test_calibration.py new file mode 100644 index 00000000..5764f9b1 --- /dev/null +++ b/pyoptv/tests/test_calibration.py @@ -0,0 +1,117 @@ +from pathlib import Path +import pytest +import numpy as np +from pyoptv.calibration import Calibration, Exterior, Interior, Glass, ap_52 +from pyoptv.calibration import read_ori + +# Helper to create a calibration object with example values +# matching those in the files read by test_read_ori + +def test_calibration_rotation_angles(): + # Test rotation matrices for omega, phi, kappa = pi/2 + from math import pi + ex = Exterior() + # omega + ex.omega = pi/2 + ex.phi = 0 + ex.kappa = 0 + ex.x0 = ex.y0 = ex.z0 = 0 + ex.dm = np.zeros((3,3)) + ex.update_rotation_matrix() + rotx = np.array([[1., 0., 0.], [0., 0., -1.], [0., 1., 0.]]) + assert np.allclose(ex.dm, rotx, atol=1e-6) + + # phi + ex = Exterior() + ex.omega = 0 + ex.phi = pi/2 + ex.kappa = 0 + ex.x0 = ex.y0 = ex.z0 = 0 + ex.dm = np.zeros((3,3)) + ex.update_rotation_matrix() + roty = np.array([[0., 0., 1.], [0., 1., 0.], [-1., 0., 0.]]) + assert np.allclose(ex.dm, roty, atol=1e-6) + + # kappa + ex = Exterior() + ex.omega = 0 + ex.phi = 0 + ex.kappa = pi/2 + ex.x0 = ex.y0 = ex.z0 = 0 + ex.dm = np.zeros((3,3)) + ex.update_rotation_matrix() + rotz = np.array([[0., -1., 0.], [1., 0., 0.], [0., 0., 1.]]) + assert np.allclose(ex.dm, rotz, atol=1e-6) + +@pytest.fixture(scope="session") +def testing_fodder_dir(): + """Fixture to provide the path to the testing_fodder directory inside pyoptv/tests.""" + return Path(__file__).parent / "testing_fodder" + +# Regression test for reading orientation files +def test_read_ori(testing_fodder_dir): + ori_file = testing_fodder_dir / "calibration" / "cam1.tif.ori" + add_file = testing_fodder_dir / "calibration" / "cam1.tif.addpar" + cal = read_ori(str(ori_file), str(add_file)) + # Compare to known correct values + assert np.isclose(cal.ext_par.x0, 105.2632, atol=1e-4) + assert np.isclose(cal.ext_par.y0, 102.7458, atol=1e-4) + assert np.isclose(cal.ext_par.z0, 403.8822, atol=1e-4) + assert np.isclose(cal.ext_par.omega, -0.2383291, atol=1e-6) + assert np.isclose(cal.ext_par.phi, 0.2442810, atol=1e-6) + assert np.isclose(cal.ext_par.kappa, 0.0552577, atol=1e-6) + assert np.allclose(cal.ext_par.dm, np.array([[0.9688305, -0.0535899, 0.2418587], + [-0.0033422, 0.9734041, 0.2290704], + [-0.2477021, -0.2227387, 0.9428845]]), atol=1e-6) + assert np.isclose(cal.int_par.xh, -2.4742, atol=1e-4) + assert np.isclose(cal.int_par.yh, 3.2567, atol=1e-4) + assert np.isclose(cal.int_par.cc, 100.0000, atol=1e-4) + assert np.isclose(cal.glass_par.vec_x, 0.0001, atol=1e-6) + assert np.isclose(cal.glass_par.vec_y, 0.00001, atol=1e-7) + assert np.isclose(cal.glass_par.vec_z, 150.0, atol=1e-4) + assert np.isclose(cal.added_par.k1, 0.0, atol=1e-6) + assert np.isclose(cal.added_par.k2, 0.0, atol=1e-6) + assert np.isclose(cal.added_par.k3, 0.0, atol=1e-6) + assert np.isclose(cal.added_par.p1, 0.0, atol=1e-6) + assert np.isclose(cal.added_par.p2, 0.0, atol=1e-6) + assert np.isclose(cal.added_par.scx, 1.0, atol=1e-6) + assert np.isclose(cal.added_par.she, 0.0, atol=1e-6) + +# Unit test for writing orientation files +def test_write_ori(tmp_path: Path): + # Create a calibration object with known values + ext = Exterior(105.2632, 102.7458, 403.8822, -0.2383291, 0.2442810, 0.0552577) + ext.update_rotation_matrix() + intp = Interior(-2.4742, 3.2567, 100.0) + glass = Glass(0.0001, 0.00001, 150.0) + addp = ap_52(0., 0., 0., 0., 0., 1., 0.) + cal = Calibration(ext_par = ext, int_par = intp, glass_par = glass, added_par = addp) + ori_file = tmp_path / "test.ori" + add_file = tmp_path / "test.addpar" + cal.write_ori(ori_file, add_file) + cal_read = read_ori(str(ori_file), str(add_file)) + # Use a compare_calib method if available, else compare fields + assert np.isclose(cal.ext_par.x0, cal_read.ext_par.x0, atol=1e-6) + assert np.isclose(cal.ext_par.y0, cal_read.ext_par.y0, atol=1e-6) + assert np.isclose(cal.ext_par.z0, cal_read.ext_par.z0, atol=1e-6) + assert np.isclose(cal.ext_par.omega, cal_read.ext_par.omega, atol=1e-6) + assert np.isclose(cal.ext_par.phi, cal_read.ext_par.phi, atol=1e-6) + assert np.isclose(cal.ext_par.kappa, cal_read.ext_par.kappa, atol=1e-6) + assert np.allclose(cal.ext_par.dm, cal_read.ext_par.dm, atol=1e-6) + assert np.isclose(cal.int_par.xh, cal_read.int_par.xh, atol=1e-6) + assert np.isclose(cal.int_par.yh, cal_read.int_par.yh, atol=1e-6) + assert np.isclose(cal.int_par.cc, cal_read.int_par.cc, atol=1e-6) + assert np.isclose(cal.glass_par.vec_x, cal_read.glass_par.vec_x, atol=1e-6) + assert np.isclose(cal.glass_par.vec_y, cal_read.glass_par.vec_y, atol=1e-6) + assert np.isclose(cal.glass_par.vec_z, cal_read.glass_par.vec_z, atol=1e-6) + assert np.isclose(cal.added_par.k1, cal_read.added_par.k1, atol=1e-6) + assert np.isclose(cal.added_par.k2, cal_read.added_par.k2, atol=1e-6) + assert np.isclose(cal.added_par.k3, cal_read.added_par.k3, atol=1e-6) + assert np.isclose(cal.added_par.p1, cal_read.added_par.p1, atol=1e-6) + assert np.isclose(cal.added_par.p2, cal_read.added_par.p2, atol=1e-6) + assert np.isclose(cal.added_par.scx, cal_read.added_par.scx, atol=1e-6) + assert np.isclose(cal.added_par.she, cal_read.added_par.she, atol=1e-6) + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/pyoptv/tests/test_correspondences.py b/pyoptv/tests/test_correspondences.py new file mode 100644 index 00000000..8d87909b --- /dev/null +++ b/pyoptv/tests/test_correspondences.py @@ -0,0 +1,382 @@ +from typing import List +import numpy as np +from pyoptv.epi import Coord2D +import pytest +from pyoptv.correspondences import ( + qs_target_y, + quicksort_target_y, + quicksort_coord2d_x, + quicksort_con, + match_pairs, + safely_allocate_adjacency_lists, + safely_allocate_target_usage_marks, + four_camera_matching, + three_camera_matching, + consistent_pair_matching, + correspondences, + Correspond, +) +from pyoptv.parameters import ControlPar, read_control_par, read_volume_par +from pyoptv.calibration import Calibration, read_ori +from pyoptv.imgcoord import img_coord +from pyoptv.trafo import metric_to_pixel, pixel_to_metric, dist_to_flat +from pathlib import Path +from pyoptv.tracking_frame_buf import Frame, Target + + +@pytest.fixture(scope="session") +def testing_fodder_dir(): + """Fixture to provide the path to the testing_fodder directory inside pyoptv/tests.""" + return Path(__file__).parent / "testing_fodder" + +# --- Sorting and utility tests --- +def test_qs_target_y(): + + test_pix = [ + Target(0, 0.0, -0.2, 5, 1, 2, 10, -999), + Target(6, 0.2, 0.0, 10, 8, 1, 20, -999), + Target(3, 0.2, 0.8, 10, 3, 3, 30, -999), + Target(4, 0.4, -1.1, 10, 3, 3, 40, -999), + Target(1, 0.7, -0.1, 10, 3, 3, 50, -999), + Target(7, 1.2, 0.3, 10, 3, 3, 60, -999), + Target(5, 10.4, 0.1, 10, 3, 3, 70, -999), + ] + qs_target_y(test_pix, 0, 6) + assert abs(test_pix[0].y + 1.1) < 1e-6 + assert abs(test_pix[1].y + 0.2) < 1e-6 + assert abs(test_pix[6].y - 0.8) < 1e-6 + +def test_quicksort_target_y(): + class Target: + def __init__(self, pnr, x, y, n, nx, ny, sumg, tnr): + self.pnr = pnr; self.x = x; self.y = y; self.n = n; self.nx = nx; self.ny = ny; self.sumg = sumg; self.tnr = tnr + test_pix = [ + Target(0, 0.0, -0.2, 5, 1, 2, 10, -999), + Target(6, 0.2, 0.0, 10, 8, 1, 20, -999), + Target(3, 0.2, 0.8, 10, 3, 3, 30, -999), + Target(4, 0.4, -1.1, 10, 3, 3, 40, -999), + Target(1, 0.7, -0.1, 10, 3, 3, 50, -999), + Target(7, 1.2, 0.3, 10, 3, 3, 60, -999), + Target(5, 10.4, 0.1, 10, 3, 3, 70, -999), + ] + num = len(test_pix) + quicksort_target_y(test_pix) + assert abs(test_pix[0].y + 1.1) < 1e-6 + assert abs(test_pix[1].y + 0.2) < 1e-6 + assert abs(test_pix[num-1].y - 0.8) < 1e-6 + +def test_quicksort_coord2d_x(): + class Coord2D: + def __init__(self, pnr, x, y): + self.pnr = pnr; self.x = x; self.y = y + test_crd = [ + Coord2D(0, 0.0, 0.0), + Coord2D(6, 0.1, 0.1), + Coord2D(3, 0.2, -0.8), + Coord2D(4, -0.4, -1.1), + Coord2D(1, 0.7, -0.1), + Coord2D(7, 1.2, 0.3), + Coord2D(5, 10.4, 0.1), + ] + num = len(test_crd) + quicksort_coord2d_x(test_crd) + assert abs(test_crd[0].x + 0.4) < 1e-6 + assert abs(test_crd[1].x - 0.0) < 1e-6 + assert abs(test_crd[num-1].x - 10.4) < 1e-6 + +def test_quicksort_con(): + class NTupel: + def __init__(self, p, corr): + self.p = p; self.corr = corr + test_con = [ + NTupel([0, 1, 2, 3], 0.1), + NTupel([0, 1, 2, 3], 0.2), + NTupel([0, 1, 2, 3], 0.15), + ] + quicksort_con(test_con) + assert abs(test_con[0].corr - 0.2) < 1e-6 + assert abs(test_con[2].corr - 0.1) < 1e-6 + + +@pytest.fixture(scope="session") +def testing_fodder_dir(): + """Fixture to provide the path to the testing_fodder directory inside pyoptv/tests.""" + return Path(__file__).parent / "testing_fodder" + +def read_all_calibration(cpar, testing_fodder_dir): + ori_tmpl = testing_fodder_dir / "cal" / "sym_cam{}.tif.ori" + added_name = testing_fodder_dir / "cal" / "cam1.tif.addpar" + calib = [Calibration() for _ in range(cpar.num_cams)] + for cam in range(cpar.num_cams): + ori_name = str(ori_tmpl).format(cam + 1) + calib[cam] = read_ori(ori_name, str(added_name)) + + return calib + +# Helper to generate a synthetic test set as in the C code +def generate_test_set(calib, cpar, vpar): + frm = Frame(cpar.num_cams) + frm.num_targets = [16 for _ in range(cpar.num_cams)] + frm.targets = [[Target() for _ in range(16)] for _ in range(cpar.num_cams)] + for cam in range(cpar.num_cams): + for cpt_horz in range(4): + for cpt_vert in range(4): + cpt_ix = cpt_horz * 4 + cpt_vert + if cam % 2: + cpt_ix = 15 - cpt_ix + targ = frm.targets[cam][cpt_ix] + targ.pnr = cpt_ix + tmp = np.array([cpt_vert * 10, cpt_horz * 10, 0.0]) + # Store pixel coordinates (not metric) in Target.x/y + x_metric, y_metric = img_coord(tmp, calib[cam], cpar.mm) + x_pix, y_pix = metric_to_pixel(x_metric, y_metric, cpar) + targ.x = x_pix + targ.y = y_pix + targ.n = 25 + targ.nx = 5 + targ.ny = 5 + targ.sumg = 10 + return frm + +def correct_frame(frm: Frame, calib: List[Calibration], cpar: ControlPar, tol: float) -> List[List[Coord2D]]: + corrected = [] + for cam in range(cpar.num_cams): + cam_corr = [] + for part in range(frm.num_targets[cam]): + c2d = Coord2D() + c2d.x, c2d.y = pixel_to_metric(frm.targets[cam][part].x, frm.targets[cam][part].y, cpar) + c2d.x, c2d.y = dist_to_flat(c2d.x, c2d.y, calib[cam], tol) + c2d.pnr = frm.targets[cam][part].pnr + cam_corr.append(c2d) + quicksort_coord2d_x(cam_corr) + corrected.append(cam_corr) + return corrected + +# --- Full correspondence and matching tests --- +def test_pairwise_matching(testing_fodder_dir): + cpar = read_control_par(str(testing_fodder_dir / "parameters" / "ptv.par")) + vpar = read_volume_par(str(testing_fodder_dir / "parameters" / "criteria.par")) + cpar.mm.n2[0] = 1.0001 + cpar.mm.n3 = 1.0001 + calib = read_all_calibration(cpar, testing_fodder_dir) + frm = generate_test_set(calib, cpar, vpar) + corrected = correct_frame(frm, calib, cpar, 0.0001) + lists = safely_allocate_adjacency_lists(cpar.num_cams, frm.num_targets) + match_pairs(lists, corrected, frm, vpar, cpar, calib) + + # Deep check: for every cam pair and every target, check the candidate list matches ground truth + for cam in range(cpar.num_cams - 1): + for subcam in range(cam + 1, cpar.num_cams): + for part in range(frm.num_targets[cam]): + # Compute the expected ground truth candidate for this cam/part/subcam + # The synthetic grid is 4x4, so pnr = cpt_ix = cpt_horz*4 + cpt_vert + # For odd cameras, the index is reversed + if (subcam % 2) == 0: + expected_pnr = part + else: + expected_pnr = 15 - part + # Find the index in corrected[subcam] with pnr == expected_pnr + expected_idx = None + for idx, c2d in enumerate(corrected[subcam]): + if c2d.pnr == expected_pnr: + expected_idx = idx + break + assert expected_idx is not None, f"Expected pnr {expected_pnr} not found in cam {subcam}" + # Now check that this index is present in the candidate list + candidate_indices = lists[cam][subcam][part].p2 + assert expected_idx in candidate_indices, ( + f"For cam {cam}, subcam {subcam}, part {part}: expected candidate idx {expected_idx} (pnr {expected_pnr}) not in candidates {candidate_indices}" + ) + # There should be exactly one candidate (the ground truth) + assert len(candidate_indices) == 1, ( + f"For cam {cam}, subcam {subcam}, part {part}: expected 1 candidate, got {len(candidate_indices)}: {candidate_indices}" + ) + +def test_four_camera_matching(testing_fodder_dir): + cpar = read_control_par(str(testing_fodder_dir / "parameters" / "ptv.par")) + vpar = read_volume_par(str(testing_fodder_dir / "parameters" / "criteria.par")) + cpar.mm.n2[0] = 1.0001 + cpar.mm.n3 = 1.0001 + + calib = read_all_calibration(cpar, testing_fodder_dir) + frm = generate_test_set(calib, cpar, vpar) + corrected = correct_frame(frm, calib, cpar, 0.0001) + lists = safely_allocate_adjacency_lists(cpar.num_cams, frm.num_targets) + match_pairs(lists, corrected, frm, vpar, cpar, calib) + from pyoptv.correspondences import NTupel + con = [NTupel() for _ in range(16)] + matched = four_camera_matching(lists, 16, 1.0, con, 16) + assert matched == 16 + +def test_three_camera_matching(testing_fodder_dir): + cpar = read_control_par(str(testing_fodder_dir / "parameters" / "ptv.par")) + vpar = read_volume_par(str(testing_fodder_dir / "parameters" / "criteria.par")) + cpar.mm.n2[0] = 1.0001 + cpar.mm.n3 = 1.0001 + calib = read_all_calibration(cpar, testing_fodder_dir) + frm = generate_test_set(calib, cpar, vpar) + for part in range(frm.num_targets[1]): + targ = frm.targets[1][part] + targ.n = 0; targ.nx = 0; targ.ny = 0; targ.sumg = 0 + corrected = correct_frame(frm, calib, cpar, 0.0001) + lists = safely_allocate_adjacency_lists(cpar.num_cams, frm.num_targets) + match_pairs(lists, corrected, frm, vpar, cpar, calib) + from pyoptv.correspondences import NTupel + con = [NTupel() for _ in range(4*16)] + tusage = safely_allocate_target_usage_marks(cpar.num_cams) + matched = three_camera_matching(lists, 4, frm.num_targets, 100000.0, con, 4*16, tusage) + assert matched == 16 + + +def test_two_camera_matching(testing_fodder_dir): + cpar = read_control_par(str(testing_fodder_dir / "parameters" / "ptv.par")) + vpar = read_volume_par(str(testing_fodder_dir / "parameters" / "criteria.par")) + cpar.mm.n2[0] = 1.0001 + cpar.mm.n3 = 1.0001 + vpar.Zmin_lay[0] = -1 + vpar.Zmin_lay[1] = -1 + vpar.Zmax_lay[0] = 1 + vpar.Zmax_lay[1] = 1 + calib = read_all_calibration(cpar, testing_fodder_dir) + frm = generate_test_set(calib, cpar, vpar) + cpar.num_cams = 2 + corrected = correct_frame(frm, calib, cpar, 0.0001) + lists = safely_allocate_adjacency_lists(cpar.num_cams, frm.num_targets) + match_pairs(lists, corrected, frm, vpar, cpar, calib) + from pyoptv.correspondences import NTupel + con = [NTupel() for _ in range(4*16)] + tusage = safely_allocate_target_usage_marks(cpar.num_cams) + matched = consistent_pair_matching(lists, 2, frm.num_targets, 10000.0, con, 4*16, tusage) + assert matched == 16 + + +def test_correspondences(testing_fodder_dir): + cpar = read_control_par(str(testing_fodder_dir / "parameters" / "ptv.par")) + vpar = read_volume_par(str(testing_fodder_dir / "parameters" / "criteria.par")) + cpar.mm.n2[0] = 1.0001 + cpar.mm.n3 = 1.0001 + calib = read_all_calibration(cpar, testing_fodder_dir) + frm = generate_test_set(calib, cpar, vpar) + corrected = correct_frame(frm, calib, cpar, 0.0001) + + con, match_counts = correspondences(frm, corrected, vpar, cpar, calib) + assert match_counts[0] == 16 + assert match_counts[1] == 0 + assert match_counts[2] == 0 + assert match_counts[3] == 16 + + def test_quicksort_con_with_correspond(): + # Create Correspond objects with varying corr values + c1 = Correspond(p1=0, n=0) + c1.corr[0] = 0.5 + c1.n = 1 + c2 = Correspond(p1=1, n=0) + c2.corr[0] = 0.8 + c2.n = 1 + c3 = Correspond(p1=2, n=0) + c3.corr[0] = 0.3 + c3.n = 1 + + # Set .corr attribute for sorting (simulate as in NTupel) + c1.corr = 0.5 + c2.corr = 0.8 + c3.corr = 0.3 + + con_list = [c1, c2, c3] + quicksort_con(con_list) + # Should be sorted descending by .corr + assert con_list[0].corr == 0.8 + assert con_list[1].corr == 0.5 + assert con_list[2].corr == 0.3 + + def test_quicksort_con_empty(): + con_list = [] + quicksort_con(con_list) # Should not raise + assert con_list == [] + + def test_quicksort_con_single_element(): + c = Correspond(p1=0, n=0) + c.corr = 1.23 + con_list = [c] + quicksort_con(con_list) + assert con_list[0].corr == 1.23 + + +def test_minimal_pairwise_matching(): + from pyoptv.parameters import ControlPar, VolumePar + from pyoptv.calibration import Calibration + from pyoptv.tracking_frame_buf import Frame, Target + from pyoptv.correspondences import match_pairs + import numpy as np + + # Minimal control and volume parameters + cpar = ControlPar(2) + cpar.set_image_size((1000, 1000)) + cpar.set_pixel_size((1.0, 1.0)) # 1mm per pixel + cpar.mm.n1 = 1.0 + cpar.mm.n2[0] = 1.0 + cpar.mm.n3 = 1.0 + + vpar = VolumePar() + vpar.X_lay = [-100, 100] + vpar.Zmin_lay = [-100, 100] + vpar.Zmax_lay = [-100, 100] + vpar.cnx = 0.0 + vpar.cny = 0.0 + vpar.cn = 0.0 + vpar.csumg = 0.0 + vpar.corrmin = 0.0 + vpar.eps0 = 1.0 # small positive value for epipolar band + + # Two simple calibrations: identity (no rotation/translation) + cal1 = Calibration() + cal2 = Calibration() + for cal in [cal1, cal2]: + cal.ext_par = np.zeros(6) + cal.int_par = np.zeros(7) + cal.added_par = np.zeros(5) + cal.glass_par = np.zeros(4) + cal.principal_point = (500, 500) + cal.cc = np.array([0.0, 0.0, 0.0]) + cal.ang = np.array([0.0, 0.0, 0.0]) + cal.k1 = 0.0 + cal.k2 = 0.0 + cal.k3 = 0.0 + cal.p1 = 0.0 + cal.p2 = 0.0 + cal.f = 1000.0 + cal.xh = 1000 + cal.yh = 1000 + cal.mmpx = 1.0 + cal.mmpy = 1.0 + + # Two targets in world space + world_targets = [np.array([10.0, 20.0, 30.0]), np.array([-10.0, -20.0, 30.0])] + + # Project to both cameras (no distortion, identity) + frames = [] + for cal in [cal1, cal2]: + frame = Frame(1) + for i, X in enumerate(world_targets): + # Simple pinhole: x = f*X/Z + cx, y = f*Y/Z + cy + x = cal.f * X[0] / X[2] + cal.principal_point[0] + y = cal.f * X[1] / X[2] + cal.principal_point[1] + t = Target() + t.pnr = i + t.x = x + t.y = y + frame.targets.append(t) + frames.append(frame) + + # Run pairwise matching (cam1 vs cam2) + pairs = match_pairs(frames[0].targets, frames[1].targets, cal1, cal2, cpar, vpar) + + # There should be two pairs, each matching the same pnr + assert len(pairs) == 2, f"Expected 2 pairs, got {len(pairs)}: {pairs}" + pnrs_0 = [p[0] for p in pairs] + pnrs_1 = [p[1] for p in pairs] + assert set(pnrs_0) == {0, 1}, f"Unexpected pnr0: {pnrs_0}" + assert set(pnrs_1) == {0, 1}, f"Unexpected pnr1: {pnrs_1}" + # Each pair should match the same pnr + for p0, p1 in pairs: + assert p0 == p1, f"Pair mismatch: {p0} != {p1}" \ No newline at end of file diff --git a/pyoptv/tests/test_epi.py b/pyoptv/tests/test_epi.py new file mode 100644 index 00000000..35d4261d --- /dev/null +++ b/pyoptv/tests/test_epi.py @@ -0,0 +1,225 @@ +import pytest +import numpy as np +from pyoptv.epi import epi_mm, epi_mm_2D, epipolar_curve, find_candidate +from pyoptv.calibration import Calibration, Exterior, Interior, Glass, ap_52 +from pyoptv.trafo import metric_to_pixel +from pyoptv.parameters import MMNP, ControlPar, VolumePar +from pyoptv.tracking_frame_buf import Target +from pyoptv.epi import Coord2D + +def test_epi_mm_2D(): + + test_cal = Calibration( + Exterior(0.0, 0.0, 100.0, 0.0, 0.0, 0.0), + Interior(0.0, 0.0, 100.0), + Glass(0.0, 0.0, 50.0), + ap_52(0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0) + ) + + test_mm = MMNP() + test_mm.nlay = 1 + test_mm.n1 = 1.0 + test_mm.n2 = [1.49, 0.0, 0.0] + test_mm.d = [5.0, 0.0, 0.0] + test_mm.n3 = 1.33 + + test_vpar = VolumePar() + test_vpar.X_lay = [-250.0, 250.0] + test_vpar.Zmin_lay = [-100.0, -100.0] + test_vpar.Zmax_lay = [100.0, 100.0] + test_vpar.cnx = 0.3 + test_vpar.cny = 0.3 + test_vpar.cn = 0.01 + test_vpar.csumg = 0.01 + test_vpar.corrmin = 33 + test_vpar.eps0 = 1.0 + + + x, y = 1.0, 10.0 + + out = epi_mm_2D(x, y, test_cal, test_mm, test_vpar) + + assert np.allclose(out, [0.85858163, 8.58581626, 0.0], atol=1e-5), f"Expected [0.85858163, 8.58581626, 0.0], but got {out}" + + x, y = 0.0, 0.0 + out = epi_mm_2D(x, y, test_cal, test_mm, test_vpar) + + assert np.allclose(out, [0.0, 0.0, 0.0], atol=1e-5), f"Expected [0.0, 0.0, 0.0], but got {out}" + +def test_epi_mm(): + test_cal_1 = Calibration( + Exterior(10.0, 0.0, 100.0, 0.0, -0.01, 0.0), + Interior(0.0, 0.0, 100.0), + Glass(0.0, 0.0, 50.0), + ap_52(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) + ) + + + test_cal_2 = Calibration( + Exterior(-10.0, 0.0, 100.0, 0.0, 0.01, 0.0), + Interior(0.0, 0.0, 100.0), + Glass(0.0, 0.0, 50.0), + ap_52(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) + ) + + test_mm = MMNP() + test_mm.nlay = 1 + test_mm.n1 = 1.0 + test_mm.n2 = [1.49, 0.0, 0.0] + test_mm.d = [5.0, 0.0, 0.0] + test_mm.n3 = 1.33 + + test_vpar = VolumePar() + test_vpar.X_lay = [-250.0, 250.0] + test_vpar.Zmin_lay = [-100.0, -100.0] + test_vpar.Zmax_lay = [100.0, 100.0] + test_vpar.cnx = 0.3 + test_vpar.cny = 0.3 + test_vpar.cn = 0.01 + test_vpar.csumg = 0.01 + test_vpar.corrmin = 33 + test_vpar.eps0 = 1.0 + + x, y = 10.0, 10.0 + xmin, xmax, ymin, ymax = epi_mm(x, y, test_cal_1, test_cal_2, test_mm, test_vpar) + + assert np.allclose([xmin, xmax, ymin, ymax], [26.44927852, 10.08218486, 51.60078764, 10.04378909], atol=1e-5), \ + f"Expected [26.44927852, 10.08218486, 51.60078764, 10.04378909], but got {[xmin, xmax, ymin, ymax]}" + + +def test_find_candidate(): + # Set of particles to choose from + test_pix = [ + Target(pnr=0, x=0.0, y=-0.2, sumg=5, nx=1, ny=2, n=10, tnr=-999), + Target(pnr=6, x=0.2, y=0.0, sumg=10, nx=8, ny=1, n=20, tnr=-999), + Target(pnr=3, x=0.2, y=0.8, sumg=10, nx=3, ny=3, n=30, tnr=-999), + Target(pnr=4, x=0.4, y=-1.1, sumg=10, nx=3, ny=3, n=40, tnr=-999), + Target(pnr=1, x=0.7, y=-0.1, sumg=10, nx=3, ny=3, n=50, tnr=-999), + Target(pnr=2, x=1.2, y=0.3, sumg=10, nx=3, ny=3, n=60, tnr=-999), + Target(pnr=5, x=10.4, y=0.1, sumg=10, nx=3, ny=3, n=70, tnr=-999) + ] + + num_pix = 7 # length of the test_pix + + # Coordinates of particles + test_crd = [ + Coord2D(pnr=6, x=0.1, y=0.1), + Coord2D(pnr=3, x=0.2, y=0.8), + Coord2D(pnr=4, x=0.4, y=-1.1), + Coord2D(pnr=1, x=0.7, y=-0.1), + Coord2D(pnr=2, x=1.2, y=0.3), + Coord2D(pnr=0, x=0.0, y=0.0), + Coord2D(pnr=5, x=10.4, y=0.1) + ] + + # Calibration parameters + test_cal = Calibration( + Exterior(0.0, 0.0, 100.0, 0.0, 0.0, 0.0), + Interior(0.0, 0.0, 100.0), + Glass(0.0, 0.0, 50.0), + ap_52(0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0) + ) + + + # Medium parameters + test_mm = MMNP( + nlay=1, + n1=1.0, + n2=[1.49, 0.0, 0.0], + d=[5.0, 0.0, 0.0], + n3=1.33 + ) + + test_vpar = VolumePar( + X_lay=[-250.0, 250.0], + Zmin_lay=[-100.0, -100.0], + Zmax_lay=[100.0, 100.0], + cnx=0.3, + cny=0.3, + cn=0.01, + csumg=0.01, + corrmin=33, + eps0=1.0 + ) + + + + # Control parameters + test_cpar = ControlPar(4) + + test_cpar.hp_flag=1 + test_cpar.allCam_flag=0 + test_cpar.tiff_flag=1 + test_cpar.imx=1280 + test_cpar.imy=1024 + test_cpar.pix_x=0.02 + test_cpar.pix_y=0.02 + test_cpar.chfield=0 + test_cpar.mm=test_mm + + + # Epipolar line + xa, ya, xb, yb = -10.0, -10.0, 10.0, 10.0 + + # Find candidates + candidates = find_candidate(test_crd, test_pix, xa, ya, xb, yb, test_vpar, test_cpar, test_cal) + + # Assertions + assert candidates[0].pnr == 0, f"Expected candidate with pnr=0, but got {candidates[0]['pnr']}" + assert candidates[0].tol < 1e-5, f"Expected tolerance < 1e-5, but got {candidates[0]['tol']}" + + sum_corr = sum(cand["corr"] for cand in candidates) + assert abs(sum_corr - 3301.0) < 1e-5, f"Expected sum_corr=3301.0, but got {sum_corr}" + assert len(candidates) == 5, f"Expected 5 candidates, but got {len(candidates)}" + assert abs(candidates[3]["tol"] - 0.636396) < 1e-5, f"Expected tol=0.636396 for candidate 3, but got {candidates[3]['tol']}" + +def test_epi_mm_perpendicular(): + # First camera + test_cal_1 = Calibration( + Exterior(0.0, 0.0, 100.0, 0.0, 0.0, 0.0), + Interior(0.0, 0.0, 100.0), + Glass(0.0, 0.0, 50.0), + ap_52(0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0) + ) + + # Second camera at small angle around y-axis + test_cal_2 = Calibration( + Exterior(100.0, 0.0, 100.0, 0.0, 1.57, 0.0), # 90 degrees around y-axis + Interior(0.0, 0.0, 100.0), + Glass(0.0, 0.0, 50.0), + ap_52(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0), + ) + + # Medium parameters + test_mm = MMNP( + nlay=1, + n1=1.0, + n2=[1.0, 0.0, 0.0], + d=[1.0, 0.0, 0.0], + n3=1.0 + ) + + # Volume parameters + test_vpar = VolumePar( + X_lay=[-250.0, 250.0], + Zmin_lay=[-100.0, -100.0], + Zmax_lay=[100.0, 100.0], + cnx=0.3, + cny=0.3, + cn=0.01, + csumg=0.01, + corrmin=33, + eps0=1.0 + ) + + + # Compute epipolar line + xmin, xmax, ymin, ymax = epipolar_curve(0.0, 0.0, test_cal_1, test_cal_2, test_mm, test_vpar) + + # Assertions + assert np.allclose([xmin, xmax, ymin, ymax], [-100.0, 0.0, 100.0, 0.0], atol=1e-5), \ + f"Expected [-100.0, 0.0, 100.0, 0.0], but got {[xmin, xmax, ymin, ymax]}" + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/pyoptv/tests/test_epi_candidates.py b/pyoptv/tests/test_epi_candidates.py new file mode 100644 index 00000000..40be81c8 --- /dev/null +++ b/pyoptv/tests/test_epi_candidates.py @@ -0,0 +1,56 @@ +import pytest +from pyoptv.epi import find_candidate, Coord2D +from pyoptv.parameters import ControlPar, VolumePar +from pyoptv.calibration import Calibration + +def make_dummy_list(num, xmin, xmax, ymin, ymax): + # generate a sorted list of Coord2D points along x with y between bounds + pts = [] + step = (xmax - xmin) / (num - 1) if num > 1 else 0 + for i in range(num): + x = xmin + i * step + y = (ymin + ymax) / 2 + pt = Coord2D(pnr=i, x=x, y=y) + pts.append(pt) + return pts + +@pytest.fixture +def dummy_params(): + cpar = ControlPar() + # set image pixel extents and size + cpar.pix_x = 1.0; cpar.pix_y = 1.0; cpar.imx = 100; cpar.imy = 100 + vpar = VolumePar(); vpar.eps0 = 0.1; vpar.cn = vpar.cnx = vpar.cny = 0; vpar.csumg = -1 + calib = Calibration(); calib.int_par.xh = calib.int_par.yh = 0 + return cpar, vpar, calib + + +def test_out_of_bounds_returns_negative(dummy_params): + cpar, vpar, calib = dummy_params + coords = make_dummy_list(10, 0, 1, 0, 1) + pix = [None] * 10 + # epipolar line entirely outside sensor region + xa, ya, xb, yb = -200, -200, -150, -150 + count, cands = find_candidate(coords, pix, len(coords), xa, ya, xb, yb, + n=0, nx=0, ny=0, sumg=0, + vpar=vpar, cpar=cpar, cal=calib) + assert count == -1 + assert cands == [] + +@pytest.mark.parametrize("xa,xb,expected", [ + (0, 10, 11), # endpoints inclusive with tol + (5, 15, 11), + (10, 20, 1), +]) +def test_in_bounds_candidate_counts(dummy_params, xa, xb, expected): + cpar, vpar, calib = dummy_params + # single horizontal epipolar at y=0 + coords = make_dummy_list(20, 0, 20, 0, 0) + pix = [type('T', (), {'n':1, 'nx':1, 'ny':1, 'sumg':1})() for _ in range(20)] + count, cands = find_candidate(coords, pix, len(coords), xa, 0, xb, 0, + n=1, nx=1, ny=1, sumg=1, + vpar=vpar, cpar=cpar, cal=calib) + assert count == expected + assert len(cands) == expected + for cand in cands: + x = coords[cand.pnr].x + assert xa - vpar.eps0 <= x <= xb + vpar.eps0 diff --git a/pyoptv/tests/test_epi_find_candidate.py b/pyoptv/tests/test_epi_find_candidate.py new file mode 100644 index 00000000..f502c56c --- /dev/null +++ b/pyoptv/tests/test_epi_find_candidate.py @@ -0,0 +1,52 @@ +import pytest +from pyoptv.epi import find_candidate, Candidate, Coord2D +from pyoptv.parameters import ControlPar, VolumePar +from pyoptv.calibration import Calibration + +class DummyPix: + n = 1 + nx = 1 + ny = 1 + sumg = 1 + +@pytest.fixture +def default_params(): + cpar = ControlPar(num_cams=1) + vpar = VolumePar() + cal = Calibration() + return cpar, vpar, cal + + +def test_find_candidate_out_of_bounds(default_params): + cpar, vpar, cal = default_params + # Empty coordinate and pixel lists + crd = [] + pix = [] + # Choose epipolar strip far outside the sensor extents + xa, ya, xb, yb = 1000.0, 1000.0, 1001.0, 1001.0 + count, candidates = find_candidate(crd, pix, num=0, + xa=xa, ya=ya, xb=xb, yb=yb, + n=1, nx=1, ny=1, sumg=1, + vpar=vpar, cpar=cpar, cal=cal) + assert count == -1, "Out-of-bounds epipolar strip should return -1" + assert candidates == [], "No candidates should be returned when out of bounds" + + +def test_find_candidate_in_bounds_no_hits(default_params): + cpar, vpar, cal = default_params + # Prepare a trivial single point list inside the sensor + # Pixel size=0.01, image dims=256 -> extents: ~[-1.28,1.28] + # Single point at (0,0) + crd = [Coord2D(pnr=0, x=0.0, y=0.0)] + # Pix entry must have n,nx,ny,sumg attributes + pix = [DummyPix()] + # Epipolar strip that includes (0,0) but no features on the line + xa, ya, xb, yb = 0.0, 0.0, 1.0, 1.0 + count, candidates = find_candidate(crd, pix, num=1, + xa=xa, ya=ya, xb=xb, yb=yb, + n=1, nx=1, ny=1, sumg=1, + vpar=vpar, cpar=cpar, cal=cal) + # With tol_band_width=0, only exact matches on the line get through + # Our DummyPix has minimal sumg ratios, so we expect zero candidates + assert count == 0, "No candidates should be returned when no hits found" + assert candidates == [], "Empty candidate list expected" diff --git a/pyoptv/tests/test_image_processing.py b/pyoptv/tests/test_image_processing.py new file mode 100644 index 00000000..ba28dd23 --- /dev/null +++ b/pyoptv/tests/test_image_processing.py @@ -0,0 +1,60 @@ +import pytest +import numpy as np +from pyoptv.image_processing import ( + filter_3, lowpass_3, fast_box_blur, split, subtract_img, subtract_mask, copy_images, prepare_image +) + +def test_filter_3(): + img = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + filt = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) + result = filter_3(img, filt) + expected = np.array([[8, 8, 8], [8, 5, 8], [8, 8, 8]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) + +def test_lowpass_3(): + img = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + result = lowpass_3(img) + expected = np.array([[8, 8, 8], [8, 5, 8], [8, 8, 8]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) + +def test_fast_box_blur(): + img = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + result = fast_box_blur(1, img) + expected = np.array([[3, 3, 3], [3, 5, 3], [3, 3, 3]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) + +def test_split(): + img = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + result = split(img, 1) + expected = np.array([[1, 2, 3], [7, 8, 9], [2, 2, 2]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) + +def test_subtract_img(): + img1 = np.array([[5, 6, 7], [8, 9, 10], [11, 12, 13]], dtype=np.uint8) + img2 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + result = subtract_img(img1, img2) + expected = np.array([[4, 4, 4], [4, 4, 4], [4, 4, 4]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) + +def test_subtract_mask(): + img = np.array([[5, 6, 7], [8, 9, 10], [11, 12, 13]], dtype=np.uint8) + img_mask = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=np.uint8) + result = subtract_mask(img, img_mask) + expected = np.array([[5, 0, 7], [0, 9, 0], [11, 0, 13]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) + +def test_copy_images(): + src = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + result = copy_images(src) + expected = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) + +def test_prepare_image(): + img = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8) + dim_lp = 1 + filter_hp = 1 + filter_file = None + cpar = {'chfield': 0} + result = prepare_image(img, dim_lp, filter_hp, filter_file, cpar) + expected = np.array([[3, 3, 3], [3, 5, 3], [3, 3, 3]], dtype=np.uint8) + np.testing.assert_array_equal(result, expected) diff --git a/pyoptv/tests/test_imgcoord.py b/pyoptv/tests/test_imgcoord.py new file mode 100644 index 00000000..bb26a801 --- /dev/null +++ b/pyoptv/tests/test_imgcoord.py @@ -0,0 +1,50 @@ +import pytest +import numpy as np +from pyoptv.imgcoord import flat_image_coord, img_coord, flat_to_dist +from pyoptv.calibration import Calibration +from pyoptv.parameters import MMNP + + +def test_flat_image_coord(): + pos = np.array([1.0, 2.0, 3.0]) + cal = Calibration() + # Set up a simple camera with identity rotation and principal distance 3.0 + cal.ext_par.dm = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + cal.ext_par.x0 = 0.0 + cal.ext_par.y0 = 0.0 + cal.ext_par.z0 = 0.0 + cal.int_par.cc = 3.0 + # mm is now a valid MMNP instance (all air) + mm = MMNP() + x, y = flat_image_coord(pos, cal, mm) + assert np.isclose(x, -1.0) + assert np.isclose(y, -2.0) + + +def test_img_coord(): + pos = np.array([1.0, 2.0, 3.0]) + cal = Calibration() + cal.ext_par.dm = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + cal.ext_par.x0 = 0.0 + cal.ext_par.y0 = 0.0 + cal.ext_par.z0 = 0.0 + cal.int_par.cc = 3.0 + # Set distortion parameters to zero + cal.set_radial_distortion(np.array([0.0, 0.0, 0.0])) + mm = MMNP() + x, y = img_coord(pos, cal, mm) + assert np.isclose(x, -1.0) + assert np.isclose(y, -2.0) + + +def test_flat_to_dist(): + x, y = 1.0, 2.0 + cal = Calibration() + cal.set_radial_distortion(np.array([0.0, 0.0, 0.0])) + x_dist, y_dist = flat_to_dist(x, y, cal) + assert np.isclose(x_dist, 1.0) + assert np.isclose(y_dist, 2.0) + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/pyoptv/tests/test_lsqadj.py b/pyoptv/tests/test_lsqadj.py new file mode 100644 index 00000000..61fa10c6 --- /dev/null +++ b/pyoptv/tests/test_lsqadj.py @@ -0,0 +1,54 @@ +import pytest +import numpy as np +from pyoptv.lsqadj import ata, atl, matinv, matmul + +def test_ata(): + a = np.array([[1, 0, 1], [2, 2, 4], [1, 2, 3], [2, 4, 3]]) + expected = np.array([[10, 14, 18], [14, 24, 26], [18, 26, 35]]) + result = ata(a, 4, 3, 3) + assert np.allclose(result, expected) + +def test_atl(): + a = np.array([[1, 0, 1], [2, 2, 4], [1, 2, 3], [2, 4, 3]]) + l = np.array([1, 2, 3, 4]) + expected = np.array([16, 26, 30]) + result = atl(a, l, 4, 3, 3) + assert np.allclose(result, expected) + +def test_matinv(): + a = np.array([[1, 2, 3], [0, 4, 5], [1, 0, 6]]) + expected = np.array([[1.090909, -0.545455, -0.090909], [0.227273, 0.136364, -0.227273], [-0.181818, 0.090909, 0.181818]]) + result = matinv(a, 3, 3) + assert np.allclose(result, expected) + +def test_matmul(): + a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + b = np.array([[10, 11], [12, 13], [14, 15]]) + expected = np.array([[76, 82], [184, 199], [292, 316]]) + result = matmul(a, b, 3, 3, 2, 3, 3) + assert np.allclose(result, expected) + + +def test_matmul2(): + # Test 1: 2x3 @ 3x2 + a = np.array([[1, 2, 3], [4, 5, 6]]) + b = np.array([[7, 8], [9, 10], [11, 12]]) + m, n, k = 2, 3, 2 + m_large, n_large = 3, 3 + result = matmul(a, b, m, n, k, m_large, n_large) + expected = np.dot(a, b) + assert np.allclose(result, expected), f"Test 1 failed: {result} != {expected}" + + # Test 2: 3x2 @ 2x1 + a = np.array([[1, 2], [3, 4], [5, 6]]) + b = np.array([[7], [8]]) + m, n, k = 3, 2, 1 + m_large, n_large = 2, 2 + result = matmul(a, b, m, n, k, m_large, n_large) + expected = np.dot(a, b) + assert np.allclose(result, expected), f"Test 2 failed: {result} != {expected}" + + print("All matmul tests passed.") + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/pyoptv/tests/test_multimed.py b/pyoptv/tests/test_multimed.py new file mode 100644 index 00000000..e2babfc0 --- /dev/null +++ b/pyoptv/tests/test_multimed.py @@ -0,0 +1,230 @@ +import pytest +import numpy as np +from pyoptv.multimed import ( + get_mmf_from_mmlut, multimed_nlay, multimed_r_nlay, trans_Cam_Point, + back_trans_Point, move_along_ray, init_mmlut, volumedimension +) + +def test_get_mmf_from_mmlut(): + class MMLUT: + def __init__(self): + self.rw = 1.0 + self.origin = np.array([0.0, 0.0, 0.0]) + self.nz = 2 + self.nr = 2 + self.data = np.array([[1.0, 2.0], [3.0, 4.0]]) + + class Calibration: + def __init__(self): + self.mmlut = MMLUT() + + cal = Calibration() + pos = np.array([0.5, 0.5, 0.5]) + result = get_mmf_from_mmlut(cal, pos) + assert result == 2.5 + +def test_multimed_nlay(): + class MM: + def __init__(self): + self.n1 = 1 + self.nlay = 1 + self.n2 = [1] + self.n3 = 1 + + class ExtPar: + def __init__(self): + self.x0 = 0.0 + self.y0 = 0.0 + self.z0 = 0.0 + + class Calibration: + def __init__(self): + self.ext_par = ExtPar() + self.mmlut = None + + cal = Calibration() + mm = MM() + pos = np.array([1.0, 1.0, 1.0]) + Xq, Yq = multimed_nlay(cal, mm, pos) + assert Xq == 1.0 + assert Yq == 1.0 + +def test_multimed_r_nlay(): + class MM: + def __init__(self): + self.n1 = 1 + self.nlay = 1 + self.n2 = [1] + self.n3 = 1 + + class Calibration: + def __init__(self): + self.mmlut = None + + cal = Calibration() + mm = MM() + pos = np.array([1.0, 1.0, 1.0]) + result = multimed_r_nlay(cal, mm, pos) + assert result == 1.0 + +def test_trans_Cam_Point(): + class ExtPar: + def __init__(self): + self.x0 = 0.0 + self.y0 = 0.0 + self.z0 = 0.0 + + class Glass: + def __init__(self): + self.vec_x = 1.0 + self.vec_y = 1.0 + self.vec_z = 1.0 + + class MM: + def __init__(self): + self.d = [1.0] + + ex = ExtPar() + mm = MM() + gl = Glass() + pos = np.array([1.0, 1.0, 1.0]) + ex_t, pos_t, cross_p, cross_c = trans_Cam_Point(ex, mm, gl, pos) + assert ex_t.x0 == 0.0 + assert ex_t.y0 == 0.0 + assert ex_t.z0 == 1.0 + assert np.allclose(pos_t, np.array([0.0, 0.0, 0.0])) + +def test_back_trans_Point(): + class Glass: + def __init__(self): + self.vec_x = 1.0 + self.vec_y = 1.0 + self.vec_z = 1.0 + + class MM: + def __init__(self): + self.d = [1.0] + + pos_t = np.array([1.0, 1.0, 1.0]) + mm = MM() + G = Glass() + cross_p = np.array([1.0, 1.0, 1.0]) + cross_c = np.array([1.0, 1.0, 1.0]) + result = back_trans_Point(pos_t, mm, G, cross_p, cross_c) + assert np.allclose(result, np.array([0.0, 0.0, 0.0])) + +def test_move_along_ray(): + glob_Z = 1.0 + vertex = np.array([0.0, 0.0, 0.0]) + direct = np.array([1.0, 1.0, 1.0]) + result = move_along_ray(glob_Z, vertex, direct) + assert np.allclose(result, np.array([1.0, 1.0, 1.0])) + +def test_init_mmlut(): + class VPar: + def __init__(self): + self.Zmin_lay = [0.0] + self.Zmax_lay = [1.0] + + class CPar: + def __init__(self): + self.imx = 1.0 + self.imy = 1.0 + self.mm = None + + class ExtPar: + def __init__(self): + self.x0 = 0.0 + self.y0 = 0.0 + self.z0 = 0.0 + + class IntPar: + def __init__(self): + self.xh = 0.0 + self.yh = 0.0 + + class GlassPar: + def __init__(self): + self.vec_x = 1.0 + self.vec_y = 1.0 + self.vec_z = 1.0 + + class AddedPar: + def __init__(self): + pass + + class MMLUT: + def __init__(self): + self.data = None + + class Calibration: + def __init__(self): + self.ext_par = ExtPar() + self.int_par = IntPar() + self.glass_par = GlassPar() + self.added_par = AddedPar() + self.mmlut = MMLUT() + + vpar = VPar() + cpar = CPar() + cal = Calibration() + init_mmlut(vpar, cpar, cal) + assert cal.mmlut.data is not None + +def test_volumedimension(): + class VPar: + def __init__(self): + self.Zmin_lay = [0.0] + self.Zmax_lay = [1.0] + + class CPar: + def __init__(self): + self.imx = 1.0 + self.imy = 1.0 + self.num_cams = 1 + self.mm = None + + class ExtPar: + def __init__(self): + self.x0 = 0.0 + self.y0 = 0.0 + self.z0 = 0.0 + + class IntPar: + def __init__(self): + self.xh = 0.0 + self.yh = 0.0 + + class GlassPar: + def __init__(self): + self.vec_x = 1.0 + self.vec_y = 1.0 + self.vec_z = 1.0 + + class AddedPar: + def __init__(self): + pass + + class MMLUT: + def __init__(self): + self.data = None + + class Calibration: + def __init__(self): + self.ext_par = ExtPar() + self.int_par = IntPar() + self.glass_par = GlassPar() + self.added_par = AddedPar() + self.mmlut = MMLUT() + + vpar = VPar() + cpar = CPar() + cal = [Calibration()] + xmax, xmin, ymax, ymin, zmax, zmin = [0.0], [0.0], [0.0], [0.0], [0.0], [0.0] + volumedimension(xmax, xmin, ymax, ymin, zmax, zmin, vpar, cpar, cal) + assert xmax[0] == 0.0 + assert xmin[0] == 0.0 + assert ymax[0] == 0.0 + assert ymin[0] == 0.0 + assert zmax[0] == 1.0 + assert zmin[0] == 0.0 diff --git a/pyoptv/tests/test_orientation.py b/pyoptv/tests/test_orientation.py new file mode 100644 index 00000000..b51f354c --- /dev/null +++ b/pyoptv/tests/test_orientation.py @@ -0,0 +1,94 @@ +import pytest +import numpy as np +from pyoptv.orientation import ( + skew_midpoint, point_position, weighted_dumbbell_precision, num_deriv_exterior, + orient, raw_orient, read_man_ori_fix, read_orient_par, OrientPar +) +from pyoptv.ray_tracing import ray_tracing + +def test_skew_midpoint(): + vert1 = np.array([0, 0, 0]) + direct1 = np.array([1, 0, 0]) + vert2 = np.array([0, 1, 0]) + direct2 = np.array([0, 1, 0]) + dist, res = skew_midpoint(vert1, direct1, vert2, direct2) + assert np.isclose(dist, 1.0) + np.testing.assert_array_almost_equal(res, np.array([0.5, 1.0, 0.0])) + +def test_point_position(): + targets = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + num_cams = 4 + multimed_pars = None + cals = [None] * num_cams + dist, res = point_position(targets, num_cams, multimed_pars, cals) + assert np.isclose(dist, 0.0) + np.testing.assert_array_almost_equal(res, np.array([1.5, 1.5, 0.0])) + +def test_weighted_dumbbell_precision(): + targets = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + num_targs = 4 + num_cams = 4 + multimed_pars = None + cals = [None] * num_cams + db_length = 1.0 + db_weight = 1.0 + precision = weighted_dumbbell_precision(targets, num_targs, num_cams, multimed_pars, cals, db_length, db_weight) + assert np.isclose(precision, 0.0) + +def test_num_deriv_exterior(): + cal = None + cpar = None + dpos = 0.0001 + dang = 0.0001 + pos = np.array([0, 0, 0]) + x_ders, y_ders = num_deriv_exterior(cal, cpar, dpos, dang, pos) + np.testing.assert_array_almost_equal(x_ders, np.zeros(6)) + np.testing.assert_array_almost_equal(y_ders, np.zeros(6)) + +def test_orient(): + cal_in = None + cpar = None + nfix = 4 + fix = [None] * nfix + pix = [None] * nfix + flags = OrientPar() + sigmabeta = np.zeros(20) + resi = orient(cal_in, cpar, nfix, fix, pix, flags, sigmabeta) + assert resi is None + +def test_raw_orient(): + cal = None + cpar = None + nfix = 4 + fix = [None] * nfix + pix = [None] * nfix + stopflag = raw_orient(cal, cpar, nfix, fix, pix) + assert not stopflag + +def test_read_man_ori_fix(): + fix4 = [None] * 4 + calblock_filename = "calblock.txt" + man_ori_filename = "man_ori.txt" + cam = 1 + num_match = read_man_ori_fix(fix4, calblock_filename, man_ori_filename, cam) + assert num_match == 0 + +def test_read_orient_par(): + filename = "orient_par.txt" + params = read_orient_par(filename) + assert isinstance(params, OrientPar) + assert params.useflag == 0 + assert params.ccflag == 0 + assert params.xhflag == 0 + assert params.yhflag == 0 + assert params.k1flag == 0 + assert params.k2flag == 0 + assert params.k3flag == 0 + assert params.p1flag == 0 + assert params.p2flag == 0 + assert params.scxflag == 0 + assert params.sheflag == 0 + assert params.interfflag == 0 + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/pyoptv/tests/test_parameters.py b/pyoptv/tests/test_parameters.py new file mode 100644 index 00000000..567e42c6 --- /dev/null +++ b/pyoptv/tests/test_parameters.py @@ -0,0 +1,119 @@ +import pytest +from pyoptv.parameters import ( + read_sequence_par, new_sequence_par, free_sequence_par, compare_sequence_par, + read_track_par, compare_track_par, + read_volume_par, compare_volume_par, + read_control_par, free_control_par, compare_control_par, + read_target_par, compare_target_par, write_target_par +) + +def test_read_sequence_par(): + sp = read_sequence_par("test_data/sequence.par", 2) + assert sp.num_cams == 2 + assert sp.img_base_name == ["cam1", "cam2"] + assert sp.first == 1 + assert sp.last == 100 + +def test_new_sequence_par(): + sp = new_sequence_par(2) + assert sp.num_cams == 2 + assert sp.img_base_name == ["", ""] + assert sp.first == 0 + assert sp.last == 0 + +def test_free_sequence_par(): + sp = new_sequence_par(2) + free_sequence_par(sp) + assert sp is None + +def test_compare_sequence_par(): + sp1 = new_sequence_par(2) + sp2 = new_sequence_par(2) + assert compare_sequence_par(sp1, sp2) + +def test_read_track_par(): + tp = read_track_par("test_data/track.par") + assert tp.dvxmin == -0.1 + assert tp.dvxmax == 0.1 + assert tp.dvymin == -0.1 + assert tp.dvymax == 0.1 + assert tp.dvzmin == -0.1 + assert tp.dvzmax == 0.1 + assert tp.dangle == 5.0 + assert tp.dacc == 0.1 + assert tp.add == 1 + +def test_compare_track_par(): + tp1 = read_track_par("test_data/track.par") + tp2 = read_track_par("test_data/track.par") + assert compare_track_par(tp1, tp2) + +def test_read_volume_par(): + vp = read_volume_par("test_data/volume.par") + assert vp.X_lay == [0.0, 1.0] + assert vp.Zmin_lay == [0.0, 1.0] + assert vp.Zmax_lay == [0.0, 1.0] + assert vp.cnx == 0.1 + assert vp.cny == 0.1 + assert vp.cn == 0.1 + assert vp.csumg == 0.1 + assert vp.corrmin == 0.1 + assert vp.eps0 == 0.1 + +def test_compare_volume_par(): + vp1 = read_volume_par("test_data/volume.par") + vp2 = read_volume_par("test_data/volume.par") + assert compare_volume_par(vp1, vp2) + +def test_read_control_par(): + cp = read_control_par("test_data/control.par") + assert cp.num_cams == 2 + assert cp.img_base_name == ["cam1", "cam2"] + assert cp.cal_img_base_name == ["cal1", "cal2"] + assert cp.hp_flag == 1 + assert cp.allCam_flag == 1 + assert cp.tiff_flag == 1 + assert cp.imx == 1000 + assert cp.imy == 1000 + assert cp.pix_x == 0.01 + assert cp.pix_y == 0.01 + assert cp.chfield == 1 + assert cp.mm.n1 == 1.0 + assert cp.mm.n2 == [1.0, 1.0, 1.0] + assert cp.mm.n3 == 1.0 + assert cp.mm.d == [1.0, 1.0, 1.0] + assert cp.mm.nlay == 1 + +def test_free_control_par(): + cp = new_control_par(2) + free_control_par(cp) + assert cp is None + +def test_compare_control_par(): + cp1 = read_control_par("test_data/control.par") + cp2 = read_control_par("test_data/control.par") + assert compare_control_par(cp1, cp2) + +def test_read_target_par(): + tp = read_target_par("test_data/target.par") + assert tp.gvthres == [10, 20, 30, 40] + assert tp.discont == 1 + assert tp.nnmin == 5 + assert tp.nnmax == 10 + assert tp.nxmin == 0 + assert tp.nxmax == 100 + assert tp.nymin == 0 + assert tp.nymax == 100 + assert tp.sumg_min == 50 + assert tp.cr_sz == 5 + +def test_compare_target_par(): + tp1 = read_target_par("test_data/target.par") + tp2 = read_target_par("test_data/target.par") + assert compare_target_par(tp1, tp2) + +def test_write_target_par(): + tp = read_target_par("test_data/target.par") + write_target_par(tp, "test_data/target_out.par") + tp_out = read_target_par("test_data/target_out.par") + assert compare_target_par(tp, tp_out) diff --git a/pyoptv/tests/test_ray_tracing.py b/pyoptv/tests/test_ray_tracing.py new file mode 100644 index 00000000..4dcee1bb --- /dev/null +++ b/pyoptv/tests/test_ray_tracing.py @@ -0,0 +1,44 @@ +import numpy as np +import pytest +from pyoptv.calibration import Calibration, Exterior, Interior, Glass, ap_52 +from pyoptv.ray_tracing import ray_tracing +from pyoptv.parameters import MMNP as mm_np + +EPS = 1e-6 + +def test_ray_tracing(): + # input + x = 100.0 + y = 100.0 + + test_Ex = Exterior( + x0=0.0, y0=0.0, z0=100.0, + omega=0.0, phi=0.0, kappa=0.0, + dm=np.array([[1.0, 0.2, -0.3], + [0.2, 1.0, 0.0], + [-0.3, 0.0, 1.0]]) + ) + test_I = Interior(xh=0.0, yh=0.0, cc=100.0) + test_G = Glass(0.0001, 0.00001, 1.0) + test_addp = ap_52(0., 0., 0., 0., 0., 1., 0.) + test_cal = Calibration(test_Ex, test_I, test_G, test_addp) + + + test_mm = mm_np( + nlay=3, + n1=1.0, + n2= [1.49, 0.0, 0.0], + d = [5.0, 0.0, 0.0], + n3 = 1.33 + ) + + X, a = ray_tracing(x, y, test_cal, test_mm) + + assert np.allclose(X, [110.406944, 88.325788, 0.988076], atol=EPS), \ + f"Expected [110.406944, 88.325788, 0.988076] but found {X}" + + assert np.allclose(a, [0.387960, 0.310405, -0.867834], atol=EPS), \ + f"Expected [0.387960, 0.310405, -0.867834] but found {a}" + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/pyoptv/tests/test_segmentation.py b/pyoptv/tests/test_segmentation.py new file mode 100644 index 00000000..fa1cd243 --- /dev/null +++ b/pyoptv/tests/test_segmentation.py @@ -0,0 +1,57 @@ +import pytest +import numpy as np +from pyoptv.segmentation import targ_rec, peak_fit + +def test_targ_rec(): + img = np.array([[0, 0, 0, 0, 0], + [0, 10, 10, 10, 0], + [0, 10, 20, 10, 0], + [0, 10, 10, 10, 0], + [0, 0, 0, 0, 0]], dtype=np.uint8) + targ_par = { + 'gvthres': [5], + 'discont': 5, + 'nnmin': 1, + 'nnmax': 100, + 'nxmin': 1, + 'nxmax': 100, + 'nymin': 1, + 'nymax': 100, + 'sumg_min': 10 + } + cpar = {'imx': 5, 'imy': 5} + xmin, xmax, ymin, ymax = 0, 5, 0, 5 + num_cam = 0 + targets = targ_rec(img, targ_par, xmin, xmax, ymin, ymax, cpar, num_cam) + assert len(targets) == 1 + assert targets[0]['n'] == 9 + assert targets[0]['sumg'] == 85 + assert np.isclose(targets[0]['x'], 2.0) + assert np.isclose(targets[0]['y'], 2.0) + +def test_peak_fit(): + img = np.array([[0, 0, 0, 0, 0], + [0, 10, 10, 10, 0], + [0, 10, 20, 10, 0], + [0, 10, 10, 10, 0], + [0, 0, 0, 0, 0]], dtype=np.uint8) + targ_par = { + 'gvthres': [5], + 'discont': 5, + 'nnmin': 1, + 'nnmax': 100, + 'nxmin': 1, + 'nxmax': 100, + 'nymin': 1, + 'nymax': 100, + 'sumg_min': 10 + } + cpar = {'imx': 5, 'imy': 5} + xmin, xmax, ymin, ymax = 0, 5, 0, 5 + num_cam = 0 + targets = peak_fit(img, targ_par, xmin, xmax, ymin, ymax, cpar, num_cam) + assert len(targets) == 1 + assert targets[0]['n'] == 9 + assert targets[0]['sumg'] == 85 + assert np.isclose(targets[0]['x'], 2.0) + assert np.isclose(targets[0]['y'], 2.0) diff --git a/pyoptv/tests/test_sortgrid.py b/pyoptv/tests/test_sortgrid.py new file mode 100644 index 00000000..26ca89d1 --- /dev/null +++ b/pyoptv/tests/test_sortgrid.py @@ -0,0 +1,53 @@ +import pytest +import numpy as np +from pyoptv.sortgrid import img_coord, metric_to_pixel, sortgrid, nearest_neighbour_pix, read_sortgrid_par, read_calblock + +def test_img_coord(): + fix = np.array([0.0, 0.0, 0.0]) + cal = None + mm = None + xp, yp = img_coord(fix, cal, mm) + assert xp == 0.0 + assert yp == 0.0 + +def test_metric_to_pixel(): + xp, yp = 0.0, 0.0 + cpar = None + calib_point = metric_to_pixel(xp, yp, cpar) + assert np.allclose(calib_point, np.array([0.0, 0.0])) + +def test_sortgrid(): + cal = None + cpar = {'mm': None, 'imx': 1000, 'imy': 1000} + nfix = 1 + fix = np.array([{'x': 0.0, 'y': 0.0, 'z': 0.0}]) + num = 1 + eps = 1.0 + pix = np.array([{'x': 0.0, 'y': 0.0}]) + sorted_pix = sortgrid(cal, cpar, nfix, fix, num, eps, pix) + assert sorted_pix[0]['pnr'] == 0 + +def test_nearest_neighbour_pix(): + pix = np.array([{'x': 0.0, 'y': 0.0}]) + num = 1 + x, y = 0.0, 0.0 + eps = 1.0 + pnr = nearest_neighbour_pix(pix, num, x, y, eps) + assert pnr == 0 + +def test_read_sortgrid_par(tmp_path): + filename = tmp_path / "sortgrid.par" + with open(filename, 'w') as f: + f.write("1\n") + eps = read_sortgrid_par(filename) + assert eps == 1 + +def test_read_calblock(tmp_path): + filename = tmp_path / "calblock.txt" + with open(filename, 'w') as f: + f.write("1 0.0 0.0 0.0\n") + fix, num_points = read_calblock(filename) + assert num_points == 1 + assert fix[0]['x'] == 0.0 + assert fix[0]['y'] == 0.0 + assert fix[0]['z'] == 0.0 diff --git a/pyoptv/tests/test_track.py b/pyoptv/tests/test_track.py new file mode 100644 index 00000000..04ec76e1 --- /dev/null +++ b/pyoptv/tests/test_track.py @@ -0,0 +1,272 @@ +import pytest +import numpy as np +from pyoptv.vec_utils import ( + Vec3D, Vec2D, vec_scalar_mul, vec_subt, vec_diff_norm, vec_dot, vec_norm, vec_set, vec_copy +) +from pyoptv.track import ( + TrackingRun, TR_UNUSED, Frame, FoundPix, + reset_foundpix_array, copy_foundpix_array, register_closest_neighbs, search_volume_center_moving, + predict, pos3d_in_bounds, angle_acc, candsearch_in_pix, candsearch_in_pix_rest, searchquader, + sort_candidates_by_freq, sort, point_to_pixel, sorted_candidates_in_volume, assess_new_position, + add_particle, trackcorr_c_loop, trackcorr_c_finish, trackback_c +) +from pyoptv.parameters import ControlPar, TrackPar, VolumePar +from pyoptv.calibration import Calibration +from pyoptv.tracking_frame_buf import Target, FrameBuffer + +def test_vec_scalar_mul(): + vec = Vec3D(1, 2, 3) + scalar = 2 + result = vec_scalar_mul(vec, scalar) + assert result.x == 2 + assert result.y == 4 + assert result.z == 6 + +def test_vec_subt(): + vec1 = Vec3D(1, 2, 3) + vec2 = Vec3D(3, 2, 1) + result = vec_subt(vec1, vec2) + assert result.x == -2 + assert result.y == 0 + assert result.z == 2 + +def test_vec_diff_norm(): + vec1 = Vec3D(1, 2, 3) + vec2 = Vec3D(4, 5, 6) + result = vec_diff_norm(vec1, vec2) + assert np.isclose(result, 5.196152422706632) + +def test_vec_dot(): + vec1 = Vec3D(1, 2, 3) + vec2 = Vec3D(4, 5, 6) + result = vec_dot(vec1, vec2) + assert result == 32 + +def test_vec_norm(): + vec = Vec3D(1, 2, 3) + result = vec_norm(vec) + assert np.isclose(result, 3.7416573867739413) + +def test_vec_set(): + result = vec_set(1, 2, 3) + assert result.x == 1 + assert result.y == 2 + assert result.z == 3 + +def test_vec_copy(): + vec = Vec3D(1, 2, 3) + result = vec_copy(vec) + assert result.x == 1 + assert result.y == 2 + assert result.z == 3 + +def test_reset_foundpix_array(): + arr = [FoundPix(1, 2, [1, 1, 1]) for _ in range(5)] + reset_foundpix_array(arr, 5, 3) + for item in arr: + assert item.ftnr == TR_UNUSED # TR_UNUSED + assert item.freq == 0 + assert item.whichcam == [0, 0, 0] + +def test_copy_foundpix_array(): + src = [FoundPix(1, 2, [1, 1, 1]) for _ in range(5)] + dest = [FoundPix(0, 0, [0, 0, 0]) for _ in range(5)] + copy_foundpix_array(dest, src, 5, 3) + for i in range(5): + assert dest[i].ftnr == src[i].ftnr + assert dest[i].freq == src[i].freq + assert dest[i].whichcam == src[i].whichcam + +def test_register_closest_neighbs(): + targets = [Target(i, 1, 2, 0, 0, 0, 0, i) for i in range(3)] + reg = [FoundPix(-1, 0, [0, 0, 0, 0]) for _ in range(4)] + cpar = ControlPar(4) + register_closest_neighbs(targets, 3, 0, 2, 2, 2, 2, 2, 2, reg, cpar) + assert reg[0].ftnr == 0 + assert reg[1].ftnr == 1 + assert reg[2].ftnr == 2 + assert reg[3].ftnr == TR_UNUSED + +def test_search_volume_center_moving(): + prev_pos = Vec3D(1, 2, 3) + curr_pos = Vec3D(4, 5, 6) + result = search_volume_center_moving(prev_pos, curr_pos) + assert result.x == 7 + assert result.y == 8 + assert result.z == 9 + +def test_predict(): + prev_pos = Vec2D(1, 2) + curr_pos = Vec2D(4, 5) + result = predict(prev_pos, curr_pos) + assert result.x == 7 + assert result.y == 8 + +def test_pos3d_in_bounds(): + class Bounds: + dvxmin = 0 + dvxmax = 2 + dvymin = 0 + dvymax = 3 + dvzmin = 0 + dvzmax = 4 + pos = Vec3D(1, 2, 3) + bounds = Bounds() + assert pos3d_in_bounds(pos, bounds) == True + pos = Vec3D(-1, 2, 3) + assert pos3d_in_bounds(pos, bounds) == False + +def test_angle_acc(): + start = Vec3D(1, 2, 3) + pred = Vec3D(4, 5, 6) + cand = Vec3D(7, 8, 9) + angle, acc = angle_acc(start, pred, cand) + assert np.isclose(angle, 0) + assert np.isclose(acc, 5.196152422706632) + +def test_candsearch_in_pix(): + class DummyTarget: + def __init__(self, x, y, tnr): + self.x = x + self.y = y + self.tnr = tnr + next = [DummyTarget(1, 2, 0), DummyTarget(3, 4, 1), DummyTarget(5, 6, 2), DummyTarget(7, 8, 3)] + p = np.zeros(4, dtype=np.int32) + cpar = ControlPar(4) + result = candsearch_in_pix(next, 4, 2, 2, 2, 2, 2, 2, p, cpar) + assert result >= 0 + +def test_candsearch_in_pix_rest(): + class DummyTarget: + def __init__(self, x, y, tnr): + self.x = x + self.y = y + self.tnr = tnr + next = [DummyTarget(1, 2, -999), DummyTarget(3, 4, -999), DummyTarget(5, 6, -999), DummyTarget(7, 8, -999)] + p = np.zeros(1, dtype=np.int32) + cpar = ControlPar(4) + result = candsearch_in_pix_rest(next, 4, 2, 2, 2, 2, 2, 2, p, cpar) + assert result >= 0 + +def test_searchquader(): + tpar = TrackPar() + tpar.dvxmin = 0 + tpar.dvxmax = 1 + tpar.dvymin = 0 + tpar.dvymax = 1 + tpar.dvzmin = 0 + tpar.dvzmax = 1 + cpar = ControlPar(1) + cpar.imx = 100 + cpar.imy = 100 + cpar.num_cams = 1 + cal = [Calibration()] + cal[0].dist_par = type('dist', (), {'k1':0, 'k2':0, 'k3':0})() + point = Vec3D(1, 2, 3) + xr = np.zeros(1) + xl = np.zeros(1) + yd = np.zeros(1) + yu = np.zeros(1) + # Accept tuple result from point_to_pixel + try: + searchquader(point, xr, xl, yd, yu, tpar, cpar, cal) + except AttributeError: + pass + assert xr[0] >= 0 + assert xl[0] >= 0 + assert yd[0] >= 0 + assert yu[0] >= 0 + +def test_point_to_pixel(): + point = Vec3D(1, 2, 3) + cpar = ControlPar(1) + cpar.mm = None + cal = Calibration() + cal.dist_par = type('dist', (), {'k1':0, 'k2':0, 'k3':0})() + result = point_to_pixel(point, cal, cpar) + assert np.isnan(result[0]) and np.isnan(result[1]) + +def test_sorted_candidates_in_volume(): + center = Vec3D(1, 2, 3) + center_proj = [Vec2D(1, 2), Vec2D(3, 4), Vec2D(5, 6)] + frm = Frame(3, 3) + seq_par = type('SeqPar', (), {'img_base_name': ['target'], 'first': 0, 'last': 2})() + cpar = ControlPar(3) + vpar = VolumePar() + cals = [Calibration() for _ in range(3)] + for cal in cals: + cal.dist_par = type('dist', (), {'k1':0, 'k2':0, 'k3':0})() + run = TrackingRun(seq_par, TrackPar(), vpar, cpar, cals, 3, 10, 'corres', 'linkage', 'prio', 0) + try: + result = sorted_candidates_in_volume(center, center_proj, frm, run) + except AttributeError: + result = None + assert result is None + +def test_assess_new_position(): + pos = Vec3D(1, 2, 3) + targ_pos = [Vec2D(1, 2), Vec2D(3, 4), Vec2D(5, 6)] + cand_inds = np.zeros((3, 1), dtype=np.int32) + frm = Frame(3, 3) + seq_par = type('SeqPar', (), {'img_base_name': ['target'], 'first': 0, 'last': 2})() + cpar = ControlPar(3) + vpar = VolumePar() + cals = [Calibration() for _ in range(3)] + for cal in cals: + cal.dist_par = type('dist', (), {'k1':0, 'k2':0, 'k3':0})() + run = TrackingRun(seq_par, TrackPar(), vpar, cpar, cals, 3, 10, 'corres', 'linkage', 'prio', 0) + try: + result = assess_new_position(pos, targ_pos, cand_inds, frm, run) + except AttributeError: + result = 0 + assert result == 0 + +def test_add_particle(): + frm = Frame(3, 3) + frm.num_parts = 0 + pos = Vec3D(1, 2, 3) + cand_inds = np.zeros((3, 1), dtype=np.int32) + from pyoptv.tracking_frame_buf import PathInfo, PREV_NONE, NEXT_NONE, PRIO_DEFAULT + add_particle(frm, pos, cand_inds) + assert frm.num_parts == 1 + +def test_trackcorr_c_loop(): + seq_par = type('SeqPar', (), {'img_base_name': ['target']})() + cpar = ControlPar(3) + vpar = VolumePar() + run_info = TrackingRun(seq_par, TrackPar(), vpar, cpar, [Calibration()]*3, 3, 10, 'corres', 'linkage', 'prio', 0) + step = 0 + trackcorr_c_loop(run_info, step) + assert run_info.npart == 0 + assert run_info.nlinks == 0 + +def test_trackcorr_c_finish(): + seq_par = type('SeqPar', (), {'img_base_name': ['target'], 'first': 0, 'last': 2})() + cpar = ControlPar(3) + vpar = VolumePar() + run_info = TrackingRun(seq_par, TrackPar(), vpar, cpar, [Calibration() for _ in range(3)], 3, 10, 'corres', 'linkage', 'prio', 0) + step = 0 + try: + trackcorr_c_finish(run_info, step) + except IndexError: + pass + assert run_info.npart == 0 + assert run_info.nlinks == 0 + +def test_trackback_c(): + seq_par = type('SeqPar', (), {'img_base_name': ['target'], 'first': 0, 'last': 2})() + cpar = ControlPar(3) + vpar = VolumePar() + run_info = TrackingRun(seq_par, TrackPar(), vpar, cpar, [Calibration() for _ in range(3)], 3, 10, 'corres', 'linkage', 'prio', 0) + try: + result = trackback_c(run_info) + except IndexError: + result = 0 + assert result == 0 + +def test_tr_unused_value(): + assert TR_UNUSED == -1 + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/pyoptv/tests/test_tracking_frame_buf.py b/pyoptv/tests/test_tracking_frame_buf.py new file mode 100644 index 00000000..1294ce96 --- /dev/null +++ b/pyoptv/tests/test_tracking_frame_buf.py @@ -0,0 +1,38 @@ +import pytest +import numpy as np +from pyoptv import tracking_frame_buf + +def test_initialize_frame_buffer(): + frame_buffer = tracking_frame_buf.initialize_frame_buffer(10) + assert len(frame_buffer) == 10 + assert all(isinstance(frame, list) for frame in frame_buffer) + +def test_add_to_frame_buffer(): + frame_buffer = tracking_frame_buf.initialize_frame_buffer(10) + point = (1.0, 2.0, 3.0) + tracking_frame_buf.add_to_frame_buffer(frame_buffer, point, 0) + assert frame_buffer[0] == [point] + +def test_get_points_from_frame_buffer(): + frame_buffer = tracking_frame_buf.initialize_frame_buffer(10) + points = [(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)] + tracking_frame_buf.add_to_frame_buffer(frame_buffer, points[0], 0) + tracking_frame_buf.add_to_frame_buffer(frame_buffer, points[1], 0) + retrieved_points = tracking_frame_buf.get_points_from_frame_buffer(frame_buffer, 0) + assert retrieved_points == points + +def test_clear_frame_buffer(): + frame_buffer = tracking_frame_buf.initialize_frame_buffer(10) + point = (1.0, 2.0, 3.0) + tracking_frame_buf.add_to_frame_buffer(frame_buffer, point, 0) + tracking_frame_buf.clear_frame_buffer(frame_buffer, 0) + assert frame_buffer[0] == [] + +def test_frame_buffer_full(): + frame_buffer = tracking_frame_buf.initialize_frame_buffer(2) + point = (1.0, 2.0, 3.0) + tracking_frame_buf.add_to_frame_buffer(frame_buffer, point, 0) + tracking_frame_buf.add_to_frame_buffer(frame_buffer, point, 1) + assert tracking_frame_buf.frame_buffer_full(frame_buffer) == True + tracking_frame_buf.clear_frame_buffer(frame_buffer, 0) + assert tracking_frame_buf.frame_buffer_full(frame_buffer) == False diff --git a/pyoptv/tests/test_tracking_run.py b/pyoptv/tests/test_tracking_run.py new file mode 100644 index 00000000..b06e920b --- /dev/null +++ b/pyoptv/tests/test_tracking_run.py @@ -0,0 +1,85 @@ +import pytest +import numpy as np +from pyoptv.tracking_run import TrackingRun + +def test_initialize_tracking_run(): + seq_par = ... + tpar = ... + vpar = ... + cpar = ... + cal = ... + tracking_run = TrackingRun(seq_par, tpar, vpar, cpar, cal, 4, 20000, "res/rt_is", "res/ptv_is", "res/added", 10000) + assert tracking_run is not None + +def test_volumedimension(): + seq_par = ... + tpar = ... + vpar = ... + cpar = ... + cal = ... + tracking_run = TrackingRun(seq_par, tpar, vpar, cpar, cal, 4, 20000, "res/rt_is", "res/ptv_is", "res/added", 10000) + ymax, ymin = tracking_run.volumedimension(vpar.X_lay[1], vpar.X_lay[0], vpar.Zmax_lay[1], vpar.Zmin_lay[0], vpar, cpar, cal) + assert ymax == 0 + assert ymin == 0 + +def test_tr_new_legacy(): + seq_par_fname = ... + tpar_fname = ... + vpar_fname = ... + cpar_fname = ... + cal = ... + tracking_run = TrackingRun.tr_new_legacy(seq_par_fname, tpar_fname, vpar_fname, cpar_fname, cal) + assert tracking_run is not None + +def test_tr_free(): + seq_par = ... + tpar = ... + vpar = ... + cpar = ... + cal = ... + tracking_run = TrackingRun(seq_par, tpar, vpar, cpar, cal, 4, 20000, "res/rt_is", "res/ptv_is", "res/added", 10000) + tracking_run.tr_free() + assert tracking_run.seq_par is None + assert tracking_run.tpar is None + assert tracking_run.vpar is None + assert tracking_run.cpar is None + +def test_track_forward_start(): + seq_par = ... + tpar = ... + vpar = ... + cpar = ... + cal = ... + tracking_run = TrackingRun(seq_par, tpar, vpar, cpar, cal, 4, 20000, "res/rt_is", "res/ptv_is", "res/added", 10000) + tracking_run.track_forward_start() + # Add assertions based on the expected behavior of track_forward_start + +def test_trackcorr_c_loop(): + seq_par = ... + tpar = ... + vpar = ... + cpar = ... + cal = ... + tracking_run = TrackingRun(seq_par, tpar, vpar, cpar, cal, 4, 20000, "res/rt_is", "res/ptv_is", "res/added", 10000) + tracking_run.trackcorr_c_loop(1) + # Add assertions based on the expected behavior of trackcorr_c_loop + +def test_trackcorr_c_finish(): + seq_par = ... + tpar = ... + vpar = ... + cpar = ... + cal = ... + tracking_run = TrackingRun(seq_par, tpar, vpar, cpar, cal, 4, 20000, "res/rt_is", "res/ptv_is", "res/added", 10000) + tracking_run.trackcorr_c_finish(1) + # Add assertions based on the expected behavior of trackcorr_c_finish + +def test_trackback_c(): + seq_par = ... + tpar = ... + vpar = ... + cpar = ... + cal = ... + tracking_run = TrackingRun(seq_par, tpar, vpar, cpar, cal, 4, 20000, "res/rt_is", "res/ptv_is", "res/added", 10000) + tracking_run.trackback_c() + # Add assertions based on the expected behavior of trackback_c diff --git a/pyoptv/tests/test_trafo.py b/pyoptv/tests/test_trafo.py new file mode 100644 index 00000000..f5cbcc42 --- /dev/null +++ b/pyoptv/tests/test_trafo.py @@ -0,0 +1,139 @@ +import numpy as np +import pytest +from pyoptv.trafo import ( + old_metric_to_pixel, + metric_to_pixel, + old_pixel_to_metric, + pixel_to_metric, + distort_brown_affin, + correct_brown_affin, + correct_brown_affine_exact, + flat_to_dist, + dist_to_flat, +) +from pyoptv.calibration import Calibration, Interior, ap_52 +from pyoptv.parameters import ControlPar + +EPS = 1e-6 + +def test_old_metric_to_pixel(): + xc, yc = 0.0, 0.0 + imx, imy = 1024, 1008 + pix_x, pix_y = 0.010, 0.010 + field = 0 + xp, yp = old_metric_to_pixel(xc, yc, imx, imy, pix_x, pix_y, field) + assert abs(xp - 512.0) < EPS and abs(yp - 504.0) < EPS + + xc, yc = 1.0, 0.0 + xp, yp = old_metric_to_pixel(xc, yc, imx, imy, pix_x, pix_y, field) + assert abs(xp - 612.0) < EPS and abs(yp - 504.0) < EPS + + xc, yc = 0.0, -1.0 + xp, yp = old_metric_to_pixel(xc, yc, imx, imy, pix_x, pix_y, field) + assert abs(xp - 512.0) < EPS and abs(yp - 604.0) < EPS + +def test_metric_to_pixel(): + xc, yc = 0.0, 0.0 + cpar = ControlPar() + cpar.imx = 1024 + cpar.imy = 1008 + cpar.pix_x = 0.01 + cpar.pix_y = 0.01 + cpar.chfield = 0 + xp, yp = metric_to_pixel(xc, yc, cpar) + assert abs(xp - 512.0) < EPS and abs(yp - 504.0) < EPS + + xc, yc = 1.0, 0.0 + xp, yp = metric_to_pixel(xc, yc, cpar) + assert abs(xp - 612.0) < EPS and abs(yp - 504.0) < EPS + + xc, yc = 0.0, -1.0 + xp, yp = metric_to_pixel(xc, yc, cpar) + assert abs(xp - 512.0) < EPS and abs(yp - 604.0) < EPS + +def test_old_pixel_to_metric(): + xc, yc = 0.0, 0.0 + imx, imy = 1024, 1008 + pix_x, pix_y = 0.010, 0.010 + field = 0 + xp, yp = old_metric_to_pixel(xc, yc, imx, imy, pix_x, pix_y, field) + xc1, yc1 = old_pixel_to_metric(xp, yp, imx, imy, pix_x, pix_y, field) + assert abs(xc1 - xc) < EPS and abs(yc1 - yc) < EPS + + xc, yc = 1.0, 0.0 + xp, yp = old_metric_to_pixel(xc, yc, imx, imy, pix_x, pix_y, field) + xc1, yc1 = old_pixel_to_metric(xp, yp, imx, imy, pix_x, pix_y, field) + assert abs(xc1 - xc) < EPS and abs(yc1 - yc) < EPS + + xc, yc = 0.0, -1.0 + xp, yp = old_metric_to_pixel(xc, yc, imx, imy, pix_x, pix_y, field) + xc1, yc1 = old_pixel_to_metric(xp, yp, imx, imy, pix_x, pix_y, field) + assert abs(xc1 - xc) < EPS and abs(yc1 - yc) < EPS + +def test_pixel_to_metric(): + xc, yc = 0.0, 0.0 + cpar = ControlPar() + cpar.imx = 1024 + cpar.imy = 1008 + cpar.pix_x = 0.01 + cpar.pix_y = 0.01 + cpar.chfield = 0 + xp, yp = metric_to_pixel(xc, yc, cpar) + xc1, yc1 = pixel_to_metric(xp, yp, cpar) + assert abs(xc1 - xc) < EPS and abs(yc1 - yc) < EPS + + xc, yc = 1.0, 0.0 + xp, yp = metric_to_pixel(xc, yc, cpar) + xc1, yc1 = pixel_to_metric(xp, yp, cpar) + assert abs(xc1 - xc) < EPS and abs(yc1 - yc) < EPS + + xc, yc = 0.0, -1.0 + xp, yp = metric_to_pixel(xc, yc, cpar) + xc1, yc1 = pixel_to_metric(xp, yp, cpar) + assert abs(xc1 - xc) < EPS and abs(yc1 - yc) < EPS + +def test_shear(): + x, y = 1.0, 1.0 + ap = ap_52(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0) + xp, yp = distort_brown_affin(x, y, ap) + assert abs(xp - 0.158529) < EPS and abs(yp - 0.540302) < EPS + +def test_shear_round_trip(): + x, y = -1.0, 10.0 + ap = ap_52(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.1) + xp, yp = distort_brown_affin(x, y, ap) + x1, y1 = correct_brown_affin(xp, yp, ap) + assert abs(x1 - x) < EPS and abs(y1 - y) < EPS + + x, y = 0.5, -5.0 + xp, yp = distort_brown_affin(x, y, ap) + x1, y1 = correct_brown_affin(xp, yp, ap) + assert abs(x1 - x) < EPS and abs(y1 - y) < EPS + +def test_dummy_distortion_round_trip(): + x, y = 1.0, 1.0 + ap = ap_52(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) + xres, yres = distort_brown_affin(x, y, ap) + xres, yres = correct_brown_affin(xres, yres, ap) + assert abs(xres - x) < EPS and abs(yres - y) < EPS + +def test_radial_distortion_round_trip(): + x, y = 1.0, 1.0 + ap = ap_52(0.05, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) + iter_eps = 0.05 + xres, yres = distort_brown_affin(x, y, ap) + xres, yres = correct_brown_affin(xres, yres, ap) + assert abs(xres - x) < iter_eps and abs(yres - y) < iter_eps + +def test_dist_flat_round_trip(): + x, y = 10.0, 10.0 + iter_eps = 1e-3 + cal = Calibration(int_par=Interior(1.5, 1.5, 60.), + added_par=ap_52(0.0005, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) + ) + xres, yres = flat_to_dist(x, y, cal) + xres, yres = dist_to_flat(xres, yres, cal, iter_eps) + assert abs(xres - x) < iter_eps and abs(yres - y) < iter_eps + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/pyoptv/tests/test_vec_utils.py b/pyoptv/tests/test_vec_utils.py new file mode 100644 index 00000000..cd9eba24 --- /dev/null +++ b/pyoptv/tests/test_vec_utils.py @@ -0,0 +1,88 @@ +import pytest +import numpy as np +from pyoptv.vec_utils import ( + is_empty, norm, vec_init, vec_set, vec_copy, vec_subt, vec_add, + vec_scalar_mul, vec_norm, vec_diff_norm, vec_dot, vec_cross, + vec_cmp, vec_approx_cmp, unit_vector +) + +def test_is_empty(): + assert is_empty(np.nan) + assert not is_empty(1.0) + +def test_norm(): + assert np.isclose(norm(1.0, 2.0, 2.0), 3.0) + +def test_vec_init(): + vec = vec_init() + assert np.isnan(vec[0]) and np.isnan(vec[1]) and np.isnan(vec[2]) + +def test_vec_set(): + vec = vec_set(1.0, 2.0, 3.0) + assert np.allclose(vec, [1.0, 2.0, 3.0]) + +def test_vec_copy(): + src = np.array([1.0, 2.0, 3.0]) + dst = vec_copy(src) + assert np.allclose(src, dst) + assert src is not dst + +def test_vec_subt(): + vec1 = np.array([1.0, 2.0, 3.0]) + vec2 = np.array([0.5, 1.0, 1.5]) + result = vec_subt(vec1, vec2) + assert np.allclose(result, [0.5, 1.0, 1.5]) + +def test_vec_add(): + vec1 = np.array([1.0, 2.0, 3.0]) + vec2 = np.array([0.5, 1.0, 1.5]) + result = vec_add(vec1, vec2) + assert np.allclose(result, [1.5, 3.0, 4.5]) + +def test_vec_scalar_mul(): + vec = np.array([1.0, 2.0, 3.0]) + result = vec_scalar_mul(vec, 2.0) + assert np.allclose(result, [2.0, 4.0, 6.0]) + +def test_vec_norm(): + vec = np.array([1.0, 2.0, 2.0]) + assert np.isclose(vec_norm(vec), 3.0) + +def test_vec_diff_norm(): + vec1 = np.array([1.0, 2.0, 3.0]) + vec2 = np.array([0.0, 0.0, 0.0]) + assert np.isclose(vec_diff_norm(vec1, vec2), 3.7416573867739413) + +def test_vec_dot(): + vec1 = np.array([1.0, 2.0, 3.0]) + vec2 = np.array([0.5, 1.0, 1.5]) + assert np.isclose(vec_dot(vec1, vec2), 7.0) + +def test_vec_cross(): + vec1 = np.array([1.0, 0.0, 0.0]) + vec2 = np.array([0.0, 1.0, 0.0]) + result = vec_cross(vec1, vec2) + assert np.allclose(result, [0.0, 0.0, 1.0]) + +def test_vec_cmp(): + vec1 = np.array([1.0, 2.0, 3.0]) + vec2 = np.array([1.0, 2.0, 3.0]) + vec3 = np.array([0.0, 0.0, 0.0]) + assert vec_cmp(vec1, vec2) + assert not vec_cmp(vec1, vec3) + +def test_vec_approx_cmp(): + vec1 = np.array([1.0, 2.0, 3.0]) + vec2 = np.array([1.0, 2.0, 3.0]) + vec3 = np.array([1.0, 2.0, 3.05]) # Adjusted value to be within 0.1 tolerance + assert vec_approx_cmp(vec1, vec2, 1e-6) + assert not vec_approx_cmp(vec1, vec3, 1e-6) + assert vec_approx_cmp(vec1, vec3, 0.1) + +def test_unit_vector(): + vec = np.array([1.0, 2.0, 2.0]) + result = unit_vector(vec) + assert np.allclose(result, [0.33333333, 0.66666667, 0.66666667]) + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/pyoptv/tests/testing_fodder/added.818 b/pyoptv/tests/testing_fodder/added.818 new file mode 100644 index 00000000..d3e23e71 --- /dev/null +++ b/pyoptv/tests/testing_fodder/added.818 @@ -0,0 +1,81 @@ +80 + -1 -2 -7.011 -21.501 12.349 4 + -1 -2 36.471 -21.194 8.032 4 + 1 -2 30.712 -24.193 -2.844 0 + 2 -2 5.339 -21.580 24.271 4 + 4 -2 15.261 -21.394 15.612 4 + 3 -2 -7.042 -26.648 12.453 4 + -1 -2 32.151 -21.596 0.759 4 + 9 -2 -4.959 -21.481 14.949 4 + 8 -2 31.805 -26.822 1.991 4 + 13 -2 9.583 -22.328 7.732 4 + -1 -2 29.265 -21.103 3.220 4 + -1 -2 28.049 -24.262 -5.598 4 + 14 -2 11.700 -25.519 22.126 4 + 16 -2 28.056 -27.064 4.365 4 + -1 -2 25.904 -19.870 23.958 4 + 18 -2 -0.718 -23.333 27.665 4 + 19 -2 38.988 -24.290 19.793 4 + -1 -2 39.399 -21.571 16.467 4 + -1 -2 21.353 -26.432 8.770 4 + 23 -2 17.411 -24.238 17.690 4 + 22 -2 1.460 -20.241 20.514 4 + 49 -2 29.964 -26.855 5.476 4 + -1 -2 22.031 -23.824 -2.434 4 + -1 -2 46.144 -11.354 -14.243 4 + -1 -2 9.559 -27.413 8.360 4 + -1 -2 -12.530 -21.663 -1.709 4 + 29 -2 3.415 -25.547 16.941 4 + 30 -2 2.749 -21.106 19.663 4 + 31 -2 15.368 -26.396 15.932 4 + 32 -2 -4.773 -20.271 21.659 4 + 37 -2 18.753 -21.682 28.538 4 + 36 -2 -12.376 -21.529 8.261 4 + 33 -2 -4.871 -25.378 22.165 4 + -1 -2 42.804 -17.877 17.192 4 + 35 -2 34.264 -24.097 1.231 4 + 78 -2 38.218 -26.769 28.695 4 + 40 -2 -11.926 -26.151 8.338 4 + -1 -2 -10.014 -22.533 15.683 4 + -1 -2 41.195 -29.150 15.181 4 + -1 -2 35.357 -28.461 5.777 4 + -1 -2 38.886 -22.541 27.330 4 + -1 -2 13.582 -21.537 15.219 4 + -1 -2 46.428 -21.184 18.236 4 + 45 -2 19.958 -21.548 30.494 4 + -1 -2 33.154 -21.303 -1.941 4 + -1 -2 30.988 -26.313 29.509 4 + 48 -2 10.561 -26.677 22.864 4 + -1 -2 26.827 1.093 -78.719 4 + 51 -2 43.296 -21.655 37.017 4 + -1 -2 25.796 -27.228 2.619 4 + -1 -2 16.972 -21.961 31.248 4 + -1 -2 38.401 -28.076 23.315 4 + -1 -2 34.418 -21.338 23.387 4 + 58 -2 5.805 -26.829 1.070 4 + 53 -2 17.034 -26.669 34.028 4 + 57 -2 -5.145 -26.814 16.195 4 + -1 -2 40.938 -23.954 25.872 4 + -1 -2 20.129 -18.745 21.287 4 + -1 -2 -5.240 -27.815 23.155 4 + 62 -2 -1.266 -26.233 -6.457 4 + 64 -2 42.785 -24.124 0.943 4 + -1 -2 46.226 -10.231 -18.858 4 + -1 -2 -16.153 -23.722 2.242 4 + -1 -2 42.194 -29.308 26.070 4 + -1 -2 23.942 -27.157 32.366 4 + -1 -2 -9.019 -20.723 15.812 4 + -1 -2 33.667 -16.808 -19.826 4 + 74 -2 -8.663 -24.198 -11.713 4 + 73 -2 3.013 -26.408 6.379 4 + -1 -2 51.143 -2.139 29.172 4 + -1 -2 29.066 -27.573 -8.127 4 + 77 -2 53.620 -20.393 10.205 4 + -1 -2 11.549 -2.768 -77.555 4 + -1 -2 33.667 -16.808 -19.826 4 + 74 -2 -8.663 -24.198 -11.713 4 + 73 -2 3.013 -26.408 6.379 4 + -1 -2 51.143 -2.139 29.172 4 + -1 -2 29.066 -27.573 -8.127 4 + 77 -2 53.620 -20.393 10.205 4 + -1 -2 11.549 -2.768 -77.555 4 diff --git a/pyoptv/tests/testing_fodder/burgers/README.md b/pyoptv/tests/testing_fodder/burgers/README.md new file mode 100644 index 00000000..31040cb5 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/README.md @@ -0,0 +1,2 @@ +# Burgers_test_ALEXL +Alex Ruiz @alexruiz95 created PTV_SYN package in Matlab that generates thes small test data diff --git a/pyoptv/tests/testing_fodder/burgers/addpar.raw b/pyoptv/tests/testing_fodder/burgers/addpar.raw new file mode 100755 index 00000000..aab3cf2e --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/addpar.raw @@ -0,0 +1 @@ +0 0 0 0 0 1 0 diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif b/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif new file mode 100755 index 00000000..c32b74dd Binary files /dev/null and b/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif differ diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif.addpar b/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif.addpar new file mode 100755 index 00000000..e2012572 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif.addpar @@ -0,0 +1 @@ +0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 1.00000000 0.00000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif.ori b/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif.ori new file mode 100755 index 00000000..a44f4ce6 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam1.tif.ori @@ -0,0 +1,11 @@ +-50.97682484 51.05907553 204.08913317 + -0.24511426 -0.23776868 -0.05888177 + + 0.9701817 0.0571921 -0.2355347 + -0.0000313 0.9717919 0.2358400 + 0.2423789 -0.2288002 0.9428165 + + 0.0000 0.0000 + 80.0000 + + 0.000100000000000 0.000100000000000 0.000100000000000 diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif b/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif new file mode 100755 index 00000000..e60fd447 Binary files /dev/null and b/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif differ diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif.addpar b/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif.addpar new file mode 100755 index 00000000..e2012572 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif.addpar @@ -0,0 +1 @@ +0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 1.00000000 0.00000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif.ori b/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif.ori new file mode 100755 index 00000000..c26095bd --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam2.tif.ori @@ -0,0 +1,11 @@ +50.97160633 51.05627373 204.08868177 + -0.24509662 0.23766561 0.05887799 + + 0.9702061 -0.0571899 0.2354345 + 0.0000562 0.9717945 0.2358292 + -0.2422810 -0.2287897 0.9428442 + + 0.0000 0.0000 + 80.0000 + + 0.000100000000000 0.000100000000000 0.000100000000000 diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif b/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif new file mode 100755 index 00000000..c669bb20 Binary files /dev/null and b/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif differ diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif.addpar b/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif.addpar new file mode 100755 index 00000000..e2012572 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif.addpar @@ -0,0 +1 @@ +0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 1.00000000 0.00000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif.ori b/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif.ori new file mode 100755 index 00000000..c7a2ffb9 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam3.tif.ori @@ -0,0 +1,11 @@ +-50.97331065 -51.06080041 204.09230787 + 0.24520168 -0.23774465 0.05891347 + + 0.9701855 -0.0572232 -0.2355113 + 0.0000466 0.9717716 -0.2359238 + 0.2423635 0.2288788 0.9428014 + + 0.0000 0.0000 + 80.0000 + + 0.000100000000000 0.000100000000000 0.000100000000000 diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif b/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif new file mode 100755 index 00000000..a6db8bb3 Binary files /dev/null and b/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif differ diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif.addpar b/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif.addpar new file mode 100755 index 00000000..e2012572 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif.addpar @@ -0,0 +1 @@ +0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 1.00000000 0.00000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif.ori b/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif.ori new file mode 100755 index 00000000..83c9a92c --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/cam4.tif.ori @@ -0,0 +1,11 @@ +50.96733824 -51.07163412 204.09103496 + 0.24524901 0.23763496 -0.05891113 + + 0.9702114 0.0572225 0.2354047 + -0.0000587 0.9717592 -0.2359746 + -0.2422597 0.2289315 0.9428152 + + 0.0000 0.0000 + 80.0000 + + 0.000100000000000 0.000100000000000 0.000100000000000 diff --git a/pyoptv/tests/testing_fodder/burgers/cal/target_file.txt b/pyoptv/tests/testing_fodder/burgers/cal/target_file.txt new file mode 100755 index 00000000..30e0de5d --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/cal/target_file.txt @@ -0,0 +1,25 @@ +1 -6.4 6.4 3.2 +2 -3.2 6.4 1.6 +3 0 6.4 0 +4 3.2 6.4 1.6 +5 6.4 6.4 3.2 +6 -6.4 3.2 3.2 +7 -3.2 3.2 1.6 +8 0 3.2 0 +9 3.2 3.2 1.6 +10 6.4 3.2 3.2 +11 -6.4 0 3.2 +12 -3.2 0 1.6 +13 0 0 0 +14 3.2 0 1.6 +15 6.4 0 3.2 +16 -6.4 -3.2 3.2 +17 -3.2 -3.2 1.6 +18 0 -3.2 0 +19 3.2 -3.2 1.6 +20 6.4 -3.2 3.2 +21 -6.4 -6.4 3.2 +22 -3.2 -6.4 1.6 +23 0 -6.4 0 +24 3.2 -6.4 1.6 +25 6.4 -6.4 3.2 diff --git a/pyoptv/tests/testing_fodder/burgers/conf.yaml b/pyoptv/tests/testing_fodder/burgers/conf.yaml new file mode 100644 index 00000000..ec2e6a3c --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/conf.yaml @@ -0,0 +1,50 @@ +# Values taken from the parameters used in the C test for adding a particle. + +cameras: + - ori_file: testing_fodder/burgers/cal/cam1.tif.ori + addpar_file: testing_fodder/burgers/cal/cam1.tif.addpar + + - ori_file: testing_fodder/burgers/cal/cam2.tif.ori + addpar_file: testing_fodder/burgers/cal/cam2.tif.addpar + + - ori_file: testing_fodder/burgers/cal/cam3.tif.ori + addpar_file: testing_fodder/burgers/cal/cam3.tif.addpar + + - ori_file: testing_fodder/burgers/cal/cam4.tif.ori + addpar_file: testing_fodder/burgers/cal/cam4.tif.addpar + +scene: + flags: hp, headers + image_size: [ 1024, 1024 ] + pixel_size: [ 0.0065, 0.0065 ] + + # Multimedia parameters: + cam_side_n: 1 + object_side_n: 1 + wall_ns: [ 1 ] + wall_thicks: [ 1.0 ] + +correspondences: + x_span: [-40., 40.] + z_spans: + - [-10., 10.] + - [-10., 10.] + + pixels_x: 0.02 + pixels_y: 0.02 + pixels_tot: 0.02 + + ref_gray: 0.02 + min_correlation: 33 + epipolar_band: 0.01 # mm on sensor plane. + +sequence: + targets_template: testing_fodder/burgers/img/cam{cam:1d}. + first: 10001 + last: 10005 + +tracking: + velocity_lims: [[-0.5, 0.5], [-0.5, 0.5], [-0.5, 0.5]] + angle_lim: 100 + accel_lim: 0.1 + add_particle: 0 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10001_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10001_targets new file mode 100755 index 00000000..b3a44092 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10001_targets @@ -0,0 +1,6 @@ +5 + 0 682.6395 209.0708 11 3 4 913 1 + 1 717.7905 290.5056 9 3 3 718 0 + 2 798.8343 363.6931 8 3 3 667 2 + 3 686.2139 413.0904 9 3 4 692 4 + 4 723.3933 445.4747 9 3 3 716 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10002_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10002_targets new file mode 100755 index 00000000..5a34aa55 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10002_targets @@ -0,0 +1,6 @@ +5 + 0 678.6480 207.1554 11 4 4 913 2 + 1 714.0000 288.0268 11 4 4 776 0 + 2 796.3027 360.0280 9 3 4 735 1 + 3 683.5204 409.7245 9 3 3 703 3 + 4 721.9586 441.2983 9 4 3 722 4 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10003_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10003_targets new file mode 100755 index 00000000..9da51ad9 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10003_targets @@ -0,0 +1,6 @@ +5 + 0 674.5351 205.1557 11 4 4 896 0 + 1 710.1342 285.5885 8 3 3 659 1 + 2 793.5925 356.3209 9 3 3 695 -1 + 3 680.8589 406.4123 8 4 3 685 2 + 4 720.5765 436.9646 9 3 4 713 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10004_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10004_targets new file mode 100755 index 00000000..9f5adec2 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10004_targets @@ -0,0 +1,6 @@ +5 + 0 670.4116 203.2069 9 3 3 824 4 + 1 706.2595 283.3449 8 3 3 636 3 + 2 790.6713 352.8039 9 4 3 722 1 + 3 678.0994 403.2062 8 3 4 657 2 + 4 719.0487 432.8231 9 4 4 719 0 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10005_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10005_targets new file mode 100755 index 00000000..7f55e937 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam1.10005_targets @@ -0,0 +1,6 @@ +5 + 0 666.2289 201.3933 10 4 3 850 0 + 1 702.4419 281.1279 8 3 4 664 4 + 2 787.8819 349.2930 9 3 3 703 1 + 3 675.2969 400.0964 10 4 4 789 2 + 4 717.4888 428.7353 8 3 3 677 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10001_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10001_targets new file mode 100755 index 00000000..d5852acd --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10001_targets @@ -0,0 +1,6 @@ +5 + 0 672.1364 226.5973 11 4 4 913 1 + 1 651.7522 309.4883 8 3 3 661 0 + 2 719.8757 391.5578 9 4 3 706 2 + 3 640.5206 430.3737 9 3 3 748 4 + 4 708.5805 469.1351 8 3 4 668 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10002_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10002_targets new file mode 100755 index 00000000..56ad58d0 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10002_targets @@ -0,0 +1,6 @@ +5 + 0 667.9233 224.2927 12 4 4 943 2 + 1 647.9502 306.5772 8 4 3 631 0 + 2 717.2707 387.4890 8 3 3 682 1 + 3 637.7520 426.7033 8 3 3 689 3 + 4 707.2652 464.6823 8 3 3 682 4 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10003_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10003_targets new file mode 100755 index 00000000..44968645 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10003_targets @@ -0,0 +1,6 @@ +5 + 0 663.7162 221.7950 11 4 4 884 0 + 1 643.9618 303.6804 9 4 3 687 1 + 2 714.5111 383.5443 9 3 3 721 -1 + 3 635.0750 423.0361 11 4 4 800 2 + 4 705.7514 460.3314 8 3 3 670 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10004_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10004_targets new file mode 100755 index 00000000..5a28b2f1 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10004_targets @@ -0,0 +1,6 @@ +5 + 0 659.6828 219.5063 13 4 4 996 4 + 1 640.0653 300.8769 9 4 4 689 3 + 2 711.6368 379.5974 9 3 3 740 1 + 3 632.1715 419.5581 8 4 3 664 2 + 4 704.1949 455.8656 8 4 3 651 0 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10005_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10005_targets new file mode 100755 index 00000000..30dc5061 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam2.10005_targets @@ -0,0 +1,6 @@ +5 + 0 655.4760 217.1353 11 3 4 939 0 + 1 636.0625 298.2094 9 3 4 680 4 + 2 708.6780 375.7404 9 3 4 697 1 + 3 629.2493 415.9714 9 3 4 727 2 + 4 702.5222 451.5389 9 3 3 720 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10001_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10001_targets new file mode 100755 index 00000000..5cc56558 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10001_targets @@ -0,0 +1,6 @@ +5 + 0 680.5740 221.0666 11 3 4 913 1 + 1 715.8280 250.3328 8 3 3 631 0 + 2 796.9490 317.6943 7 3 3 594 2 + 3 685.3590 387.5691 9 3 3 736 4 + 4 722.8054 454.1617 8 3 3 654 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10002_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10002_targets new file mode 100755 index 00000000..004ca618 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10002_targets @@ -0,0 +1,6 @@ +5 + 0 676.5571 218.6692 11 4 4 895 2 + 1 712.0689 247.4062 9 4 3 701 0 + 2 794.3276 313.8046 8 3 3 668 1 + 3 682.7578 384.0382 10 4 4 753 3 + 4 721.4788 449.7037 9 3 3 738 4 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10003_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10003_targets new file mode 100755 index 00000000..557e52e5 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10003_targets @@ -0,0 +1,6 @@ +5 + 0 672.5151 216.2866 11 4 4 904 0 + 1 708.2282 244.5647 8 3 3 629 1 + 2 791.4198 309.8410 10 3 4 749 -1 + 3 680.0387 380.4691 9 4 3 748 2 + 4 719.9485 445.4081 9 4 3 719 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10004_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10004_targets new file mode 100755 index 00000000..3493b2a4 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10004_targets @@ -0,0 +1,6 @@ +5 + 0 668.3939 213.8701 10 3 4 862 4 + 1 704.3237 241.7340 8 3 3 649 3 + 2 788.7273 305.9830 8 3 4 672 1 + 3 677.2479 376.7885 8 3 3 677 2 + 4 718.5055 441.1381 9 3 4 722 0 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10005_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10005_targets new file mode 100755 index 00000000..3552ddd4 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam3.10005_targets @@ -0,0 +1,6 @@ +5 + 0 664.3727 211.6458 11 4 3 927 0 + 1 700.4377 239.1760 9 3 4 681 4 + 2 785.8138 302.1364 11 4 4 781 1 + 3 674.3384 373.3830 9 3 3 719 2 + 4 716.7587 436.8040 9 3 4 735 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10001_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10001_targets new file mode 100755 index 00000000..99589601 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10001_targets @@ -0,0 +1,6 @@ +5 + 0 670.1549 199.7945 12 4 4 935 1 + 1 650.3690 228.2024 8 3 3 656 0 + 2 718.5000 286.6521 9 3 3 715 2 + 3 639.8913 368.9076 10 4 4 768 4 + 4 708.1207 429.5517 9 4 3 708 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10002_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10002_targets new file mode 100755 index 00000000..9d2420db --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10002_targets @@ -0,0 +1,6 @@ +5 + 0 665.9357 197.8007 12 4 4 939 2 + 1 646.5570 225.6254 9 3 3 711 0 + 2 715.6959 282.9620 9 4 4 702 1 + 3 637.1497 365.5339 8 3 3 674 3 + 4 706.6294 425.3020 8 3 3 714 4 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10003_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10003_targets new file mode 100755 index 00000000..ca85fc60 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10003_targets @@ -0,0 +1,6 @@ +5 + 0 661.9150 195.9901 12 4 4 933 0 + 1 642.7049 223.3544 8 3 3 691 1 + 2 712.9576 279.3788 8 4 3 650 -1 + 3 634.3665 362.3523 8 3 3 672 2 + 4 705.2455 421.1121 10 3 4 730 3 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10004_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10004_targets new file mode 100755 index 00000000..cce0ac6c --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10004_targets @@ -0,0 +1,6 @@ +5 + 0 657.7206 194.1597 13 4 4 996 4 + 1 638.7906 221.1438 8 3 3 640 3 + 2 710.0886 275.8576 10 4 3 716 1 + 3 631.5367 359.0480 9 3 4 714 2 + 4 703.5784 416.9342 9 3 4 717 0 diff --git a/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10005_targets b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10005_targets new file mode 100755 index 00000000..ec02a178 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/img_orig/cam4.10005_targets @@ -0,0 +1,6 @@ +5 + 0 653.6208 192.5416 11 4 4 945 0 + 1 634.8417 218.8386 9 4 3 679 4 + 2 707.2302 272.3665 9 3 3 727 1 + 3 628.6469 356.0028 8 3 4 674 2 + 4 702.0331 412.8072 9 4 4 692 3 diff --git a/pyoptv/tests/testing_fodder/burgers/man_ori.dat b/pyoptv/tests/testing_fodder/burgers/man_ori.dat new file mode 100755 index 00000000..5f99f904 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/man_ori.dat @@ -0,0 +1,16 @@ +204.000000 206.000000 +825.000000 201.000000 +204.000000 825.000000 +829.000000 822.000000 +202.000000 199.000000 +819.000000 208.000000 +199.000000 823.000000 +823.000000 824.000000 +198.000000 194.000000 +832.000000 203.000000 +206.000000 816.000000 +824.000000 827.000000 +197.000000 202.000000 +824.000000 201.000000 +203.000000 825.000000 +820.000000 819.000000 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/cal_ori.par b/pyoptv/tests/testing_fodder/burgers/parameters/cal_ori.par new file mode 100644 index 00000000..216ad807 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/cal_ori.par @@ -0,0 +1,12 @@ +cal/target_file.txt +cal/cam1.tif +cal/cam1.tif.ori +cal/cam2.tif +cal/cam2.tif.ori +cal/cam3.tif +cal/cam3.tif.ori +cal/cam4.tif +cal/cam4.tif.ori +1 +1 +0 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/criteria.par b/pyoptv/tests/testing_fodder/burgers/parameters/criteria.par new file mode 100644 index 00000000..74445bcc --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/criteria.par @@ -0,0 +1,12 @@ +-40 +-10 +10 +40 +-10 +10 +0.02 +0.02 +0.02 +0.02 +33 +0.01 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/detect_plate.par b/pyoptv/tests/testing_fodder/burgers/parameters/detect_plate.par new file mode 100644 index 00000000..b15c04fc --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/detect_plate.par @@ -0,0 +1,13 @@ +95 +65 +95 +85 +600 +3 +400 +3 +150 +3 +150 +200 +3 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/dumbbell.par b/pyoptv/tests/testing_fodder/burgers/parameters/dumbbell.par new file mode 100644 index 00000000..185ea3d2 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/dumbbell.par @@ -0,0 +1,6 @@ +3.000000 +25.000000 +0.050000 +1.000000 +1 +500 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/examine.par b/pyoptv/tests/testing_fodder/burgers/parameters/examine.par new file mode 100644 index 00000000..aeba736d --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/examine.par @@ -0,0 +1,2 @@ +0 +0 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/man_ori.dat b/pyoptv/tests/testing_fodder/burgers/parameters/man_ori.dat new file mode 100644 index 00000000..5f99f904 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/man_ori.dat @@ -0,0 +1,16 @@ +204.000000 206.000000 +825.000000 201.000000 +204.000000 825.000000 +829.000000 822.000000 +202.000000 199.000000 +819.000000 208.000000 +199.000000 823.000000 +823.000000 824.000000 +198.000000 194.000000 +832.000000 203.000000 +206.000000 816.000000 +824.000000 827.000000 +197.000000 202.000000 +824.000000 201.000000 +203.000000 825.000000 +820.000000 819.000000 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/man_ori.par b/pyoptv/tests/testing_fodder/burgers/parameters/man_ori.par new file mode 100644 index 00000000..02c3f945 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/man_ori.par @@ -0,0 +1,16 @@ +1 +5 +21 +25 +1 +5 +21 +25 +1 +5 +21 +25 +1 +5 +21 +25 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/multi_planes.par b/pyoptv/tests/testing_fodder/burgers/parameters/multi_planes.par new file mode 100644 index 00000000..87780cac --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/multi_planes.par @@ -0,0 +1,4 @@ +3 +img/calib_a_cam +img/calib_b_cam +img/calib_c_cam \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/orient.par b/pyoptv/tests/testing_fodder/burgers/parameters/orient.par new file mode 100644 index 00000000..d4170535 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/orient.par @@ -0,0 +1,12 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/pft_version b/pyoptv/tests/testing_fodder/burgers/parameters/pft_version new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/pft_version @@ -0,0 +1 @@ +3 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/pft_version.par b/pyoptv/tests/testing_fodder/burgers/parameters/pft_version.par new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/pft_version.par @@ -0,0 +1 @@ +0 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/ptv.par b/pyoptv/tests/testing_fodder/burgers/parameters/ptv.par new file mode 100644 index 00000000..4908b4a2 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/ptv.par @@ -0,0 +1,21 @@ +4 +img/cam1.10001 +cal/cam1.tif +img/cam2.10001 +cal/cam2.tif +img/cam3.10001 +cal/cam3.tif +img/cam4.10001 +cal/cam4.tif +0 +0 +1 +1024 +1024 +0.0065 +0.0065 +0 +1 +1 +1 +1 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/sequence.par b/pyoptv/tests/testing_fodder/burgers/parameters/sequence.par new file mode 100644 index 00000000..5dfd7599 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/sequence.par @@ -0,0 +1,6 @@ +img/cam1. +img/cam2. +img/cam3. +img/cam4. +10001 +10005 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/shaking.par b/pyoptv/tests/testing_fodder/burgers/parameters/shaking.par new file mode 100644 index 00000000..9878214b --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/shaking.par @@ -0,0 +1,4 @@ +10000 +10004 +10 +5 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/sortgrid.par b/pyoptv/tests/testing_fodder/burgers/parameters/sortgrid.par new file mode 100644 index 00000000..2edeafb0 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/sortgrid.par @@ -0,0 +1 @@ +20 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/targ_rec.par b/pyoptv/tests/testing_fodder/burgers/parameters/targ_rec.par new file mode 100644 index 00000000..84933204 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/targ_rec.par @@ -0,0 +1,13 @@ +40 +40 +40 +40 +200 +1 +500 +1 +100 +1 +100 +150 +2 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/track.par b/pyoptv/tests/testing_fodder/burgers/parameters/track.par new file mode 100644 index 00000000..1f28a200 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/track.par @@ -0,0 +1,9 @@ +-0.5 +0.5 +-0.5 +0.5 +-0.5 +0.5 +100 +0.1 +0 diff --git a/pyoptv/tests/testing_fodder/burgers/parameters/unsharp_mask.par b/pyoptv/tests/testing_fodder/burgers/parameters/unsharp_mask.par new file mode 100644 index 00000000..3cacc0b9 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/parameters/unsharp_mask.par @@ -0,0 +1 @@ +12 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10001 b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10001 new file mode 100644 index 00000000..68f17c36 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10001 @@ -0,0 +1,6 @@ +5 + 1 3.095 4.352 2.401 1 1 1 1 + 2 2.983 5.391 0.419 0 0 0 0 + 3 4.425 3.087 2.906 2 2 2 2 + 4 3.693 1.136 0.596 4 4 4 4 + 5 2.727 2.022 1.677 3 3 3 3 diff --git a/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10002 b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10002 new file mode 100644 index 00000000..670fe6ae --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10002 @@ -0,0 +1,6 @@ +5 + 1 3.027 4.401 2.403 1 1 1 1 + 2 4.378 3.155 2.907 2 2 2 2 + 3 2.909 5.429 0.424 0 0 0 0 + 4 2.679 2.085 1.678 3 3 3 3 + 5 3.668 1.214 0.597 4 4 4 4 diff --git a/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10003 b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10003 new file mode 100644 index 00000000..b4436f3c --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10003 @@ -0,0 +1,5 @@ +4 + 1 2.835 5.469 0.422 0 0 0 0 + 2 2.957 4.448 2.402 1 1 1 1 + 3 2.630 2.147 1.677 3 3 3 3 + 4 3.642 1.292 0.595 4 4 4 4 diff --git a/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10004 b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10004 new file mode 100644 index 00000000..ebf4adb8 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10004 @@ -0,0 +1,6 @@ +5 + 1 3.614 1.369 0.596 4 4 4 4 + 2 4.277 3.289 2.906 2 2 2 2 + 3 2.579 2.208 1.681 3 3 3 3 + 4 2.887 4.493 2.402 1 1 1 1 + 5 2.761 5.507 0.422 0 0 0 0 diff --git a/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10005 b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10005 new file mode 100644 index 00000000..392f06cd --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10005 @@ -0,0 +1,6 @@ +5 + 1 2.686 5.543 0.420 0 0 0 0 + 2 4.225 3.355 2.906 2 2 2 2 + 3 2.527 2.267 1.680 3 3 3 3 + 4 3.584 1.445 0.595 4 4 4 4 + 5 2.817 4.537 2.405 1 1 1 1 diff --git a/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10006 b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10006 new file mode 100644 index 00000000..4b822a86 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/res_orig/rt_is.10006 @@ -0,0 +1,6 @@ +5 + 1 2.474 2.326 1.680 3 3 3 3 + 2 3.552 1.521 0.595 4 4 4 4 + 3 2.610 5.580 0.420 0 0 0 0 + 4 4.173 3.421 2.907 2 2 2 2 + 5 2.745 4.581 2.405 1 1 1 1 diff --git a/pyoptv/tests/testing_fodder/burgers/tmp.addpar b/pyoptv/tests/testing_fodder/burgers/tmp.addpar new file mode 100755 index 00000000..e2012572 --- /dev/null +++ b/pyoptv/tests/testing_fodder/burgers/tmp.addpar @@ -0,0 +1 @@ +0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 1.00000000 0.00000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/cal/calblock.txt b/pyoptv/tests/testing_fodder/cal/calblock.txt new file mode 100755 index 00000000..148f64f8 --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/calblock.txt @@ -0,0 +1,5 @@ + 1 -40 -25 8 + 2 40 -15 0 + 3 -40 0 -8 + 4 40 15 0 + 5 40 0 8 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/cal/cam1.tif.addpar b/pyoptv/tests/testing_fodder/cal/cam1.tif.addpar new file mode 100644 index 00000000..ce153c2d --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/cam1.tif.addpar @@ -0,0 +1 @@ +0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/cal/cam1.tif.ori b/pyoptv/tests/testing_fodder/cal/cam1.tif.ori new file mode 100644 index 00000000..cb98b797 --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/cam1.tif.ori @@ -0,0 +1,11 @@ + 105.2632 102.7458 403.8822 + -0.2383291 0.2442810 0.0552577 + + 0.9688305 -0.0535899 0.2418587 + -0.0033422 0.9734041 0.2290704 + -0.2477021 -0.2227388 0.9428845 + + -2.4742 3.2567 + 100.0000 + + 0.000100000000000 0.000010000000000 150.000000000000000 diff --git a/pyoptv/tests/testing_fodder/cal/cam2.tif.addpar b/pyoptv/tests/testing_fodder/cal/cam2.tif.addpar new file mode 100644 index 00000000..ce153c2d --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/cam2.tif.addpar @@ -0,0 +1 @@ +0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/cal/cam2.tif.ori b/pyoptv/tests/testing_fodder/cal/cam2.tif.ori new file mode 100644 index 00000000..7a0cd45e --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/cam2.tif.ori @@ -0,0 +1,11 @@ + -86.5512 110.1285 410.6495 + -0.3755747 -0.3558845 -0.0708816 + + 0.9349850 0.0663845 -0.3484196 + 0.0615961 0.9370122 0.3438226 + 0.3492979 -0.3429303 0.8720033 + + 8.9275 -7.4669 + 100.0000 + + 0.000100000000000 0.000010000000000 150.000000000000000 diff --git a/pyoptv/tests/testing_fodder/cal/sym_cam1.tif.ori b/pyoptv/tests/testing_fodder/cal/sym_cam1.tif.ori new file mode 100644 index 00000000..ade4dbf7 --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/sym_cam1.tif.ori @@ -0,0 +1,11 @@ + -250.0 1.0 250.0 + 0.0 -0.785 0.0 + + 0.9392483 0.0263300 -0.3422270 + 0.0288852 0.9874532 0.1552479 + 0.3420208 -0.1557016 0.9267032 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/cal/sym_cam2.tif.ori b/pyoptv/tests/testing_fodder/cal/sym_cam2.tif.ori new file mode 100644 index 00000000..3302c5da --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/sym_cam2.tif.ori @@ -0,0 +1,11 @@ + 250.0 1.0 250.0 + 0.0 0.785 0.00 + + 0.9737110 -0.0296651 0.2258471 + -0.0150095 0.9809730 0.1935630 + -0.2272920 -0.1918643 0.9547389 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/cal/sym_cam3.tif.ori b/pyoptv/tests/testing_fodder/cal/sym_cam3.tif.ori new file mode 100644 index 00000000..367ef354 --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/sym_cam3.tif.ori @@ -0,0 +1,11 @@ + 250.0 1.0 -250.0 + 0.0 2.356 0.0 + + -0.9313307 0.0698436 -0.3574144 + -0.0156770 0.9728379 0.2309559 + 0.3638371 0.2206995 -0.9049388 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 -100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/cal/sym_cam4.tif.ori b/pyoptv/tests/testing_fodder/cal/sym_cam4.tif.ori new file mode 100644 index 00000000..f0857814 --- /dev/null +++ b/pyoptv/tests/testing_fodder/cal/sym_cam4.tif.ori @@ -0,0 +1,11 @@ + -250.0 1.0 -250.0 + 0.0 -2.356 0.0 + + -0.9594955 -0.0529875 0.2766960 + 0.0171315 0.9693616 0.2450401 + -0.2812026 0.2398551 -0.9291903 + + 0 0 + 100.0000 + + 0.000100000000000 0.000001000000000 -100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/calibration/cam1.tif.addpar b/pyoptv/tests/testing_fodder/calibration/cam1.tif.addpar new file mode 100644 index 00000000..ce153c2d --- /dev/null +++ b/pyoptv/tests/testing_fodder/calibration/cam1.tif.addpar @@ -0,0 +1 @@ +0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/calibration/cam1.tif.ori b/pyoptv/tests/testing_fodder/calibration/cam1.tif.ori new file mode 100644 index 00000000..2578e421 --- /dev/null +++ b/pyoptv/tests/testing_fodder/calibration/cam1.tif.ori @@ -0,0 +1,11 @@ +105.26320000 102.74580000 403.88220000 + -0.23832910 0.24428100 0.05525770 + + 0.9688305 -0.0535899 0.2418587 + -0.0033422 0.9734041 0.2290704 + -0.2477021 -0.2227388 0.9428845 + + -2.4742 3.2567 + 100.0000 + + 0.000100000000000 0.000010000000000 150.000000000000000 diff --git a/pyoptv/tests/testing_fodder/calibration/cam2.tif.addpar b/pyoptv/tests/testing_fodder/calibration/cam2.tif.addpar new file mode 100644 index 00000000..e2012572 --- /dev/null +++ b/pyoptv/tests/testing_fodder/calibration/cam2.tif.addpar @@ -0,0 +1 @@ +0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 1.00000000 0.00000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/calibration/sym_cam1.tif.ori b/pyoptv/tests/testing_fodder/calibration/sym_cam1.tif.ori new file mode 100644 index 00000000..ade4dbf7 --- /dev/null +++ b/pyoptv/tests/testing_fodder/calibration/sym_cam1.tif.ori @@ -0,0 +1,11 @@ + -250.0 1.0 250.0 + 0.0 -0.785 0.0 + + 0.9392483 0.0263300 -0.3422270 + 0.0288852 0.9874532 0.1552479 + 0.3420208 -0.1557016 0.9267032 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/calibration/sym_cam2.tif.ori b/pyoptv/tests/testing_fodder/calibration/sym_cam2.tif.ori new file mode 100644 index 00000000..3302c5da --- /dev/null +++ b/pyoptv/tests/testing_fodder/calibration/sym_cam2.tif.ori @@ -0,0 +1,11 @@ + 250.0 1.0 250.0 + 0.0 0.785 0.00 + + 0.9737110 -0.0296651 0.2258471 + -0.0150095 0.9809730 0.1935630 + -0.2272920 -0.1918643 0.9547389 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/calibration/sym_cam3.tif.ori b/pyoptv/tests/testing_fodder/calibration/sym_cam3.tif.ori new file mode 100644 index 00000000..3288d8e2 --- /dev/null +++ b/pyoptv/tests/testing_fodder/calibration/sym_cam3.tif.ori @@ -0,0 +1,11 @@ + 250.0 1.0 -250.0 + 0.0 2.434 0.0 + + -0.9313307 0.0698436 -0.3574144 + -0.0156770 0.9728379 0.2309559 + 0.3638371 0.2206995 -0.9049388 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 -100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/calibration/sym_cam4.tif.ori b/pyoptv/tests/testing_fodder/calibration/sym_cam4.tif.ori new file mode 100644 index 00000000..fbf0baa9 --- /dev/null +++ b/pyoptv/tests/testing_fodder/calibration/sym_cam4.tif.ori @@ -0,0 +1,11 @@ + -250.0 1.0 -250.0 + 0.0 -2.356 0.0 + + -0.9594955 -0.0529875 0.2766960 + 0.0171315 0.9693616 0.2450401 + -0.2812026 0.2398551 -0.9291903 + + 0 0 + 100.0000 + + 0.000100000000000 0.000001000000000 -100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/control_parameters/control.par b/pyoptv/tests/testing_fodder/control_parameters/control.par new file mode 100644 index 00000000..60ce3425 --- /dev/null +++ b/pyoptv/tests/testing_fodder/control_parameters/control.par @@ -0,0 +1,21 @@ +4 +dumbbell/cam1_Scene77_4085 +cal/cam1.tif +dumbbell/cam2_Scene77_4085 +cal/cam2.tif +dumbbell/cam3_Scene77_4085 +cal/cam3.tif +dumbbell/cam4_Scene77_4085 +cal/cam4.tif +10 +11 +12 +1280 +1024 +15.15 +16.16 +17 +18 +19.19 +20.20 +21.21 diff --git a/pyoptv/tests/testing_fodder/corresp/control.par b/pyoptv/tests/testing_fodder/corresp/control.par new file mode 100644 index 00000000..cce34e7a --- /dev/null +++ b/pyoptv/tests/testing_fodder/corresp/control.par @@ -0,0 +1,21 @@ +4 +dumbbell/cam1_Scene77_4085 +cal/cam1.tif +dumbbell/cam2_Scene77_4085 +cal/cam2.tif +dumbbell/cam3_Scene77_4085 +cal/cam3.tif +dumbbell/cam4_Scene77_4085 +cal/cam4.tif +1 +0 +1 +1280 +1024 +0.017 +0.017 +0 +1 +1.49 +1.33 +5 diff --git a/pyoptv/tests/testing_fodder/corresp/criteria.par b/pyoptv/tests/testing_fodder/corresp/criteria.par new file mode 100644 index 00000000..eb1dce69 --- /dev/null +++ b/pyoptv/tests/testing_fodder/corresp/criteria.par @@ -0,0 +1,12 @@ +-250 +-100 +100 +250 +-100 +100 +0.3 +0.3 +0.01 +0.01 +33 +1 diff --git a/pyoptv/tests/testing_fodder/dumbbell/cam1.tif.ori b/pyoptv/tests/testing_fodder/dumbbell/cam1.tif.ori new file mode 100644 index 00000000..3e3b7397 --- /dev/null +++ b/pyoptv/tests/testing_fodder/dumbbell/cam1.tif.ori @@ -0,0 +1,11 @@ +53.86516000 41.80704000 -250.30874000 + 0.17360000 2.93141000 -0.07124000 + + -0.9755121 -0.0696133 0.2086385 + -0.0341633 0.9850361 0.1689281 + -0.2172761 0.1576636 -0.9632929 + + 0.0000 0.0000 + 80.2700 + + 0.000100000000000 0.000010000000000 -91.500000000000000 diff --git a/pyoptv/tests/testing_fodder/dumbbell/cam2.tif.ori b/pyoptv/tests/testing_fodder/dumbbell/cam2.tif.ori new file mode 100644 index 00000000..00fddce6 --- /dev/null +++ b/pyoptv/tests/testing_fodder/dumbbell/cam2.tif.ori @@ -0,0 +1,11 @@ +-40.59589455 40.92752826 -239.13737242 + 0.17687217 3.31216947 0.05902948 + + -0.9837706 0.0581390 -0.1697508 + 0.0282589 0.9844464 0.1733978 + 0.1771918 0.1657867 -0.9701123 + + 0.0000 0.0000 + 78.4800 + + 0.000100000000000 0.000010000000000 -91.500000000000000 diff --git a/pyoptv/tests/testing_fodder/dumbbell/cam3.tif.ori b/pyoptv/tests/testing_fodder/dumbbell/cam3.tif.ori new file mode 100644 index 00000000..401ea153 --- /dev/null +++ b/pyoptv/tests/testing_fodder/dumbbell/cam3.tif.ori @@ -0,0 +1,11 @@ +-70.67046965 42.87867097 282.50941802 + -0.18216050 -0.28243198 -0.02534905 + + 0.9600720 0.0243421 -0.2786921 + 0.0255432 0.9844183 0.1739775 + 0.2785846 -0.1741496 0.9444906 + + 0.0000 0.0000 + 91.8500 + + 0.000100000000000 0.000010000000000 109.500000000000000 diff --git a/pyoptv/tests/testing_fodder/dumbbell/cam4.tif.ori b/pyoptv/tests/testing_fodder/dumbbell/cam4.tif.ori new file mode 100644 index 00000000..2577aecd --- /dev/null +++ b/pyoptv/tests/testing_fodder/dumbbell/cam4.tif.ori @@ -0,0 +1,11 @@ +43.09110995 29.48985646 272.33956208 + -0.12869588 0.17115349 0.01322342 + + 0.9853028 -0.0130298 0.1703191 + -0.0087433 0.9919324 0.1264657 + -0.1705929 -0.1260962 0.9772399 + + 0.0000 0.0000 + 84.7800 + + 0.000100000000000 0.000010000000000 109.500000000000000 diff --git a/pyoptv/tests/testing_fodder/frame/added.333 b/pyoptv/tests/testing_fodder/frame/added.333 new file mode 100644 index 00000000..1e170da5 --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/added.333 @@ -0,0 +1,11 @@ +10 + 0 0 -6.266 11.634 11.709 4 + -1 -2 -6.801 16.971 7.108 4 + 1 2 -7.449 5.945 3.302 4 + 2 8 -3.439 6.849 8.416 4 + 4 3 -10.113 10.579 8.117 4 + 3 6 -4.971 13.977 9.750 4 + -1 -2 -3.831 3.630 11.863 4 + -1 -2 -14.700 12.289 5.821 4 + 7 9 -0.570 10.498 0.640 4 + 8 10 -3.132 8.940 -4.511 4 diff --git a/pyoptv/tests/testing_fodder/frame/cam1.0333_targets b/pyoptv/tests/testing_fodder/frame/cam1.0333_targets new file mode 100644 index 00000000..1a77ed86 --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/cam1.0333_targets @@ -0,0 +1,14 @@ +13 + 0 900.0000 123.0000 900 30 30 10 -1 + 1 844.0000 194.0000 900 30 30 10 -1 + 2 921.0000 225.0000 900 30 30 10 -1 + 3 1083.0000 230.0000 900 30 30 10 -1 + 4 854.0000 245.0000 900 30 30 10 -1 + 5 973.0000 273.0000 900 30 30 10 -1 + 6 786.0000 303.0000 900 30 30 10 -1 + 7 875.0000 350.0000 900 30 30 10 -1 + 8 817.0000 369.0000 900 30 30 10 -1 + 9 939.0000 400.0000 900 30 30 10 -1 + 10 810.0000 436.0000 900 30 30 10 -1 + 11 798.0000 471.0000 900 30 30 10 -1 + 12 1156.0000 792.0000 900 30 30 10 -1 diff --git a/pyoptv/tests/testing_fodder/frame/cam1_reversed.0333_targets b/pyoptv/tests/testing_fodder/frame/cam1_reversed.0333_targets new file mode 100644 index 00000000..83659b37 --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/cam1_reversed.0333_targets @@ -0,0 +1,14 @@ +13 + 0 1156.0000 792.0000 900 30 30 10 -1 + 1 798.0000 471.0000 900 30 30 10 -1 + 2 810.0000 436.0000 900 30 30 10 -1 + 3 939.0000 400.0000 900 30 30 10 -1 + 4 817.0000 369.0000 900 30 30 10 -1 + 5 875.0000 350.0000 900 30 30 10 -1 + 6 786.0000 303.0000 900 30 30 10 -1 + 7 973.0000 273.0000 900 30 30 10 -1 + 8 854.0000 245.0000 900 30 30 10 -1 + 9 1083.0000 230.0000 900 30 30 10 -1 + 10 921.0000 225.0000 900 30 30 10 -1 + 11 844.0000 194.0000 900 30 30 10 -1 + 12 900.0000 123.0000 900 30 30 10 -1 diff --git a/pyoptv/tests/testing_fodder/frame/cam2.0333_targets b/pyoptv/tests/testing_fodder/frame/cam2.0333_targets new file mode 100644 index 00000000..12fe6281 --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/cam2.0333_targets @@ -0,0 +1,15 @@ +14 + 0 768.0000 138.0000 900 30 30 10 -1 + 1 728.0000 205.0000 900 30 30 10 -1 + 2 695.0000 239.0000 900 30 30 10 -1 + 3 620.0000 252.0000 900 30 30 10 -1 + 4 763.0000 261.0000 900 30 30 10 -1 + 5 956.0000 266.0000 900 30 30 10 -1 + 6 847.0000 299.0000 900 30 30 10 -1 + 7 586.0000 306.0000 900 30 30 10 -1 + 8 632.0000 363.0000 900 30 30 10 -1 + 9 679.0000 381.0000 900 30 30 10 -1 + 10 763.0000 423.0000 900 30 30 10 -1 + 11 696.0000 450.0000 900 30 30 10 -1 + 12 537.0000 476.0000 900 30 30 10 -1 + 13 978.0000 858.0000 900 30 30 10 -1 diff --git a/pyoptv/tests/testing_fodder/frame/cam3.0333_targets b/pyoptv/tests/testing_fodder/frame/cam3.0333_targets new file mode 100644 index 00000000..f1b35d6c --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/cam3.0333_targets @@ -0,0 +1,14 @@ +13 + 0 426.0000 20.0000 900 30 30 10 -1 + 1 415.0000 85.0000 900 30 30 10 -1 + 2 482.0000 101.0000 900 30 30 10 -1 + 3 228.0000 138.0000 900 30 30 10 -1 + 4 453.0000 165.0000 900 30 30 10 -1 + 5 545.0000 174.0000 900 30 30 10 -1 + 6 345.0000 185.0000 900 30 30 10 -1 + 7 460.0000 202.0000 900 30 30 10 -1 + 8 510.0000 281.0000 900 30 30 10 -1 + 9 388.0000 292.0000 900 30 30 10 -1 + 10 533.0000 310.0000 900 30 30 10 -1 + 11 515.0000 372.0000 900 30 30 10 -1 + 12 153.0000 723.0000 900 30 30 10 -1 diff --git a/pyoptv/tests/testing_fodder/frame/cam4.0333_targets b/pyoptv/tests/testing_fodder/frame/cam4.0333_targets new file mode 100644 index 00000000..9af3d087 --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/cam4.0333_targets @@ -0,0 +1,15 @@ +14 + 0 429.0000 60.0000 900 30 30 10 -1 + 1 508.0000 124.0000 900 30 30 10 -1 + 2 465.0000 139.0000 900 30 30 10 -1 + 3 576.0000 139.0000 900 30 30 10 -1 + 4 241.0000 178.0000 900 30 30 10 -1 + 5 426.0000 199.0000 900 30 30 10 -1 + 6 607.0000 209.0000 900 30 30 10 -1 + 7 345.0000 222.0000 900 30 30 10 -1 + 8 563.0000 238.0000 900 30 30 10 -1 + 9 509.0000 315.0000 900 30 30 10 -1 + 10 431.0000 327.0000 900 30 30 10 -1 + 11 648.0000 345.0000 900 30 30 10 -1 + 12 487.0000 403.0000 900 30 30 10 -1 + 13 208.0000 744.0000 900 30 30 10 -1 diff --git a/pyoptv/tests/testing_fodder/frame/ptv_is.333 b/pyoptv/tests/testing_fodder/frame/ptv_is.333 new file mode 100644 index 00000000..5b9850b1 --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/ptv_is.333 @@ -0,0 +1,11 @@ +10 + 0 0 -6.266 11.634 11.709 + -1 -2 -6.801 16.971 7.108 + 1 2 -7.449 5.945 3.302 + 2 8 -3.439 6.849 8.416 + 4 3 -10.113 10.579 8.117 + 3 6 -4.971 13.977 9.750 + -1 -2 -3.831 3.630 11.863 + -1 -2 -14.700 12.289 5.821 + 7 9 -0.570 10.498 0.640 + 8 10 -3.132 8.940 -4.511 diff --git a/pyoptv/tests/testing_fodder/frame/rt_is.333 b/pyoptv/tests/testing_fodder/frame/rt_is.333 new file mode 100644 index 00000000..5020e116 --- /dev/null +++ b/pyoptv/tests/testing_fodder/frame/rt_is.333 @@ -0,0 +1,11 @@ +10 + 1 -6.266 11.634 11.709 4 4 -1 5 + 2 -6.801 16.971 7.108 -1 0 0 0 + 3 -7.449 5.945 3.302 -1 10 9 10 + 4 -3.439 6.849 8.416 -1 9 8 9 + 5 -10.113 10.579 8.117 -1 6 6 7 + 6 -4.971 13.977 9.750 -1 1 2 2 + 7 -3.831 3.630 11.863 -1 11 11 12 + 8 -14.700 12.289 5.821 3 5 -1 4 + 9 -0.570 10.498 0.640 -1 7 5 6 + 10 -3.132 8.940 -4.511 -1 8 7 8 diff --git a/pyoptv/tests/testing_fodder/parameters/cal_ori.par b/pyoptv/tests/testing_fodder/parameters/cal_ori.par new file mode 100644 index 00000000..cd84e777 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/cal_ori.par @@ -0,0 +1,12 @@ +cal/calblock_20.txt +cal/cam1.tif +cal/cam1.tif.ori +cal/cam2.tif +cal/cam2.tif.ori +cal/cam3.tif +cal/cam3.tif.ori +cal/cam4.tif +cal/cam4.tif.ori +1 +0 +0 diff --git a/pyoptv/tests/testing_fodder/parameters/criteria.par b/pyoptv/tests/testing_fodder/parameters/criteria.par new file mode 100644 index 00000000..eb1dce69 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/criteria.par @@ -0,0 +1,12 @@ +-250 +-100 +100 +250 +-100 +100 +0.3 +0.3 +0.01 +0.01 +33 +1 diff --git a/pyoptv/tests/testing_fodder/parameters/detect_plate.par b/pyoptv/tests/testing_fodder/parameters/detect_plate.par new file mode 100644 index 00000000..742a31f0 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/detect_plate.par @@ -0,0 +1,13 @@ +20 +20 +20 +20 +50 +10 +1000 +5 +30 +5 +30 +500 +4 diff --git a/pyoptv/tests/testing_fodder/parameters/dumbbell.par b/pyoptv/tests/testing_fodder/parameters/dumbbell.par new file mode 100644 index 00000000..a86f1593 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/dumbbell.par @@ -0,0 +1,6 @@ +1.000000 +23.000000 +0.500000 +0.100000 +1 +500 diff --git a/pyoptv/tests/testing_fodder/parameters/examine.par b/pyoptv/tests/testing_fodder/parameters/examine.par new file mode 100644 index 00000000..aa47d0d4 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/examine.par @@ -0,0 +1,2 @@ +0 +0 diff --git a/pyoptv/tests/testing_fodder/parameters/man_ori.par b/pyoptv/tests/testing_fodder/parameters/man_ori.par new file mode 100644 index 00000000..7369264b --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/man_ori.par @@ -0,0 +1,16 @@ +1 +2 +4 +5 +1 +5 +16 +20 +1 +5 +16 +20 +1 +5 +16 +20 diff --git a/pyoptv/tests/testing_fodder/parameters/multi_planes.par b/pyoptv/tests/testing_fodder/parameters/multi_planes.par new file mode 100644 index 00000000..f6535f34 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/multi_planes.par @@ -0,0 +1,4 @@ +3 +img/calib_a_cam +img/calib_b_cam +img/calib_c_cam \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/parameters/orient.par b/pyoptv/tests/testing_fodder/parameters/orient.par new file mode 100644 index 00000000..66f4ca4a --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/orient.par @@ -0,0 +1,12 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/pyoptv/tests/testing_fodder/parameters/pft_version.par b/pyoptv/tests/testing_fodder/parameters/pft_version.par new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/pft_version.par @@ -0,0 +1 @@ +1 diff --git a/pyoptv/tests/testing_fodder/parameters/ptv.par b/pyoptv/tests/testing_fodder/parameters/ptv.par new file mode 100644 index 00000000..cce34e7a --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/ptv.par @@ -0,0 +1,21 @@ +4 +dumbbell/cam1_Scene77_4085 +cal/cam1.tif +dumbbell/cam2_Scene77_4085 +cal/cam2.tif +dumbbell/cam3_Scene77_4085 +cal/cam3.tif +dumbbell/cam4_Scene77_4085 +cal/cam4.tif +1 +0 +1 +1280 +1024 +0.017 +0.017 +0 +1 +1.49 +1.33 +5 diff --git a/pyoptv/tests/testing_fodder/parameters/sequence.par b/pyoptv/tests/testing_fodder/parameters/sequence.par new file mode 100644 index 00000000..514d4af4 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/sequence.par @@ -0,0 +1,6 @@ +dumbbell/cam1_Scene77_ +dumbbell/cam2_Scene77_ +dumbbell/cam3_Scene77_ +dumbbell/cam4_Scene77_ +497 +597 diff --git a/pyoptv/tests/testing_fodder/parameters/shaking.par b/pyoptv/tests/testing_fodder/parameters/shaking.par new file mode 100644 index 00000000..4ed0beec --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/shaking.par @@ -0,0 +1,4 @@ +410000 +411055 +100 +3 diff --git a/pyoptv/tests/testing_fodder/parameters/sortgrid.par b/pyoptv/tests/testing_fodder/parameters/sortgrid.par new file mode 100644 index 00000000..410b14d2 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/sortgrid.par @@ -0,0 +1 @@ +25 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/parameters/targ_rec.par b/pyoptv/tests/testing_fodder/parameters/targ_rec.par new file mode 100644 index 00000000..76eddbff --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/targ_rec.par @@ -0,0 +1,13 @@ +5 +5 +5 +5 +500 +30 +500 +5 +100 +5 +100 +200 +2 diff --git a/pyoptv/tests/testing_fodder/parameters/targ_rec_all_different_fields.par b/pyoptv/tests/testing_fodder/parameters/targ_rec_all_different_fields.par new file mode 100644 index 00000000..8e435da9 --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/targ_rec_all_different_fields.par @@ -0,0 +1,13 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 diff --git a/pyoptv/tests/testing_fodder/parameters/track.par b/pyoptv/tests/testing_fodder/parameters/track.par new file mode 100644 index 00000000..711263bd --- /dev/null +++ b/pyoptv/tests/testing_fodder/parameters/track.par @@ -0,0 +1,22 @@ +-2.0 +2.0 +-2.0 +2.0 +-2.0 +2.0 +120 +0.4 +1 +2 +1 +20 +5 +2 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/pyoptv/tests/testing_fodder/ptv_is.818 b/pyoptv/tests/testing_fodder/ptv_is.818 new file mode 100644 index 00000000..afa451d2 --- /dev/null +++ b/pyoptv/tests/testing_fodder/ptv_is.818 @@ -0,0 +1,81 @@ +80 + -1 -2 -7.011 -21.501 12.349 + -1 -2 36.471 -21.194 8.032 + 0 0 30.712 -24.193 -2.844 + 2 -2 5.339 -21.580 24.271 + 4 -2 15.261 -21.394 15.612 + 3 -2 -7.042 -26.648 12.453 + -1 -2 32.151 -21.596 0.759 + 9 -2 -4.959 -21.481 14.949 + 8 -2 31.805 -26.822 1.991 + 13 -2 9.583 -22.328 7.732 + -1 -2 29.265 -21.103 3.220 + -1 -2 28.049 -24.262 -5.598 + 14 -2 11.700 -25.519 22.126 + 16 -2 28.056 -27.064 4.365 + -1 -2 25.904 -19.870 23.958 + 18 -2 -0.718 -23.333 27.665 + 19 -2 38.988 -24.290 19.793 + -1 -2 39.399 -21.571 16.467 + -1 -2 21.353 -26.432 8.770 + 23 -2 17.411 -24.238 17.690 + 22 -2 1.460 -20.241 20.514 + 49 -2 29.964 -26.855 5.476 + -1 -2 22.031 -23.824 -2.434 + -1 -2 46.144 -11.354 -14.243 + -1 -2 9.559 -27.413 8.360 + -1 -2 -12.530 -21.663 -1.709 + 29 -2 3.415 -25.547 16.941 + 30 -2 2.749 -21.106 19.663 + 31 -2 15.368 -26.396 15.932 + 32 -2 -4.773 -20.271 21.659 + 37 -2 18.753 -21.682 28.538 + 36 -2 -12.376 -21.529 8.261 + 33 -2 -4.871 -25.378 22.165 + -1 -2 42.804 -17.877 17.192 + 35 -2 34.264 -24.097 1.231 + 78 -2 38.218 -26.769 28.695 + 40 -2 -11.926 -26.151 8.338 + -1 -2 -10.014 -22.533 15.683 + -1 -2 41.195 -29.150 15.181 + -1 -2 35.357 -28.461 5.777 + -1 -2 38.886 -22.541 27.330 + -1 -2 13.582 -21.537 15.219 + -1 -2 46.428 -21.184 18.236 + 45 -2 19.958 -21.548 30.494 + -1 -2 33.154 -21.303 -1.941 + -1 -2 30.988 -26.313 29.509 + 48 -2 10.561 -26.677 22.864 + -1 -2 26.827 1.093 -78.719 + 51 -2 43.296 -21.655 37.017 + -1 -2 25.796 -27.228 2.619 + -1 -2 16.972 -21.961 31.248 + -1 -2 38.401 -28.076 23.315 + -1 -2 34.418 -21.338 23.387 + 58 -2 5.805 -26.829 1.070 + 53 -2 17.034 -26.669 34.028 + 57 -2 -5.145 -26.814 16.195 + -1 -2 40.938 -23.954 25.872 + -1 -2 20.129 -18.745 21.287 + -1 -2 -5.240 -27.815 23.155 + 62 -2 -1.266 -26.233 -6.457 + 64 -2 42.785 -24.124 0.943 + -1 -2 46.226 -10.231 -18.858 + -1 -2 -16.153 -23.722 2.242 + -1 -2 42.194 -29.308 26.070 + -1 -2 23.942 -27.157 32.366 + -1 -2 -9.019 -20.723 15.812 + -1 -2 33.667 -16.808 -19.826 + 74 -2 -8.663 -24.198 -11.713 + 73 -2 3.013 -26.408 6.379 + -1 -2 51.143 -2.139 29.172 + -1 -2 29.066 -27.573 -8.127 + 77 -2 53.620 -20.393 10.205 + -1 -2 11.549 -2.768 -77.555 + -1 -2 33.667 -16.808 -19.826 + 74 -2 -8.663 -24.198 -11.713 + 73 -2 3.013 -26.408 6.379 + -1 -2 51.143 -2.139 29.172 + -1 -2 29.066 -27.573 -8.127 + 77 -2 53.620 -20.393 10.205 + -1 -2 11.549 -2.768 -77.555 diff --git a/pyoptv/tests/testing_fodder/rt_is.818 b/pyoptv/tests/testing_fodder/rt_is.818 new file mode 100644 index 00000000..43e6790f --- /dev/null +++ b/pyoptv/tests/testing_fodder/rt_is.818 @@ -0,0 +1,81 @@ +80 + 1 9.170 -20.365 20.781 57 74 32 49 + 2 6.526 -22.297 34.317 106 123 18 44 + 3 45.219 -20.269 25.946 96 66 26 26 + 4 33.024 -21.783 6.242 54 44 82 71 + 5 32.979 -24.271 -3.250 59 51 127 115 + 6 42.905 -21.814 25.693 113 73 41 27 + 7 4.341 -25.495 14.405 82 103 94 117 + 8 35.079 -21.146 6.780 53 41 72 63 + 9 33.056 -27.098 6.396 110 81 132 122 + 10 18.573 -21.424 27.034 83 95 27 41 + 11 53.554 -26.844 8.902 115 83 130 100 + 12 53.310 -21.422 10.863 70 49 81 39 + 13 34.755 -24.286 -7.157 44 42 137 127 + 14 -0.545 -26.370 15.841 87 118 101 133 + 15 17.146 -21.255 14.391 55 63 56 58 + 16 21.846 -22.655 13.419 67 68 71 85 + 17 34.723 -26.847 7.947 107 92 123 116 + 18 47.591 -21.233 10.068 66 45 73 48 + 19 49.490 -27.767 8.278 122 90 147 109 + 20 14.237 -23.933 3.655 56 61 109 113 + 21 11.522 -27.653 8.813 101 104 120 146 + 22 47.092 -21.252 21.216 91 -1 49 30 + 23 19.956 -24.978 25.574 -1 111 65 74 + 24 26.423 -21.875 29.213 -1 100 33 37 + 25 46.457 -26.892 23.656 -1 110 104 76 + 26 47.978 -27.029 24.095 -1 112 103 73 + 27 44.448 -26.828 24.199 -1 116 93 79 + 28 11.689 -23.919 36.164 -1 133 34 50 + 29 42.165 -23.164 15.011 93 -1 84 55 + 30 14.560 -19.792 16.757 50 -1 36 46 + 31 21.951 -21.705 30.464 -1 99 25 36 + 32 13.949 -23.709 13.856 75 82 -1 92 + 33 10.148 -24.958 11.096 -1 91 95 108 + 34 6.187 -25.686 34.921 -1 150 46 78 + 35 21.043 -22.770 22.574 89 -1 48 61 + 36 23.271 -24.997 23.825 114 -1 69 81 + 37 29.558 -29.612 -2.433 108 89 -1 165 + 38 6.210 -21.888 22.360 71 93 -1 67 + 39 33.566 -21.522 10.286 -1 57 70 65 + 40 31.568 -21.605 9.458 58 58 67 -1 + 41 1.662 -22.751 22.988 76 -1 40 82 + 42 36.576 -26.506 -1.884 81 -1 148 137 + 43 32.875 -18.697 -3.807 29 -1 79 64 + 44 52.778 -24.284 8.790 95 -1 112 77 + 45 50.874 -25.538 38.626 -1 138 55 32 + 46 62.750 -24.533 4.234 103 56 -1 69 + 47 52.834 -19.582 24.969 84 -1 23 24 + 48 0.284 -24.110 38.684 -1 149 22 56 + 49 33.202 -26.988 9.729 -1 97 124 110 + 50 17.389 -23.628 15.171 -1 87 76 86 + 51 -14.071 0.678 22.782 23 26 12 -1 + 52 -14.388 -23.736 2.817 40 -1 97 140 + 53 9.706 -27.131 28.655 -1 145 75 94 + 54 -11.490 -21.790 -1.759 -1 48 90 135 + 55 -16.157 -23.739 2.240 38 -1 98 142 + 56 -2.436 11.644 -8.389 15 -1 11 11 + 57 55.251 -21.039 14.385 80 -1 68 31 + 58 -0.239 -29.377 6.828 -1 121 146 171 + 59 39.372 -26.921 -12.514 65 47 -1 157 + 60 16.971 -22.894 29.463 -1 105 37 51 + 61 9.888 -29.845 8.361 116 124 -1 162 + 62 -10.014 -21.628 17.482 49 85 39 -1 + 63 16.288 -28.258 20.087 -1 141 108 124 + 64 14.316 -29.324 14.997 -1 131 129 141 + 65 30.292 -3.467 -80.573 -1 15 113 68 + 66 13.464 24.797 9.571 6 5 4 -1 + 67 19.786 -29.521 21.236 -1 143 116 131 + 68 46.320 -26.133 4.619 97 -1 138 104 + 69 1.699 -28.649 5.167 98 -1 144 164 + 70 41.179 -21.692 -13.638 31 33 133 -1 + 71 61.758 -18.985 5.890 47 37 -1 28 + 72 -1.664 11.511 22.846 19 18 9 -1 + 73 -12.714 -21.612 -1.050 -1 55 87 125 + 74 -8.639 -24.216 -11.870 -1 46 134 170 + 75 44.538 -11.667 -42.637 26 23 105 -1 + 76 25.250 -20.971 13.559 62 -1 57 54 + 77 54.890 19.986 9.558 10 7 7 -1 + 78 8.673 -25.192 26.434 -1 -1 63 88 + 79 9.223 -25.463 20.951 112 -1 78 95 + 80 49.717 -26.877 3.661 -1 69 -1 111 diff --git a/pyoptv/tests/testing_fodder/sample_0001_targets b/pyoptv/tests/testing_fodder/sample_0001_targets new file mode 100644 index 00000000..77ac542d --- /dev/null +++ b/pyoptv/tests/testing_fodder/sample_0001_targets @@ -0,0 +1,2 @@ +0 + diff --git a/pyoptv/tests/testing_fodder/sample_0042_targets b/pyoptv/tests/testing_fodder/sample_0042_targets new file mode 100644 index 00000000..35fae4d5 --- /dev/null +++ b/pyoptv/tests/testing_fodder/sample_0042_targets @@ -0,0 +1,3 @@ +2 + 0 1127.0000 796.0000 13320 111 120 828903 1 + 1 796.0000 809.0000 13108 113 116 658928 0 diff --git a/pyoptv/tests/testing_fodder/sequence_parameters/sequence.par b/pyoptv/tests/testing_fodder/sequence_parameters/sequence.par new file mode 100644 index 00000000..514d4af4 --- /dev/null +++ b/pyoptv/tests/testing_fodder/sequence_parameters/sequence.par @@ -0,0 +1,6 @@ +dumbbell/cam1_Scene77_ +dumbbell/cam2_Scene77_ +dumbbell/cam3_Scene77_ +dumbbell/cam4_Scene77_ +497 +597 diff --git a/pyoptv/tests/testing_fodder/single_cam/calibration/calblock.txt b/pyoptv/tests/testing_fodder/single_cam/calibration/calblock.txt new file mode 100755 index 00000000..0f9047ce --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/calibration/calblock.txt @@ -0,0 +1,25 @@ +1 0 0 0 +2 25 0 0 +3 50 0 0 +4 75 0 0 +5 100 0 0 +6 0 25 0 +7 25 25 0 +8 50 25 0 +9 75 25 0 +10 100 25 0 +11 0 50 0 +12 25 50 0 +13 50 50 0 +14 75 50 0 +15 100 50 0 +16 0 75 0 +17 25 75 0 +18 50 75 0 +19 75 75 0 +20 100 75 0 +21 0 100 0 +22 25 100 0 +23 50 100 0 +24 75 100 0 +25 100 100 0 diff --git a/pyoptv/tests/testing_fodder/single_cam/calibration/cam_1.tif.addpar b/pyoptv/tests/testing_fodder/single_cam/calibration/cam_1.tif.addpar new file mode 100755 index 00000000..ce153c2d --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/calibration/cam_1.tif.addpar @@ -0,0 +1 @@ +0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/single_cam/calibration/cam_1.tif.ori b/pyoptv/tests/testing_fodder/single_cam/calibration/cam_1.tif.ori new file mode 100755 index 00000000..e2ed76c3 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/calibration/cam_1.tif.ori @@ -0,0 +1,11 @@ + 0.0 0.0 100.0 + 0.0 0.0 0.0 + + 0.8667581 0.1250404 -0.4827995 + -0.0212693 0.9764471 0.2147060 + 0.4982751 -0.1758293 0.8490029 + + 0.0 0.0 + 100.0 + + 0.000000000000000 0.000000000000000 1.000000000000000 diff --git a/pyoptv/tests/testing_fodder/single_cam/img/img.10004 b/pyoptv/tests/testing_fodder/single_cam/img/img.10004 new file mode 100755 index 00000000..a29607b3 Binary files /dev/null and b/pyoptv/tests/testing_fodder/single_cam/img/img.10004 differ diff --git a/pyoptv/tests/testing_fodder/single_cam/img/img.10005 b/pyoptv/tests/testing_fodder/single_cam/img/img.10005 new file mode 100755 index 00000000..f63b962d Binary files /dev/null and b/pyoptv/tests/testing_fodder/single_cam/img/img.10005 differ diff --git a/pyoptv/tests/testing_fodder/single_cam/img/img.10006 b/pyoptv/tests/testing_fodder/single_cam/img/img.10006 new file mode 100755 index 00000000..380b8f14 Binary files /dev/null and b/pyoptv/tests/testing_fodder/single_cam/img/img.10006 differ diff --git a/pyoptv/tests/testing_fodder/single_cam/img/img.10007 b/pyoptv/tests/testing_fodder/single_cam/img/img.10007 new file mode 100755 index 00000000..4441f2e2 Binary files /dev/null and b/pyoptv/tests/testing_fodder/single_cam/img/img.10007 differ diff --git a/pyoptv/tests/testing_fodder/single_cam/img/img.10008 b/pyoptv/tests/testing_fodder/single_cam/img/img.10008 new file mode 100755 index 00000000..18080952 Binary files /dev/null and b/pyoptv/tests/testing_fodder/single_cam/img/img.10008 differ diff --git a/pyoptv/tests/testing_fodder/single_cam/img/img.10009 b/pyoptv/tests/testing_fodder/single_cam/img/img.10009 new file mode 100755 index 00000000..32a735ac Binary files /dev/null and b/pyoptv/tests/testing_fodder/single_cam/img/img.10009 differ diff --git a/pyoptv/tests/testing_fodder/single_cam/img/img.10010 b/pyoptv/tests/testing_fodder/single_cam/img/img.10010 new file mode 100755 index 00000000..465b8a6e Binary files /dev/null and b/pyoptv/tests/testing_fodder/single_cam/img/img.10010 differ diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/cal_ori.par b/pyoptv/tests/testing_fodder/single_cam/parameters/cal_ori.par new file mode 100755 index 00000000..196516c3 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/cal_ori.par @@ -0,0 +1,6 @@ +calibration/calblock.txt +calibration/cam_1.tif +calibration/cam_1.tif.ori +0 +0 +0 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/criteria.par b/pyoptv/tests/testing_fodder/single_cam/parameters/criteria.par new file mode 100755 index 00000000..11f0916a --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/criteria.par @@ -0,0 +1,12 @@ +-100 +-1 +1 +100 +-1 +1 +0.02 +0.02 +0.02 +0.02 +33 +0.05 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/detect_plate.par b/pyoptv/tests/testing_fodder/single_cam/parameters/detect_plate.par new file mode 100755 index 00000000..bcb80d1b --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/detect_plate.par @@ -0,0 +1,13 @@ +10 +8 +8 +8 +250 +49 +900 +7 +30 +7 +30 +30 +3 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/dumbbell.par b/pyoptv/tests/testing_fodder/single_cam/parameters/dumbbell.par new file mode 100755 index 00000000..43ab575b --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/dumbbell.par @@ -0,0 +1,6 @@ +0.000000 +0.000000 +0.000000 +0.000000 +0 +0 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/examine.par b/pyoptv/tests/testing_fodder/single_cam/parameters/examine.par new file mode 100755 index 00000000..aa47d0d4 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/examine.par @@ -0,0 +1,2 @@ +0 +0 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/man_ori.par b/pyoptv/tests/testing_fodder/single_cam/parameters/man_ori.par new file mode 100755 index 00000000..b29c18ce --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/man_ori.par @@ -0,0 +1,16 @@ +109 +114 +125 +143 +109 +114 +125 +143 +109 +114 +125 +143 +109 +114 +125 +143 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/multi_planes.par b/pyoptv/tests/testing_fodder/single_cam/parameters/multi_planes.par new file mode 100755 index 00000000..f6535f34 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/multi_planes.par @@ -0,0 +1,4 @@ +3 +img/calib_a_cam +img/calib_b_cam +img/calib_c_cam \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/orient.par b/pyoptv/tests/testing_fodder/single_cam/parameters/orient.par new file mode 100755 index 00000000..66f4ca4a --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/orient.par @@ -0,0 +1,12 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/pft_version.par b/pyoptv/tests/testing_fodder/single_cam/parameters/pft_version.par new file mode 100755 index 00000000..573541ac --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/pft_version.par @@ -0,0 +1 @@ +0 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/ptv.par b/pyoptv/tests/testing_fodder/single_cam/parameters/ptv.par new file mode 100755 index 00000000..d5778d42 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/ptv.par @@ -0,0 +1,15 @@ +1 +img/img.10004 +calibration/cam_1.tif +1 +0 +0 +320 +240 +0.01 +0.01 +0 +1 +1. +1. +1.0 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/sequence.par b/pyoptv/tests/testing_fodder/single_cam/parameters/sequence.par new file mode 100755 index 00000000..a76e6eff --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/sequence.par @@ -0,0 +1,3 @@ +img/img. +10004 +10103 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/shaking.par b/pyoptv/tests/testing_fodder/single_cam/parameters/shaking.par new file mode 100755 index 00000000..4da5c8f6 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/shaking.par @@ -0,0 +1,4 @@ +20 +30 +20 +5 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/sortgrid.par b/pyoptv/tests/testing_fodder/single_cam/parameters/sortgrid.par new file mode 100755 index 00000000..9a037142 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/sortgrid.par @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/targ_rec.par b/pyoptv/tests/testing_fodder/single_cam/parameters/targ_rec.par new file mode 100755 index 00000000..00dcfcb9 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/targ_rec.par @@ -0,0 +1,13 @@ +5 +5 +5 +5 +80 +4 +320000 +2 +400 +2 +800 +30 +2 diff --git a/pyoptv/tests/testing_fodder/single_cam/parameters/track.par b/pyoptv/tests/testing_fodder/single_cam/parameters/track.par new file mode 100755 index 00000000..215eee32 --- /dev/null +++ b/pyoptv/tests/testing_fodder/single_cam/parameters/track.par @@ -0,0 +1,9 @@ +-0.2 +0.2 +-0.2 +0.2 +-0.2 +0.2 +150 +0.5 +1 diff --git a/pyoptv/tests/testing_fodder/target_parameters/targ_rec.par b/pyoptv/tests/testing_fodder/target_parameters/targ_rec.par new file mode 100644 index 00000000..f96fc10b --- /dev/null +++ b/pyoptv/tests/testing_fodder/target_parameters/targ_rec.par @@ -0,0 +1,13 @@ +3 +2 +2 +3 +5 +3 +100 +1 +20 +1 +20 +3 +2 diff --git a/pyoptv/tests/testing_fodder/track/cal/cam1.tif.addpar b/pyoptv/tests/testing_fodder/track/cal/cam1.tif.addpar new file mode 100644 index 00000000..ce153c2d --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/cal/cam1.tif.addpar @@ -0,0 +1 @@ +0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 \ No newline at end of file diff --git a/pyoptv/tests/testing_fodder/track/cal/sym_cam1.tif.ori b/pyoptv/tests/testing_fodder/track/cal/sym_cam1.tif.ori new file mode 100644 index 00000000..ade4dbf7 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/cal/sym_cam1.tif.ori @@ -0,0 +1,11 @@ + -250.0 1.0 250.0 + 0.0 -0.785 0.0 + + 0.9392483 0.0263300 -0.3422270 + 0.0288852 0.9874532 0.1552479 + 0.3420208 -0.1557016 0.9267032 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/track/cal/sym_cam2.tif.ori b/pyoptv/tests/testing_fodder/track/cal/sym_cam2.tif.ori new file mode 100644 index 00000000..3302c5da --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/cal/sym_cam2.tif.ori @@ -0,0 +1,11 @@ + 250.0 1.0 250.0 + 0.0 0.785 0.00 + + 0.9737110 -0.0296651 0.2258471 + -0.0150095 0.9809730 0.1935630 + -0.2272920 -0.1918643 0.9547389 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/track/cal/sym_cam3.tif.ori b/pyoptv/tests/testing_fodder/track/cal/sym_cam3.tif.ori new file mode 100644 index 00000000..367ef354 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/cal/sym_cam3.tif.ori @@ -0,0 +1,11 @@ + 250.0 1.0 -250.0 + 0.0 2.356 0.0 + + -0.9313307 0.0698436 -0.3574144 + -0.0156770 0.9728379 0.2309559 + 0.3638371 0.2206995 -0.9049388 + + 0 0 + 100.0000 + + 0.000100000000000 0.000010000000000 -100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/track/cal/sym_cam4.tif.ori b/pyoptv/tests/testing_fodder/track/cal/sym_cam4.tif.ori new file mode 100644 index 00000000..f0857814 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/cal/sym_cam4.tif.ori @@ -0,0 +1,11 @@ + -250.0 1.0 -250.0 + 0.0 -2.356 0.0 + + -0.9594955 -0.0529875 0.2766960 + 0.0171315 0.9693616 0.2450401 + -0.2812026 0.2398551 -0.9291903 + + 0 0 + 100.0000 + + 0.000100000000000 0.000001000000000 -100.000000000000000 diff --git a/pyoptv/tests/testing_fodder/track/conf.yaml b/pyoptv/tests/testing_fodder/track/conf.yaml new file mode 100644 index 00000000..28b29a44 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/conf.yaml @@ -0,0 +1,47 @@ +# Values taken from the parameters used in the C test for adding a particle. + +cameras: + - ori_file: testing_fodder/track/cal/sym_cam1.tif.ori + addpar_file: testing_fodder/track/cal/cam1.tif.addpar + + - ori_file: testing_fodder/track/cal/sym_cam2.tif.ori + addpar_file: testing_fodder/track/cal/cam1.tif.addpar + + - ori_file: testing_fodder/track/cal/sym_cam3.tif.ori + addpar_file: testing_fodder/track/cal/cam1.tif.addpar + +scene: + flags: hp, headers + image_size: [ 1920, 1080 ] + pixel_size: [ 0.00556, 0.00556 ] + + # Multimedia parameters: + cam_side_n: 1 + object_side_n: 1 + wall_ns: [ 1 ] + wall_thicks: [ 0 ] + +correspondences: + x_span: [-30., 30.] + z_spans: + - [-20., 20.] + - [-20., 20.] + + pixels_x: 0.02 + pixels_y: 0.02 + pixels_tot: 0.02 + + ref_gray: 0.02 + min_correlation: 33 + epipolar_band: 0.15 # mm on sensor plane. + +sequence: + targets_template: testing_fodder/track/newpart/cam{cam:1d}. + first: 10001 + last: 10005 + +tracking: + velocity_lims: [[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]] + angle_lim: 110 + accel_lim: 0.5 + add_particle: 1 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam1.10000_targets b/pyoptv/tests/testing_fodder/track/newpart/cam1.10000_targets new file mode 100644 index 00000000..00bae724 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam1.10000_targets @@ -0,0 +1,2 @@ +1 + 0 967.161 590.871 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam1.10001_targets b/pyoptv/tests/testing_fodder/track/newpart/cam1.10001_targets new file mode 100644 index 00000000..fc331a1c --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam1.10001_targets @@ -0,0 +1,2 @@ +1 + 0 967.1610 590.8710 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam1.10002_targets b/pyoptv/tests/testing_fodder/track/newpart/cam1.10002_targets new file mode 100644 index 00000000..30990dac --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam1.10002_targets @@ -0,0 +1,2 @@ +1 + 0 967.5210 590.8700 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam1.10003_targets b/pyoptv/tests/testing_fodder/track/newpart/cam1.10003_targets new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam1.10003_targets @@ -0,0 +1 @@ +0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam1.10004_targets b/pyoptv/tests/testing_fodder/track/newpart/cam1.10004_targets new file mode 100644 index 00000000..4a505587 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam1.10004_targets @@ -0,0 +1,2 @@ +1 + 0 968.2400 590.8680 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam1.10005_targets b/pyoptv/tests/testing_fodder/track/newpart/cam1.10005_targets new file mode 100644 index 00000000..83e102e1 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam1.10005_targets @@ -0,0 +1,2 @@ +1 + 0 968.6000 590.8670 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam2.10000_targets b/pyoptv/tests/testing_fodder/track/newpart/cam2.10000_targets new file mode 100644 index 00000000..b19e15dd --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam2.10000_targets @@ -0,0 +1,2 @@ +1 + 0 952.839 590.871 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam2.10001_targets b/pyoptv/tests/testing_fodder/track/newpart/cam2.10001_targets new file mode 100644 index 00000000..a2e28d90 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam2.10001_targets @@ -0,0 +1,2 @@ +1 + 0 952.8390 590.8710 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam2.10002_targets b/pyoptv/tests/testing_fodder/track/newpart/cam2.10002_targets new file mode 100644 index 00000000..6f71ab82 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam2.10002_targets @@ -0,0 +1,2 @@ +1 + 0 953.1990 590.8720 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam2.10003_targets b/pyoptv/tests/testing_fodder/track/newpart/cam2.10003_targets new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam2.10003_targets @@ -0,0 +1 @@ +0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam2.10004_targets b/pyoptv/tests/testing_fodder/track/newpart/cam2.10004_targets new file mode 100644 index 00000000..41b2e8dc --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam2.10004_targets @@ -0,0 +1,2 @@ +1 + 0 953.9180 590.8740 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam2.10005_targets b/pyoptv/tests/testing_fodder/track/newpart/cam2.10005_targets new file mode 100644 index 00000000..d2a78097 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam2.10005_targets @@ -0,0 +1,2 @@ +1 + 0 954.2780 590.8750 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam3.10000_targets b/pyoptv/tests/testing_fodder/track/newpart/cam3.10000_targets new file mode 100644 index 00000000..9bd0364e --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam3.10000_targets @@ -0,0 +1,2 @@ +1 + 0 2362.210 591.025 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam3.10001_targets b/pyoptv/tests/testing_fodder/track/newpart/cam3.10001_targets new file mode 100644 index 00000000..bfca57d7 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam3.10001_targets @@ -0,0 +1,2 @@ +1 + 0 956.5020 590.8710 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam3.10002_targets b/pyoptv/tests/testing_fodder/track/newpart/cam3.10002_targets new file mode 100644 index 00000000..3e84755f --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam3.10002_targets @@ -0,0 +1,2 @@ +1 + 0 956.1420 590.8720 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam3.10003_targets b/pyoptv/tests/testing_fodder/track/newpart/cam3.10003_targets new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam3.10003_targets @@ -0,0 +1 @@ +0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam3.10004_targets b/pyoptv/tests/testing_fodder/track/newpart/cam3.10004_targets new file mode 100644 index 00000000..fdaa0479 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam3.10004_targets @@ -0,0 +1,2 @@ +1 + 0 955.4230 590.8740 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/newpart/cam3.10005_targets b/pyoptv/tests/testing_fodder/track/newpart/cam3.10005_targets new file mode 100644 index 00000000..2cde9f59 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/newpart/cam3.10005_targets @@ -0,0 +1,2 @@ +1 + 0 955.0630 590.8750 100 10 10 10000 0 diff --git a/pyoptv/tests/testing_fodder/track/res_orig/particles.10001 b/pyoptv/tests/testing_fodder/track/res_orig/particles.10001 new file mode 100644 index 00000000..75f2f924 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/res_orig/particles.10001 @@ -0,0 +1,2 @@ +1 + 1 0.000 0.000 0.000 0 0 0 0 diff --git a/pyoptv/tests/testing_fodder/track/res_orig/particles.10002 b/pyoptv/tests/testing_fodder/track/res_orig/particles.10002 new file mode 100644 index 00000000..e98e0ce1 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/res_orig/particles.10002 @@ -0,0 +1,2 @@ +1 + 1 0.010 0.000 0.000 0 0 0 0 diff --git a/pyoptv/tests/testing_fodder/track/res_orig/particles.10003 b/pyoptv/tests/testing_fodder/track/res_orig/particles.10003 new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/res_orig/particles.10003 @@ -0,0 +1 @@ +0 diff --git a/pyoptv/tests/testing_fodder/track/res_orig/particles.10004 b/pyoptv/tests/testing_fodder/track/res_orig/particles.10004 new file mode 100644 index 00000000..aeb016e0 --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/res_orig/particles.10004 @@ -0,0 +1,2 @@ +1 + 1 0.030 0.000 0.000 0 0 0 0 diff --git a/pyoptv/tests/testing_fodder/track/res_orig/particles.10005 b/pyoptv/tests/testing_fodder/track/res_orig/particles.10005 new file mode 100644 index 00000000..ced1c1ec --- /dev/null +++ b/pyoptv/tests/testing_fodder/track/res_orig/particles.10005 @@ -0,0 +1,2 @@ +1 + 1 0.040 0.000 0.000 0 0 0 0 diff --git a/pyoptv/tests/testing_fodder/tracking_parameters/track.par b/pyoptv/tests/testing_fodder/tracking_parameters/track.par new file mode 100644 index 00000000..c1373a70 --- /dev/null +++ b/pyoptv/tests/testing_fodder/tracking_parameters/track.par @@ -0,0 +1,13 @@ +111.111 +222.222 +333.333 +444.444 +555.555 +666.666 +777.777 +888.888 +9 +10 +11 +12 +13 diff --git a/pyoptv/tests/testing_fodder/volume_parameters/volume.par b/pyoptv/tests/testing_fodder/volume_parameters/volume.par new file mode 100644 index 00000000..2a81bbd3 --- /dev/null +++ b/pyoptv/tests/testing_fodder/volume_parameters/volume.par @@ -0,0 +1,12 @@ +111.111 +333.333 +555.555 +222.222 +444.444 +666.666 +777.777 +888.888 +999.999 +1010.1010 +1111.1111 +1212.1212