initial commit

This commit is contained in:
unknown
2025-12-21 17:15:00 +08:00
commit f38b2138cb
86 changed files with 3748 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
# OrisysSDK API 文档
本文件提供了 OrisysSDK 的各类方法
---
## `Sensor` 类别
### 描述
创建一个传感器实例
### 输入参数
* **cam_id** (`int | str`, 可选): 传感器 ID、序列号或视频路径。默认为 `0`。
* **isstitch** (`bool`,可选): 是否启用图像拼接,默认为 `True`。
* **cuda** (`bool`,可选): 是否使用 GPU cuda 加速,默认为 `False`。
* **cfg** (`str`,可选): 指定拼接标定数据,默认为 `/config/cal.npy`。
### 返回
* `Sensor` 对象
### 示例
```python
# Example 1: 用相机编号创建
from xensesdk import Sensor
from src.sensor import Sensor
sensor = Sensor(0)
# Example 2: 用视频文件创建
sensor = Sensor("/data/data.mp4")
# Example 3: 用相机编号开启
sensor = Sensor.create(0)
```
---
## 2. `get_img` 方法
### 描述
从传感器获取图像,进行复眼图像提取和拼接。
### 返回
* 提取(`issitch = False`)或拼接后(`issitch = True`)的图像
---
## 3. `compute_deformation` 方法
### 描述
计算传感器变形和应力
---
## 4. `compute_deformation` 方法
### 描述
计算传感器变形和应力
---
## 常见问题解答 (FAQ)
TBC
+388
View File
@@ -0,0 +1,388 @@
"""
Build script to create PyArmor-obfuscated distributions for multiple Python versions.
This script builds separate wheels for Python 3.8, 3.9, and 3.10 for Windows and Linux.
"""
import os
import shutil
import subprocess
import sys
import platform
from pathlib import Path
# Python versions to build for
PYTHON_VERSIONS = ['3.8', '3.9', '3.10']
# Detect platform
IS_WINDOWS = platform.system() == 'Windows'
IS_LINUX = platform.system() == 'Linux'
PLATFORM_NAME = 'win' if IS_WINDOWS else 'linux' if IS_LINUX else 'unknown'
def clean_build_dirs():
"""Remove previous build artifacts."""
dirs_to_clean = ['build', 'dist', 'orisys.egg-info', 'dist_obfuscated']
for dir_name in dirs_to_clean:
if os.path.exists(dir_name):
print(f"Cleaning {dir_name}...")
shutil.rmtree(dir_name)
# Ensure dist directory exists with platform subdirectories
os.makedirs(f'dist/{PLATFORM_NAME}', exist_ok=True)
def get_python_executable(version):
"""Get the Python executable for a specific version based on platform."""
# First check if current Python matches the requested version
current_version = f"{sys.version_info.major}.{sys.version_info.minor}"
if current_version == version:
return [sys.executable]
if IS_WINDOWS:
# Windows paths - including conda environments
username = os.environ.get("USERNAME", "")
possible_paths = [
f'py -{version}', # Windows py launcher
f'python{version}', # Direct version command
# Conda environments with various naming patterns
f'C:\\Users\\{username}\\anaconda3\\envs\\py{version.replace(".", "")}\\python.exe',
f'C:\\Users\\{username}\\anaconda3\\envs\\py{version.replace(".", "")}-env\\python.exe',
f'C:\\Users\\{username}\\anaconda3\\envs\\python{version.replace(".", "")}\\python.exe',
f'C:\\Users\\{username}\\anaconda3\\envs\\orisys-py{version.replace(".", "")}\\python.exe',
f'C:\\Users\\{username}\\miniconda3\\envs\\py{version.replace(".", "")}\\python.exe',
# Standard Python installations
f'C:\\Python{version.replace(".", "")}\\python.exe',
f'C:\\Program Files\\Python{version.replace(".", "")}\\python.exe',
]
else:
# Linux/Unix paths
possible_paths = [
f'python{version}',
f'/usr/bin/python{version}',
f'/usr/local/bin/python{version}',
f'~/.pyenv/versions/{version}/bin/python',
f'/opt/python{version}/bin/python',
]
for path in possible_paths:
try:
# Expand user home directory
if path.startswith('~'):
path = os.path.expanduser(path)
if IS_WINDOWS and path.startswith('py -'):
# Test Windows py launcher
result = subprocess.run(
path.split() + ['-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'],
capture_output=True, text=True, shell=True
)
if result.returncode == 0 and result.stdout.strip() == version:
return path.split()
else:
# Check if file exists first
if not os.path.exists(path):
continue
result = subprocess.run(
[path, '-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'],
capture_output=True, text=True
)
if result.returncode == 0 and result.stdout.strip() == version:
return [path]
except (FileNotFoundError, subprocess.SubprocessError, PermissionError):
continue
return None
def obfuscate_code():
"""Obfuscate the source code using PyArmor (version-independent)."""
print("\n=== Obfuscating code with PyArmor ===")
# PyArmor 9.x generates version-independent obfuscated code
# Output directly to dist_obfuscated/src (PyArmor will create orisys subdirectory)
if IS_WINDOWS:
cmd = 'pyarmor gen --output dist_obfuscated/src --recursive src/orisys'
print(f"Running: {cmd}")
result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
else:
cmd = [
'pyarmor',
'gen',
'--output', 'dist_obfuscated/src',
'--recursive',
'src/orisys'
]
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error obfuscating code:\n{result.stderr}")
print(f"stdout:\n{result.stdout}")
sys.exit(1)
print("Code obfuscation successful!")
print(result.stdout)
return True
def copy_additional_files():
"""Copy non-Python files that don't need obfuscation."""
print("\n=== Copying additional files ===")
# Copy config directory
if os.path.exists('config'):
print("Copying config directory...")
shutil.copytree('config', 'dist_obfuscated/config', dirs_exist_ok=True)
# Copy README and other metadata files
files_to_copy = ['README.md', 'Readme(未完成).md', 'requirements.txt', 'LICENSE']
for file in files_to_copy:
if os.path.exists(file):
print(f"Copying {file}...")
shutil.copy(file, f'dist_obfuscated/{file}')
def create_setup_for_obfuscated(python_version=None):
"""Create a modified setup.py for the obfuscated package."""
print(f"\n=== Creating setup.py for obfuscated package (Python {python_version or '3.8-3.10'}) ===")
if python_version:
version_constraint = f'=={python_version}.*'
py_tag = f'py{python_version.replace(".", "")}'
else:
version_constraint = '>=3.8,<3.11'
py_tag = 'py38.py39.py310'
setup_content = f'''from setuptools import setup, find_packages
import os
from pathlib import Path
# Read requirements
with open('requirements.txt', 'r') as f:
requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')]
# Find all packages including pyarmor_runtime
packages = find_packages(where="src")
# Collect all package data including PyArmor runtime files
package_data = {{}}
for pkg in packages:
pkg_path = Path('src') / pkg.replace('.', '/')
if pkg_path.exists():
# Include all .pyd, .so, .dll files (PyArmor runtime)
data_files = []
for pattern in ['*.pyd', '*.so', '*.dll', '*.dylib']:
data_files.extend([str(f.relative_to(pkg_path)) for f in pkg_path.glob(pattern)])
if data_files:
package_data[pkg] = data_files
setup(
name="orisys",
version="0.1.0",
description="Orisys - Optical Tactile Sensor SDK (Protected)",
author="Your Name",
package_dir={{"": "src"}},
packages=packages,
package_data=package_data,
include_package_data=True,
install_requires=requirements,
python_requires="{version_constraint}",
# Include PyArmor runtime files
zip_safe=False,
options={{
'bdist_wheel': {{
'python_tag': '{py_tag}',
}}
}},
)
'''
with open('dist_obfuscated/setup.py', 'w') as f:
f.write(setup_content)
print("setup.py created successfully!")
def get_platform_tag():
"""Get the platform tag for wheel naming."""
system = platform.system()
machine = platform.machine().lower()
if system == 'Windows':
if machine in ('amd64', 'x86_64'):
return 'win_amd64'
elif machine in ('x86', 'i386', 'i686'):
return 'win32'
elif machine in ('arm64', 'aarch64'):
return 'win_arm64'
elif system == 'Linux':
if machine in ('x86_64', 'amd64'):
return 'manylinux2014_x86_64'
elif machine in ('aarch64', 'arm64'):
return 'manylinux2014_aarch64'
elif machine in ('i386', 'i686'):
return 'manylinux2014_i686'
elif system == 'Darwin':
if machine == 'arm64':
return 'macosx_11_0_arm64'
else:
return 'macosx_10_9_x86_64'
return 'any'
def build_wheel_for_version(python_version):
"""Build the wheel distribution for a specific Python version."""
print(f"\n{'='*60}")
print(f"Building wheel for Python {python_version} on {PLATFORM_NAME}")
print('='*60)
python_cmd = get_python_executable(python_version)
if not python_cmd:
print(f"⚠️ Python {python_version} not found on {PLATFORM_NAME}, skipping...")
return False
print(f"Using Python: {' '.join(python_cmd)}")
# Verify Python version
result = subprocess.run(
python_cmd + ['-c', 'import sys; print(f"Python {sys.version}")'],
capture_output=True, text=True
)
print(result.stdout.strip())
# Get platform tag
plat_tag = get_platform_tag()
py_tag = f'py{python_version.replace(".", "")}'
os.chdir('dist_obfuscated')
# Build the wheel using setup.py with explicit tags
cmd = python_cmd + ['setup.py', 'bdist_wheel',
'--python-tag', py_tag,
'--plat-name', plat_tag,
'--dist-dir', f'../dist/{PLATFORM_NAME}']
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
os.chdir('..')
if result.returncode != 0:
print(f"❌ Error building wheel for Python {python_version}:\n{result.stderr}")
return False
print(f"✅ Wheel for Python {python_version} on {PLATFORM_NAME} built successfully!")
print(f" Expected: orisys-0.1.0-{py_tag}-none-{plat_tag}.whl")
return True
def build_universal_wheel():
"""Build a single universal wheel compatible with all Python versions."""
print(f"\n{'='*60}")
print(f"Building Universal Wheel (Python 3.8-3.10) for {PLATFORM_NAME}")
print('='*60)
os.chdir('dist_obfuscated')
# Build the wheel
cmd = [sys.executable, '-m', 'pip', 'wheel', '.', '--no-deps', '-w', f'../dist/{PLATFORM_NAME}']
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
os.chdir('..')
if result.returncode != 0:
print(f"❌ Error building universal wheel:\n{result.stderr}")
return False
print(f"✅ Universal wheel for {PLATFORM_NAME} built successfully!")
print(result.stdout)
return True
def main():
"""Main build process."""
import argparse
parser = argparse.ArgumentParser(description='Build orisys package with PyArmor')
parser.add_argument('--mode', choices=['universal', 'multi-version', 'current'], default='current',
help='Build mode: universal (one wheel for all), multi-version (separate wheels), current (use current Python only)')
parser.add_argument('--versions', nargs='+', default=PYTHON_VERSIONS,
help='Python versions to build for (only for multi-version mode)')
args = parser.parse_args()
# Get current Python version
current_py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
print("=" * 60)
print("Building Orisys Package with PyArmor Obfuscation")
print(f"Platform: {PLATFORM_NAME} ({platform.system()})")
print(f"Current Python: {current_py_version} ({sys.executable})")
if args.mode == 'current':
print(f"Mode: Building for current Python version ({current_py_version}) only")
args.versions = [current_py_version]
# Use multi-version logic but with current version only
build_mode = 'multi-version'
else:
print(f"Python Versions: {', '.join(args.versions)}")
print(f"Mode: {args.mode}")
build_mode = args.mode
print("=" * 60)
# Ensure we're in the right directory
script_dir = Path(__file__).parent
os.chdir(script_dir)
# Step 1: Clean previous builds
clean_build_dirs()
# Step 2: Obfuscate the code (once, version-independent)
if not obfuscate_code():
print("\n❌ Build failed during obfuscation!")
sys.exit(1)
# Step 3: Copy additional files
copy_additional_files()
# Step 4 & 5: Build based on mode
if build_mode == 'universal':
# Build one wheel for all Python versions
create_setup_for_obfuscated()
if not build_universal_wheel():
print("\n❌ Build failed!")
sys.exit(1)
else:
# Build separate wheels for each Python version (or just current)
success_count = 0
for version in args.versions:
create_setup_for_obfuscated(version)
if build_wheel_for_version(version):
success_count += 1
if success_count == 0:
print("\n❌ Build failed - no wheels created!")
sys.exit(1)
print(f"\n✅ Successfully built {success_count}/{len(args.versions)} wheels")
print("\n" + "=" * 60)
print("✅ Build completed successfully!")
print("=" * 60)
print(f"\nPlatform: {PLATFORM_NAME}")
print(f"Obfuscated source: dist_obfuscated/")
print(f"Distribution packages: dist/{PLATFORM_NAME}/")
print("\nGenerated wheels:")
for wheel in Path(f'dist/{PLATFORM_NAME}').glob('*.whl'):
print(f" - {wheel.name}")
print("\nInstall with:")
print(f" pip install dist/{PLATFORM_NAME}/orisys-0.1.0-*.whl")
if __name__ == '__main__':
main()
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+63
View File
@@ -0,0 +1,63 @@
# OrisysSDK API 文档
本文件提供了 OrisysSDK 的各类方法
---
## `Sensor` 类别
### 描述
创建一个传感器实例
### 输入参数
* **cam_id** (`int | str`, 可选): 传感器 ID、序列号或视频路径。默认为 `0`。
* **isstitch** (`bool`,可选): 是否启用图像拼接,默认为 `True`。
* **cuda** (`bool`,可选): 是否使用 GPU cuda 加速,默认为 `False`。
* **cfg** (`str`,可选): 指定拼接标定数据,默认为 `/config/cal.npy`。
### 返回
* `Sensor` 对象
### 示例
```python
# Example 1: 用相机编号创建
from xensesdk import Sensor
from src.sensor import Sensor
sensor = Sensor(0)
# Example 2: 用视频文件创建
sensor = Sensor("/data/data.mp4")
# Example 3: 用相机编号开启
sensor = Sensor.create(0)
```
---
## 2. `get_img` 方法
### 描述
从传感器获取图像,进行复眼图像提取和拼接。
### 返回
* 提取(`issitch = False`)或拼接后(`issitch = True`)的图像
---
## 3. `compute_deformation` 方法
### 描述
计算传感器变形和应力
---
## 4. `compute_deformation` 方法
### 描述
计算传感器变形和应力
---
## 常见问题解答 (FAQ)
TBC
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
# Pyarmor 9.2.2 (trial), 000000, non-profits, 2025-12-15T17:04:23.768373
from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\x08\x00U\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00~\x02\x00\x00\x12\t\x04\x00\x12\xaa\x82i\x1d>\x02\x90i|\x16H\xd7\xed\x8e\x88\x00\x00\x00\x00\x00\x00\x00\x00\xe0$\xf1\xe0\x96\x7f\xad\xebdIP\x04\xd2\xf2L\x88lwh\xfb\xe6\xea5\xcd\x0f\x81\xa4\x8c\xd1a\xae\x80\x1c\x063\xbe*<2d\xa2\x9a+R\x1a\xdcL2\xd7\x83\xa5\xfc7R8\x1f\x19f\xa2\x9b\x17\xae\xf9\x881f\xd1\x85\x0b\x1a\x07Mz\xcb\x01o\x91\xa8\xe6<\xbc\xff\xca\xf7W\xd0].}^\xc2\x02\r\x9a\xdd\xe6\xa0\xc1\x8d8\xee\x1f{\xc1AE\xd0\xa5x\xce\xe5\x071YEN\xaf\xeak\xe9~\xd4K)\x07**\xf8#\xccoS\xa9\xeby\x07H\xe67B\xf3!\x88\xe3\x87^R\x1b\x03d\x19\xa4O\xedr\xe0\x05\r\xd8%\x97\xd4XE\x8c,\x90l\xa7 kBj\xf5\xf42\xc0\x1d=\xdb\x13\xbb^0@\xf3)\xb6\xc4\x84+N${\x82\xfc\xac3\x0f\x1b\xcfz\x9f\xaa\x98\xd9*\xcc\xb5\x84^!\x11[U(\xf6\x0c\xc5\xa5W\xe6]\xfa\xd8\x98\xf7\xd1\x1a\x81\'\x1a\x9b\x8c\xe6\x8fH\n\xc567c\xe5\x9d\x1avT\'\xd9"m\xfe\xe3o\xaa|\xdf%a\xcb\xcb\\\xaa\xe9OM\x965\x90\xb4n&\x86\x85\x9f0%,g\t\xb3|\xd5\xd2ip\xdf[}\xac\x93\xe3\xae0\xae\xad\x02\x9f\x8f\x1d\x12*xM?\xa1vU\x80\x91\xb3\x05\x8d#5\xa4\x0b\x00\\iw\xd9\xec\x9a<\xeb\xca\xa2\xb7\xcc\xcc\x03\x1a\xd5\xb6\xed\xc6\x80\x1f\xb0\xd8\xaa%\xb3\xa2\xf97AYB(\xd2^:/>1\xce)\xc1x\x90\x11I\x98\xc6\xfe\x978\xfa\xf0\x05+\x95\xd8\xda|\x93C8\xc8\xbc\xe24\x96\xe7*\x823\xbf\xfe\x94=\x86\xd4\xf5t\x1aR\xfd\x97\x1eM\x9a\xaf/\xbd\xc2\xfd\xbd\x9a\xdd\xefz\xab\x04\xf3h\xc6\xa5\xfe\x11\xe2\x80`<\x10b\xf8\xa2\xd2^=\xde\xaa\xb9\x8c\x164\xbb/U~<\xe3\xbfG\xc8\x1ev(\xd0\xd3F\x9e\x99\x1b|/\x99u\xdf\x12z\xcd\x07R\x92w*z\x11)\x03\xcf\xe1\xfd\xab]\xeb\nc\x84\xbc\xf5h\x1dM\x9b\xb5\x12v\x16\xf5\x86\x0b\xa4$sa\x15n\x90\xc2\x15"\x15\x08z \xfe\x00\xa3\')\xed\xa8f\x0c\x86J{\xa5>\xaaV\xcb}\x9c\x0f\xe7\xbcEX!\t\xd5\x1f\x0f\x1c\xb92\x9c\x07B#yn@\xf3C\xae\xca7\x0fPV\xe6\x9b)Q\x85\xd5\x1e\xea\'.?8\x92$\x9eH5\xad*:\x17QK\xba+d\xbd\x88p\x16j\xea\x90 It?b\xf9\x19q\xf8\x86\xff\x004\xf2\xfd\xe6\xe8\xc1\x1b\xad\x82\xb9CF\xfa\x9f\x01\x82\xf3\x84\xf5.oo\xb5\xda\x1b\xd8\x98*\xb7\xec\xa0&%\xfa\xc4\xab')
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
# Pyarmor 9.2.2 (trial), 000000, 2025-12-15T17:04:23.412278
from .pyarmor_runtime import __pyarmor__
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+5
View File
@@ -0,0 +1,5 @@
cupy_cuda12x>=12.3.0
numpy>=1.24.4
opencv_python>=4.10.0.84
scipy>=1.10.1
setuptools>=45.2.0
+42
View File
@@ -0,0 +1,42 @@
from setuptools import setup, find_packages
import os
from pathlib import Path
# Read requirements
with open('requirements.txt', 'r') as f:
requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')]
# Find all packages including pyarmor_runtime
packages = find_packages(where="src")
# Collect all package data including PyArmor runtime files
package_data = {}
for pkg in packages:
pkg_path = Path('src') / pkg.replace('.', '/')
if pkg_path.exists():
# Include all .pyd, .so, .dll files (PyArmor runtime)
data_files = []
for pattern in ['*.pyd', '*.so', '*.dll', '*.dylib']:
data_files.extend([str(f.relative_to(pkg_path)) for f in pkg_path.glob(pattern)])
if data_files:
package_data[pkg] = data_files
setup(
name="orisys",
version="0.1.0",
description="Orisys - Optical Tactile Sensor SDK (Protected)",
author="Your Name",
package_dir={"": "src"},
packages=packages,
package_data=package_data,
include_package_data=True,
install_requires=requirements,
python_requires="==3.8.*",
# Include PyArmor runtime files
zip_safe=False,
options={
'bdist_wheel': {
'python_tag': 'py38',
}
},
)
@@ -0,0 +1,11 @@
Metadata-Version: 2.1
Name: orisys
Version: 0.1.0
Summary: Orisys - Optical Tactile Sensor SDK (Protected)
Author: Your Name
Requires-Python: ==3.8.*
Requires-Dist: cupy_cuda12x>=12.3.0
Requires-Dist: numpy>=1.24.4
Requires-Dist: opencv_python>=4.10.0.84
Requires-Dist: scipy>=1.10.1
Requires-Dist: setuptools>=45.2.0
@@ -0,0 +1,18 @@
setup.py
src/orisys/Stitcher.py
src/orisys/StructuredGrid.py
src/orisys/UnstructuredGrid.py
src/orisys/__init__.py
src/orisys/nHHD.py
src/orisys/nPoisson.py
src/orisys/sensor.py
src/orisys/timer.py
src/orisys/tracker.py
src/orisys.egg-info/PKG-INFO
src/orisys.egg-info/SOURCES.txt
src/orisys.egg-info/dependency_links.txt
src/orisys.egg-info/not-zip-safe
src/orisys.egg-info/requires.txt
src/orisys.egg-info/top_level.txt
src/pyarmor_runtime_000000/__init__.py
src/pyarmor_runtime_000000/pyarmor_runtime.pyd
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1,5 @@
cupy_cuda12x>=12.3.0
numpy>=1.24.4
opencv_python>=4.10.0.84
scipy>=1.10.1
setuptools>=45.2.0
@@ -0,0 +1,2 @@
orisys
pyarmor_runtime_000000
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
# Pyarmor 9.2.2 (trial), 000000, non-profits, 2025-12-15T17:04:23.768373
from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\x08\x00U\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00~\x02\x00\x00\x12\t\x04\x00\x12\xaa\x82i\x1d>\x02\x90i|\x16H\xd7\xed\x8e\x88\x00\x00\x00\x00\x00\x00\x00\x00\xe0$\xf1\xe0\x96\x7f\xad\xebdIP\x04\xd2\xf2L\x88lwh\xfb\xe6\xea5\xcd\x0f\x81\xa4\x8c\xd1a\xae\x80\x1c\x063\xbe*<2d\xa2\x9a+R\x1a\xdcL2\xd7\x83\xa5\xfc7R8\x1f\x19f\xa2\x9b\x17\xae\xf9\x881f\xd1\x85\x0b\x1a\x07Mz\xcb\x01o\x91\xa8\xe6<\xbc\xff\xca\xf7W\xd0].}^\xc2\x02\r\x9a\xdd\xe6\xa0\xc1\x8d8\xee\x1f{\xc1AE\xd0\xa5x\xce\xe5\x071YEN\xaf\xeak\xe9~\xd4K)\x07**\xf8#\xccoS\xa9\xeby\x07H\xe67B\xf3!\x88\xe3\x87^R\x1b\x03d\x19\xa4O\xedr\xe0\x05\r\xd8%\x97\xd4XE\x8c,\x90l\xa7 kBj\xf5\xf42\xc0\x1d=\xdb\x13\xbb^0@\xf3)\xb6\xc4\x84+N${\x82\xfc\xac3\x0f\x1b\xcfz\x9f\xaa\x98\xd9*\xcc\xb5\x84^!\x11[U(\xf6\x0c\xc5\xa5W\xe6]\xfa\xd8\x98\xf7\xd1\x1a\x81\'\x1a\x9b\x8c\xe6\x8fH\n\xc567c\xe5\x9d\x1avT\'\xd9"m\xfe\xe3o\xaa|\xdf%a\xcb\xcb\\\xaa\xe9OM\x965\x90\xb4n&\x86\x85\x9f0%,g\t\xb3|\xd5\xd2ip\xdf[}\xac\x93\xe3\xae0\xae\xad\x02\x9f\x8f\x1d\x12*xM?\xa1vU\x80\x91\xb3\x05\x8d#5\xa4\x0b\x00\\iw\xd9\xec\x9a<\xeb\xca\xa2\xb7\xcc\xcc\x03\x1a\xd5\xb6\xed\xc6\x80\x1f\xb0\xd8\xaa%\xb3\xa2\xf97AYB(\xd2^:/>1\xce)\xc1x\x90\x11I\x98\xc6\xfe\x978\xfa\xf0\x05+\x95\xd8\xda|\x93C8\xc8\xbc\xe24\x96\xe7*\x823\xbf\xfe\x94=\x86\xd4\xf5t\x1aR\xfd\x97\x1eM\x9a\xaf/\xbd\xc2\xfd\xbd\x9a\xdd\xefz\xab\x04\xf3h\xc6\xa5\xfe\x11\xe2\x80`<\x10b\xf8\xa2\xd2^=\xde\xaa\xb9\x8c\x164\xbb/U~<\xe3\xbfG\xc8\x1ev(\xd0\xd3F\x9e\x99\x1b|/\x99u\xdf\x12z\xcd\x07R\x92w*z\x11)\x03\xcf\xe1\xfd\xab]\xeb\nc\x84\xbc\xf5h\x1dM\x9b\xb5\x12v\x16\xf5\x86\x0b\xa4$sa\x15n\x90\xc2\x15"\x15\x08z \xfe\x00\xa3\')\xed\xa8f\x0c\x86J{\xa5>\xaaV\xcb}\x9c\x0f\xe7\xbcEX!\t\xd5\x1f\x0f\x1c\xb92\x9c\x07B#yn@\xf3C\xae\xca7\x0fPV\xe6\x9b)Q\x85\xd5\x1e\xea\'.?8\x92$\x9eH5\xad*:\x17QK\xba+d\xbd\x88p\x16j\xea\x90 It?b\xf9\x19q\xf8\x86\xff\x004\xf2\xfd\xe6\xe8\xc1\x1b\xad\x82\xb9CF\xfa\x9f\x01\x82\xf3\x84\xf5.oo\xb5\xda\x1b\xd8\x98*\xb7\xec\xa0&%\xfa\xc4\xab')
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
# Pyarmor 9.2.2 (trial), 000000, 2025-12-15T17:04:23.412278
from .pyarmor_runtime import __pyarmor__
+38
View File
@@ -0,0 +1,38 @@
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
from orisys import Sensor
import numpy as np
def main():
global start_time
# 打开视频
# sensor1 = Sensor("./s1.mp4",isstitch=True,cuda = True,verbose = True)
# 打开摄像头
sensor1 = Sensor(0,isstitch=True,cuda = True,verbose = True)
while True:
sensor1.get_img() # 提取并拼接复眼图像
sensor1.compute_deformation() # 计算形变和分解
# 提取信息:
# 向量场: VRAW, VNORMAL, VSHEAR: 原始,法向,切向
# 帧率,时间戳: FPS, TIMESTAMP
# 变形量总和: FNORMAL , FSHEARX, FSHEARY : 法向,切向(X),切向(Y)
fps, fn, fx, fy, flow = sensor1.read_info(sensor1.info.FPS, sensor1.info.FNORMAL, sensor1.info.FSHEARX, sensor1.info.FSHEARY, sensor1.info.VRAW)
print(fps,fn,fx,fy)
# 显示向量场
arrows = sensor1.draw_arrows(sensor1.info.VRAW, threshold=5, grid_spacing=40, arrow_scale=1.0)
cv2.imshow("Deformation", arrows)
key = cv2.waitKey(1)
if key & 0xFF == ord("q"):
print("\n 停止程序")
break
sensor1.disconnect() # 断开传感器连接
if __name__ == '__main__':
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
cupy_cuda12x>=12.3.0
numpy>=1.24.4
opencv_python>=4.10.0.84
opencv-contrib-python
scipy>=1.10.1
setuptools>=45.2.0
+16
View File
@@ -0,0 +1,16 @@
from setuptools import setup, find_packages
setup(
name="orisys",
version="0.1.0",
description="Orisys - Optical Tactile Sensor SDK",
author="Your Name",
package_dir={"": "src"},
packages=find_packages(where="src"),
install_requires=[
"numpy",
"opencv-python",
"scipy",
],
python_requires=">=3.8",
)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
Metadata-Version: 2.4
Name: orisys
Version: 0.1.0
Summary: Orisys - Optical Tactile Sensor SDK
Author: Your Name
Requires-Python: >=3.7
Requires-Dist: numpy
Requires-Dist: opencv-python
Requires-Dist: scipy
Dynamic: author
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary
+15
View File
@@ -0,0 +1,15 @@
setup.py
src/orisys/Stitcher.py
src/orisys/StructuredGrid.py
src/orisys/UnstructuredGrid.py
src/orisys/__init__.py
src/orisys/nHHD.py
src/orisys/nPoisson.py
src/orisys/sensor.py
src/orisys/timer.py
src/orisys/tracker.py
src/orisys.egg-info/PKG-INFO
src/orisys.egg-info/SOURCES.txt
src/orisys.egg-info/dependency_links.txt
src/orisys.egg-info/requires.txt
src/orisys.egg-info/top_level.txt
+1
View File
@@ -0,0 +1 @@
+3
View File
@@ -0,0 +1,3 @@
numpy
opencv-python
scipy
+1
View File
@@ -0,0 +1 @@
orisys
+567
View File
@@ -0,0 +1,567 @@
import os
import numpy as np
import scipy.fft
from scipy import ndimage
import cv2
class Stitcher:
def __init__(self,isstitch=False, n=[3,3], index=np.array([1,2,3,6,7,8,11,12,13])):
# Settings
self.isstitch = isstitch
# stitch related
self.stitch_valid = False
self.boxes = self.units = None
self.index = index
self.stitch_calibration = self.tile_list = []
self.ncol = n[0]
self.nrow = n[1]
self.nominal_ov_pos=100
self.margin=10
self.bound=self.nominal_ov_pos - self.margin
# for sensor 1
self.pos_default = [(260, 56), (577, 62), (897, 69), (1214, 75), (1530, 81), (256, 391), (570, 397), (889, 403), (1208, 409), (1523, 416), (248, 724), (563, 730), (882, 738), (1201, 743), (1516, 751)]
# for sensor 2
# self.pos_default = [(195, 61), (511, 61), (831, 61), (1150, 60), (1464, 58), (195, 396), (512, 395), (830, 394), (1149, 394), (1465, 393), (197, 731), (512, 728), (831, 730), (1151, 728), (1466, 728)]
# for sensor 3
# self.pos_default = [(207, 39), (547, 40), (887, 42), (1227, 42), (1564, 44), (207, 397), (546, 399), (884, 399), (1225, 401), (1563, 403), (200, 755), (543, 757), (883, 758), (1223, 758), (1562, 760)]
def stitch_rt_optimized(self, change_sv=False, s_mean=28., v_mean=180.):
tile_list = self.tile_list
stitch_calibration = self.stitch_calibration
ncol = self.ncol
nrow = self.nrow
processed_tiles = tile_list
img_stitched_height = None
for i in range(nrow):
# Start with first tile of the row
img_stitched_width = tile_list[i * ncol]
# Stitch horizontally within the row
for j in range(1, ncol):
img_1 = img_stitched_width # Already stitched part
img_2 = processed_tiles[i * ncol + j] # if change_sv else tile_list[i * ncol + j]
ov_pos = int(stitch_calibration[i * ncol + j - 1][1])
ov_pos, ov1, ov2 = self.__find_ov(img_1, img_2, ov_pos, dim=1)
# Use optimized stitch function
img_stitched_width = self.__stitch_optimized(img_1, img_2, ov_pos, ov1, ov2, dim=1, blend=True)
# Stitch rows vertically
if i == 0:
img_stitched_height = img_stitched_width
else:
# Handle width mismatch more efficiently
width_h = img_stitched_height.shape[1]
width_w = img_stitched_width.shape[1]
wdiff = width_h - width_w
if wdiff != 0:
# Use numpy pad instead of hstack for better performance
if wdiff > 0:
# Pad right side of the narrower image
padding = ((0, 0), (0, wdiff), (0, 0)) if len(img_stitched_width.shape) == 3 else ((0, 0), (0, wdiff))
img_stitched_width = np.pad(img_stitched_width, padding, mode='edge')
else:
# Crop the wider image
img_stitched_width = img_stitched_width[:, :width_h]
ov_pos_h = int(stitch_calibration[(i + 1) * nrow - 1][1])
ov_pos_h, ov_h1, ov_h2 = self.__find_ov(img_stitched_height, img_stitched_width, ov_pos_h, dim=0)
img_stitched_height = self.__stitch_optimized(
img_stitched_height, img_stitched_width, ov_pos_h, ov_h1, ov_h2, dim=0, blend=True
)
return img_stitched_height
def __find_ov(self, img1, img2, ov_pos, dim=1):
_dim = len(img1.shape)
if dim == 1:
if _dim == 3:
# RGB case
ov_1 = img1[:, -ov_pos:, :]
ov_2 = img2[:, :ov_pos, :]
else:
# gray scale
ov_1 = img1[:, -ov_pos:]
ov_2 = img2[:, :ov_pos]
elif dim == 0:
if _dim == 3:
ov_1 = img1[-ov_pos:, :, :]
ov_2 = img2[:ov_pos, :, :]
else:
# gray scale
ov_1 = img1[-ov_pos:, :]
ov_2 = img2[:ov_pos, :]
return ov_pos, ov_1, ov_2
def find_stitch_boxes(self,img_src,show = False):
img_corr = self.__correct_edge_bloom(img_src)
self.__detect_nonblack_regions(img_corr,show = show)
return len(self.boxes)
# return self.boxes
def get_stitch_unit(self, img, flip_x=True, flip_y=True,index = np.arange(15)):
"""
Extract each tile given by boxes (x0,y0,x1,y1), default flip both X and Y due to inverting lens.
- img: source image (use corrected image if you prefer)
- boxes: list of (x0,y0,x1,y1)
- flip_x, flip_y: booleans (performed sequentially: X then Y)
- overlay_original: if True paste onto original image copy, otherwise onto blank background
- bg_color: background color for blank canvas (B,G,R)
Returns: canvas image and list of flipped tiles as (x0,y0,flipped_tile)
"""
h, w = img.shape[:2]
if self.units is None:
flipped_units = []
for i, (x0, y0, x1, y1) in enumerate(self.boxes):
# clamp coordinates
x0 = max(0, int(x0)); y0 = max(0, int(y0))
x1 = min(w, int(x1)); y1 = min(h, int(y1))
if x1 <= x0 or y1 <= y0:
continue
tile = img[y0:y1, x0:x1].copy()
# flip X then Y in default
if flip_x:
tile = cv2.flip(tile, 1) # horizontal
if flip_y:
tile = cv2.flip(tile, 0) # vertical
flipped_units.append((x0, y0, tile))
self.units = flipped_units
else:
flipped_units = self.units
index = self.index
for i, (x0, y0, x1, y1) in enumerate(self.boxes):
if i not in index:
continue
# clamp coordinates
x0 = max(0, int(x0)); y0 = max(0, int(y0))
x1 = min(w, int(x1)); y1 = min(h, int(y1))
if x1 <= x0 or y1 <= y0:
continue
tile = img[y0:y1, x0:x1].copy()
# flip X then Y in default
if flip_x:
tile = cv2.flip(tile, 1) # horizontal
if flip_y:
tile = cv2.flip(tile, 0) # vertical
# Assign instead of append
flipped_units[i] = (x0, y0, tile)
self.units = flipped_units
return True
def extract_tiles(self):
if not self.isstitch:
tile_idx = self.__get_tile_index_by_position(self.pos_default[7][0], self.pos_default[7][1])
if tile_idx is None:
tile_idx = 7
# Ensure index is always iterable (list or array)
self.index = [tile_idx] if isinstance(tile_idx, int) else tile_idx
self.ncol = 1
self.nrow = 1
tile_list = self.__get_tile_by_index(self.index)
self.tile_list = tile_list
return tile_list
def stitch_cal(self):
# init
tile_list = self.tile_list
img_stitched_height = None
stitch_calibration = []
# Flip col and row for porting
nrow, ncol = self.ncol, self.nrow
if not self.isstitch:
return self.tile_list[0], None
for i in range(ncol):
img_stitched_width = None
for j in range(nrow):
if j == 0:
img_j0 = tile_list[i*ncol]
img_stitched_width = img_j0
else:
img_1 = tile_list[i*ncol + j-1]
img_2 = tile_list[i*ncol + j]
ov_pos, ov1, ov2, c_corr = self.__get_ov(img_1.astype(np.float32), img_2.astype(np.float32), dim=1)
stitch_calibration.append(["hor {}".format(j-1),ov_pos])
# stitch images with stacking
img_stitched_width = self.__stitch_optimized(img_stitched_width, img_2, ov_pos, ov1, ov2, blend=True, dim=1)
if i == 0:
img_stitched_height = img_stitched_width
stitch_calibration.append(["ver {}".format(i),0])
pass
else:
width = img_stitched_height.shape[1]
width_new = img_stitched_width.shape[1]
wdiff = width - width_new
if wdiff > 0:
img_stitched_width = np.hstack((img_stitched_width, img_stitched_width[:, -wdiff:]))
elif wdiff < 0:
img_stitched_width = img_stitched_width[:, :wdiff]
ov_pos_h, ov_h1, ov_h2, c_corr = self.__get_ov(img_stitched_height, img_stitched_width, dim=0)
stitch_calibration.append(["ver {}".format(i),ov_pos_h])
img_stitched_height = self.__stitch_optimized(img_stitched_height, img_stitched_width, ov_pos_h, ov_h1, ov_h2, blend=True, dim=0)
self.stitch_calibration = stitch_calibration
return img_stitched_height, stitch_calibration
def __stitch_optimized(self, img1, img2, ov_pos, ov1, ov2, blend=True, dim=1):
_dim = len(img1.shape)
# Early return if no blending needed
if not blend:
ov = ov1
elif ov_pos <= 1:
ov = ov1
else:
# Vectorized blending
blend_coef = np.linspace(1.0, 0.0, ov_pos, dtype=np.float32)
# Prepare blend_coef for broadcasting
if dim == 1: # Horizontal blending
if _dim == 3:
# Shape: (1, ov_pos, 1)
blend_coef = blend_coef.reshape(1, ov_pos, 1)
else:
# Shape: (1, ov_pos)
blend_coef = blend_coef.reshape(1, ov_pos)
else: # Vertical blending
if _dim == 3:
# Shape: (ov_pos, 1, 1)
blend_coef = blend_coef.reshape(ov_pos, 1, 1)
else:
# Shape: (ov_pos, 1)
blend_coef = blend_coef.reshape(ov_pos, 1)
# Compute blended overlap in one operation
ov = (ov1.astype(np.float32) * blend_coef +
ov2.astype(np.float32) * (1 - blend_coef)).astype(np.uint8)
# Avoid unnecessary .copy() calls when possible
if dim == 1:
if _dim == 3:
# Use np.take or slicing without copy when safe
left = img1[:, :img1.shape[1] - ov_pos, :] if img1.shape[1] > ov_pos else img1[:, :0, :]
right = img2[:, ov_pos:, :]
stitched_img = np.concatenate((left, ov, right), axis=1)
else:
left = img1[:, :img1.shape[1] - ov_pos] if img1.shape[1] > ov_pos else img1[:, :0]
right = img2[:, ov_pos:]
stitched_img = np.concatenate((left, ov, right), axis=1)
else:
if _dim == 3:
top = img1[:img1.shape[0] - ov_pos, :, :] if img1.shape[0] > ov_pos else img1[:0, :, :]
bottom = img2[ov_pos:, :, :]
stitched_img = np.concatenate((top, ov, bottom), axis=0)
else:
top = img1[:img1.shape[0] - ov_pos, :] if img1.shape[0] > ov_pos else img1[:0, :]
bottom = img2[ov_pos:, :]
stitched_img = np.concatenate((top, ov, bottom), axis=0)
return stitched_img
def __get_ov(self, img1, img2, dim=1):
'''
default dimension is along axis 1 (width of the images)
'''
# pass parameters
nominal_ov = self.nominal_ov_pos
bound_ov = self.bound
cross_corr = []
h1, w1 = img1.shape[:2]
h2, w2 = img2.shape[:2]
# clamp tested overlap to valid range (1..min dimension)
max_possible = w1 if dim == 1 else h1
max_possible = min(max_possible, w2 if dim == 1 else h2)
lo = max(1, nominal_ov - bound_ov)
hi = min(max_possible, nominal_ov + bound_ov)
ov_space = range(lo, hi + 1)
for ov in ov_space:
if dim == 1:
ov_area1 = img1[:, max(0, w1 - ov):, :]
ov_area2 = img2[:, :min(ov, w2), :]
elif dim == 0:
ov_area1 = img1[max(0, h1 - ov):, :, :]
ov_area2 = img2[:min(ov, h2), :, :]
# require exact shape match (detect_nonblack_regions guarantees uniform tile size)
if ov_area1.size == 0 or ov_area2.size == 0 or ov_area1.shape != ov_area2.shape:
cross_corr.append(-1.0)
continue
# multi-channel cross correlation (average over channels)
corr = 0.0
for i in range(min(3, ov_area1.shape[2])):
a = ov_area1[:, :, i].astype(np.float32)
b = ov_area2[:, :, i].astype(np.float32)
denom = np.sqrt(np.sum(a * a) * np.sum(b * b)) + 1e-12
corr += float(np.sum(a * b) / denom)
corr = corr / float(min(3, ov_area1.shape[2]))
cross_corr.append(corr)
# pick best overlap (ignore invalid entries)
best_idx = int(np.argmax(cross_corr))
ov_pos = list(ov_space)[best_idx]
# extract the overlap regions (assume shapes match)
if dim == 1:
ov_1 = img1[:, max(0, w1 - ov_pos):, :].copy()
ov_2 = img2[:, :min(ov_pos, w2), :].copy()
else:
ov_1 = img1[max(0, h1 - ov_pos):, :, :].copy()
ov_2 = img2[:min(ov_pos, h2), :, :].copy()
return ov_pos, ov_1, ov_2, cross_corr
# Helper functions for stitching
def __get_tile_index_by_position(self, x, y, tol=20):
"""
Find flipped tile whose top-left matches (x,y) within tol pixels.
flipped_units: list of (x,y,tile)
"""
i= 0
for xi, yi, tile in self.units:
if abs(int(x) - int(xi)) <= tol and abs(int(y) - int(yi)) <= tol:
# return tile
return i
i += 1
return None
def __get_tile_by_index(self, idx):
"""
Retrieve tile by index
"""
if isinstance(idx, int):
idx = [idx]
if len(idx) > len(self.units):
raise ValueError("Index exceeds number of tiles.")
tile_list = []
for i in range(len(idx)):
[xi, yi, tile] = self.units[idx[i]]
tile_list.append(tile)
return tile_list
def __correct_edge_bloom(self, img, method="divide", kernel_size=None, clip=True):
"""
Simple illumination correction to reduce edge bloom / vignetting. Only for getting tile boxes.
- method: "divide" (default) or "subtract"
* divide: img_corrected = (img / (bg + eps)) * gain -- preserves colors
* subtract: img_corrected = img - (bg - mean_bg) -- removes low-freq background
- kernel_size: size for large-kernel blur used as background estimate (odd int).
If None, chosen from image size.
Returns corrected uint8 image.
"""
if img is None:
raise ValueError("Input image is None.")
h, w = img.shape[:2]
if kernel_size is None:
# choose large kernel relative to image size, ensure odd
k = max(31, min(h, w) // 2)
if k % 2 == 0:
k += 1
kernel_size = k
# convert to float32 for stable ops
img_f = img.astype(np.float32)
# estimate background by large Gaussian blur (works well for smooth bloom)
bg = cv2.GaussianBlur(img_f, (kernel_size, kernel_size), 0)
if method == "subtract":
# remove low-frequency background but keep overall brightness
mean_bg = bg.mean(axis=(0, 1), keepdims=True)
corr = img_f - (bg - mean_bg)
else:
# divide: normalized gain correction
eps = 1e-6
# compute gain so that mid-level stays near 128
mean_bg = bg.mean(axis=(0, 1), keepdims=True) + eps
gain = (mean_bg * 0.5) # target scale; 0.5 keeps values reasonable
corr = (img_f / (bg + eps)) * gain
if clip:
corr = np.clip(corr, 0, 255).astype(np.uint8)
else:
corr = corr.astype(np.uint8)
return corr
def __get_unit_pos(self,idx):
if isinstance(idx, int):
idx = [idx]
if len(idx) > len(self.units):
raise ValueError("Index exceeds number of vision units.")
pos_list = []
for i in range(len(idx)):
[xi, yi, tile] = self.units[idx[i]]
pos_list.append((xi, yi))
return pos_list
# Computation Functions
def __detect_nonblack_regions(self, img, morph_k=3, n_boxes_target=15, bk_thresh_step=10, bk_thresh_max=100,show = False):
"""
Return binary mask of non-black regions and list of bounding boxes (x,y,w,h).
Uses adaptive thresholding on grayscale + small morphology to be robust to illumination.
New behavior: after finding candidate boxes, center-crop all boxes to the same
width/height (smallest found) so returned boxes share a common size.
"""
if img is None:
raise ValueError("Input image is None.")
h, w = img.shape[:2]
n_boxes = 0
bk_thresh = 35 # initial black threshold
bk_thresh_step = 5
bk_thresh_max = 100
# Also include a low global V threshold to capture colored but dark tiles
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
v = hsv[:,:,2]
ww = hh = 0
while n_boxes != n_boxes_target and bk_thresh <= bk_thresh_max:
_, v_mask = cv2.threshold(v, bk_thresh, 255, cv2.THRESH_BINARY) # tune 50 if needed
mask = v_mask
# Morphology: remove small specks, fill small holes
k = max(1, morph_k)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (k, k))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
# find contours of candidate regions
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for c in contours:
x,y,ww,hh = cv2.boundingRect(c)
boxes.append((x,y,ww,hh))
# filter boxes by size (ignore too small or too large tiles)
boxes = self.__filter_boxes_by_size(boxes, (h, w))
n_boxes = len(boxes)
bk_thresh += bk_thresh_step
# print("Detected {} candidate boxes with bk_thresh={}".format(n_boxes, bk_thresh - bk_thresh_step))
if n_boxes == 0:
self.boxes = []
return n_boxes
# compute common center-crop size: use minimum width/height among boxes
widths = [b[2] for b in boxes]
heights = [b[3] for b in boxes]
target_w = max(1,int(min(widths)))
target_h = max(1,int(min(heights)))
# center-crop each box to (target_w, target_h) inside original box bounds
cropped_boxes = []
for (x, y, ww, hh) in boxes:
cx = x + ww // 2
cy = y + hh // 2
nx0 = int(cx - target_w // 2)
ny0 = int(cy - target_h // 2)
# clamp to image bounds
nx0 = max(0, min(nx0, w - target_w))
ny0 = max(0, min(ny0, h - target_h))
# Convert to (x0, y0, x1, y1) format for direct image region retrieval
nx1 = nx0 + target_w
ny1 = ny0 + target_h
cropped_boxes.append((nx0, ny0, nx1, ny1))
# sort boxes left-to-right, top-to-bottom (useful for grid)
cropped_boxes = sorted(cropped_boxes, key=lambda b: (b[1], b[0]))
self.boxes = cropped_boxes
img_with_boxes = self.draw_boxes(img= img)
if show:
cv2.imshow("Detected Boxes", img_with_boxes)
cv2.waitKey(0)
img_path = "./config/compound_eye.png"
if os.path.exists(img_path):
cv2.imwrite(img_path, img_with_boxes)
else:
os.makedirs(os.path.dirname(img_path), exist_ok=True)
cv2.imwrite(img_path, img_with_boxes)
# print("Check if compound eye detection image valid: {}".format(img_path))
return cropped_boxes
def draw_boxes(self, img, thickness=2):
"""
Draw bounding boxes on the image.
- img: input image (will be copied, not modified in-place)
- thickness: line thickness
Returns: image with drawn boxes
"""
if self.boxes is None or len(self.boxes) == 0:
print("No boxes to draw")
return img
img_draw = img.copy()
color=(0, 255, 0)
for i, (x0, y0, x1, y1) in enumerate(self.boxes):
# Draw rectangle
cv2.rectangle(img_draw, (x0, y0), (x1, y1), color, thickness)
# Draw index
text_size = cv2.getTextSize(str(i), cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0]
text_pos = (x0 + 5, y0 + text_size[1] + 5)
# Draw text background and text
cv2.rectangle(img_draw, (text_pos[0] - 2, text_pos[1] - text_size[1] - 2),
(text_pos[0] + text_size[0] + 2, text_pos[1] + 2), (0, 0, 0), -1)
cv2.putText(img_draw, str(i), text_pos, cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
return img_draw
def __filter_boxes_by_size(self, boxes, img_shape, min_area_frac=0.018, max_area_frac=0.035):
"""
Remove boxes that are too small or too large relative to the image.
- boxes: list of (x,y,w,h)
- img_shape: (height, width)
- min_area_frac / max_area_frac: allowed area fraction of image
Returns filtered list of boxes.
"""
h, w = img_shape
img_area = h * w
min_area = img_area * float(min_area_frac)
max_area = img_area * float(max_area_frac)
filtered = []
for (x,y,ww,hh) in boxes:
area = ww * hh
if area < min_area or area > max_area or (abs(ww - hh) > 20):
continue
filtered.append((x,y,ww,hh))
return filtered
+408
View File
@@ -0,0 +1,408 @@
'''
Copyright (c) 2015, Harsh Bhatia (bhatia4@llnl.gov)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
import numpy
import sys
from .timer import Timer
# Try to import CuPy for GPU acceleration
try:
import cupy as cp
CUPY_AVAILABLE = True
except ImportError:
cp = None
CUPY_AVAILABLE = False
# ------------------------------------------------------------------------------
class StructuredGrid(object):
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
def __init__(self, **kwargs):
'''
kwargs:
grid: ndarray of grid dimensions (Y,X) or (Z,Y,X)
spacings: ndarray of grid spacings (dy, dx) or (dz, dy, dx)
verbose: verbosity level
'''
args = kwargs.keys()
if ('grid' not in args) or ('spacings' not in args):
raise SyntaxError("Dimensions and spacings of the grid are required")
verbose = 0
if 'verbose' in args:
verbose = kwargs['verbose']
self.dims = kwargs['grid']
self.dx = kwargs['spacings']
self.dim = len(self.dims)
if self.dim != 2 and self.dim != 3:
raise ValueError("StructuredGrid works for 2D and 3D only")
if self.dim != len(self.dx):
raise ValueError("Dimensions of spacings should match that of the grid")
# Initialize GPU cache
self._gpu_divcurl_cache = None
if verbose > 0:
print(' Initializing', self.dim, 'D structured grid...', end ='')
sys.stdout.flush()
mtimer = Timer()
print(' Done!', end ='')
mtimer.end()
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
def divcurl(self, vfield, verbose=False):
# compute div and curl of vfield (vector field)
#if (vfield.shape[-1] != self.dim) or (vfield.shape[0:self.dim] - self.dims).any():
if (vfield.shape[-1] != self.dim) or (vfield.shape[0:self.dim] != self.dims):
raise ValueError("Dimensions of vector field should match that of the grid")
if verbose:
print(' Computing div and curl...', end ='')
sys.stdout.flush()
mtimer = Timer()
if self.dim == 2:
# self.dx = (dy,dx)
dudy, dudx = numpy.gradient(vfield[:,:,0], self.dx[0], self.dx[1])
dvdy, dvdx = numpy.gradient(vfield[:,:,1], self.dx[0], self.dx[1])
numpy.add(dudx, dvdy, dudx)
numpy.subtract(dvdx, dudy, dvdx)
if verbose:
print(' Done!', end ='')
mtimer.end()
return (dudx, dvdx)
elif self.dim == 3:
# self.dx = (dz,dy,dx)
dudz, dudy, dudx = numpy.gradient(vfield[:,:,:,0], self.dx[0], self.dx[1], self.dx[2])
dvdz, dvdy, dvdx = numpy.gradient(vfield[:,:,:,1], self.dx[0], self.dx[1], self.dx[2])
dwdz, dwdy, dwdx = numpy.gradient(vfield[:,:,:,2], self.dx[0], self.dx[1], self.dx[2])
numpy.add(dudx, dvdy, dudx)
numpy.add(dudx, dvdz, dudx)
numpy.subtract(dwdy, dvdz, dwdy)
numpy.subtract(dudz, dwdx, dudz)
numpy.subtract(dvdx, dudy, dvdx)
if verbose:
print(' Done!', end ='')
mtimer.end()
return (dudx, dwdy, dudz, dvdx)
def divcurl_gpu(self, vfield, verbose=False):
"""
GPU-accelerated computation of divergence and curl of vector field.
Uses CuPy for faster gradient computation on GPU.
"""
if not CUPY_AVAILABLE:
if verbose:
print('CuPy not available, falling back to CPU')
return self.divcurl(vfield, verbose)
if (vfield.shape[-1] != self.dim) or (vfield.shape[0:self.dim] != self.dims):
raise ValueError("Dimensions of vector field should match that of the grid")
if verbose:
print(' Computing div and curl on GPU...', end ='')
sys.stdout.flush()
mtimer = Timer()
if self.dim == 2:
# Transfer to GPU
vfield_gpu = cp.asarray(vfield, dtype=cp.float32)
# Compute gradients on GPU
dudy, dudx = cp.gradient(vfield_gpu[:,:,0], self.dx[0], self.dx[1])
dvdy, dvdx = cp.gradient(vfield_gpu[:,:,1], self.dx[0], self.dx[1])
# Compute div and curl on GPU
div_gpu = dudx + dvdy
curl_gpu = dvdx - dudy
# Transfer back to CPU
div = cp.asnumpy(div_gpu)
curl = cp.asnumpy(curl_gpu)
if verbose:
print(' Done!', end ='')
mtimer.end()
return (div, curl)
elif self.dim == 3:
# Transfer to GPU
vfield_gpu = cp.asarray(vfield, dtype=cp.float32)
# Compute gradients on GPU
dudz, dudy, dudx = cp.gradient(vfield_gpu[:,:,:,0], self.dx[0], self.dx[1], self.dx[2])
dvdz, dvdy, dvdx = cp.gradient(vfield_gpu[:,:,:,1], self.dx[0], self.dx[1], self.dx[2])
dwdz, dwdy, dwdx = cp.gradient(vfield_gpu[:,:,:,2], self.dx[0], self.dx[1], self.dx[2])
# Compute div and curl components on GPU
div_gpu = dudx + dvdy + dwdz
curlu_gpu = dwdy - dvdz
curlv_gpu = dudz - dwdx
curlw_gpu = dvdx - dudy
# Transfer back to CPU
div = cp.asnumpy(div_gpu)
curlu = cp.asnumpy(curlu_gpu)
curlv = cp.asnumpy(curlv_gpu)
curlw = cp.asnumpy(curlw_gpu)
if verbose:
print(' Done!', end ='')
mtimer.end()
return (div, curlu, curlv, curlw)
def curl3D(self, vfield, verbose=False):
#if (vfield.shape[-1] != self.dim) or (vfield.shape[0:self.dim] - self.dims).any():
if (vfield.shape[-1] != self.dim) or (vfield.shape[0:self.dim] != self.dims):
raise ValueError("Dimensions of vector field should match that of the grid")
if self.dim != 3:
raise ValueError("curl3D works only for 2D")
if verbose:
print(' Computing curl...', end ='')
sys.stdout.flush()
mtimer = Timer()
# self.dx = (dz,dy,dx)
dudz, dudy, dudx = numpy.gradient(vfield[:,:,:,0], self.dx[0], self.dx[1], self.dx[2])
dvdz, dvdy, dvdx = numpy.gradient(vfield[:,:,:,1], self.dx[0], self.dx[1], self.dx[2])
dwdz, dwdy, dwdx = numpy.gradient(vfield[:,:,:,2], self.dx[0], self.dx[1], self.dx[2])
numpy.subtract(dwdy, dvdz, dwdy)
numpy.subtract(dudz, dwdx, dudz)
numpy.subtract(dvdx, dudy, dvdx)
if verbose:
print(' Done!', end ='')
mtimer.end()
return (dwdy, dudz, dvdx)
def curl3D_gpu(self, vfield, verbose=False):
"""
GPU-accelerated computation of curl for 3D vector field.
Uses CuPy for faster gradient computation on GPU.
"""
if not CUPY_AVAILABLE:
if verbose:
print('CuPy not available, falling back to CPU')
return self.curl3D(vfield, verbose)
if (vfield.shape[-1] != self.dim) or (vfield.shape[0:self.dim] != self.dims):
raise ValueError("Dimensions of vector field should match that of the grid")
if self.dim != 3:
raise ValueError("curl3D works only for 3D")
if verbose:
print(' Computing curl on GPU...', end ='')
sys.stdout.flush()
mtimer = Timer()
# Transfer to GPU
vfield_gpu = cp.asarray(vfield, dtype=cp.float32)
# Compute gradients on GPU
dudz, dudy, dudx = cp.gradient(vfield_gpu[:,:,:,0], self.dx[0], self.dx[1], self.dx[2])
dvdz, dvdy, dvdx = cp.gradient(vfield_gpu[:,:,:,1], self.dx[0], self.dx[1], self.dx[2])
dwdz, dwdy, dwdx = cp.gradient(vfield_gpu[:,:,:,2], self.dx[0], self.dx[1], self.dx[2])
# Compute curl components on GPU
curl_u = dwdy - dvdz
curl_v = dudz - dwdx
curl_w = dvdx - dudy
# Transfer back to CPU
curlu = cp.asnumpy(curl_u)
curlv = cp.asnumpy(curl_v)
curlw = cp.asnumpy(curl_w)
if verbose:
print(' Done!', end ='')
mtimer.end()
return (curlu, curlv, curlw)
def rotated_gradient(self, sfield, verbose=False):
if (sfield.shape != self.dims):
#if (sfield.shape - self.dims).any():
raise ValueError("Dimensions of scalar field should match that of the grid")
if self.dim != 2:
raise ValueError("rotated_gradient works only for 2D")
if verbose:
print(' Computing rotated gradient...', end ='')
sys.stdout.flush()
mtimer = Timer()
ddy, ddx = numpy.gradient(sfield, self.dx[0], self.dx[1])
ddy *= -1.0
grad = numpy.stack((ddy, ddx), axis=-1)
if verbose:
print(' Done!', end ='')
mtimer.end()
return grad
def rotated_gradient_gpu(self, sfield, verbose=False):
"""
GPU-accelerated computation of rotated gradient for 2D scalar field.
Uses CuPy for faster gradient computation on GPU.
"""
if not CUPY_AVAILABLE:
if verbose:
print('CuPy not available, falling back to CPU')
return self.rotated_gradient(sfield, verbose)
if (sfield.shape != self.dims):
raise ValueError("Dimensions of scalar field should match that of the grid")
if self.dim != 2:
raise ValueError("rotated_gradient works only for 2D")
if verbose:
print(' Computing rotated gradient on GPU...', end ='')
sys.stdout.flush()
mtimer = Timer()
# Transfer to GPU
sfield_gpu = cp.asarray(sfield, dtype=cp.float32)
# Compute gradients on GPU
ddy, ddx = cp.gradient(sfield_gpu, self.dx[0], self.dx[1])
ddy *= -1.0
# Stack and transfer back to CPU
grad_gpu = cp.stack((ddy, ddx), axis=-1)
grad = cp.asnumpy(grad_gpu)
if verbose:
print(' Done!', end ='')
mtimer.end()
return grad
def gradient(self, sfield, verbose=False):
if (sfield.shape != self.dims):
#if (sfield.shape - self.dims).any():
raise ValueError("Dimensions of scalar field should match that of the grid")
if verbose:
print(' Computing gradient...', end ='')
sys.stdout.flush()
mtimer = Timer()
if self.dim == 2:
# self.dx = (dy,dx)
ddy, ddx = numpy.gradient(sfield, self.dx[0], self.dx[1])
grad = numpy.stack((ddx, ddy), axis = -1)
elif self.dim == 3:
# self.dx = (dz,dy,dx)
ddz, ddy, ddx = numpy.gradient(sfield, self.dx[0], self.dx[1], self.dx[2])
grad = numpy.stack((ddx, ddy, ddz), axis = -1)
if verbose:
print(' Done!', end ='')
mtimer.end()
return grad
def gradient_gpu(self, sfield, verbose=False):
"""
GPU-accelerated computation of gradient for scalar field.
Uses CuPy for faster gradient computation on GPU.
"""
if not CUPY_AVAILABLE:
if verbose:
print('CuPy not available, falling back to CPU')
return self.gradient(sfield, verbose)
if (sfield.shape != self.dims):
raise ValueError("Dimensions of scalar field should match that of the grid")
if verbose:
print(' Computing gradient on GPU...', end ='')
sys.stdout.flush()
mtimer = Timer()
# Transfer to GPU
sfield_gpu = cp.asarray(sfield, dtype=cp.float32)
if self.dim == 2:
# Compute gradients on GPU
ddy, ddx = cp.gradient(sfield_gpu, self.dx[0], self.dx[1])
grad_gpu = cp.stack((ddx, ddy), axis=-1)
elif self.dim == 3:
# Compute gradients on GPU
ddz, ddy, ddx = cp.gradient(sfield_gpu, self.dx[0], self.dx[1], self.dx[2])
grad_gpu = cp.stack((ddx, ddy, ddz), axis=-1)
# Transfer back to CPU
grad = cp.asnumpy(grad_gpu)
if verbose:
print(' Done!', end ='')
mtimer.end()
return grad
def clear_gpu_cache(self):
"""Clear GPU cache to free GPU memory"""
if self._gpu_divcurl_cache is not None:
self._gpu_divcurl_cache = None
if CUPY_AVAILABLE:
cp.get_default_memory_pool().free_all_blocks()
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
+424
View File
@@ -0,0 +1,424 @@
'''
Copyright (c) 2015, Harsh Bhatia (bhatia4@llnl.gov)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
import sys
import numpy
from scipy import spatial
from .timer import Timer
class UnstructuredGrid(object):
'''Class to support nHHD on unstructured grids (triangular and tetrahedral)'''
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
def need_volumes(self, verbose=False):
''' Compute volumes/areas for vertices, simplices, and corners'''
if self.pvolumes.shape != (0,):
return self.pvolumes
def tri_area(o,a,b):
return numpy.linalg.norm(numpy.cross(b-o,a-o)) / 2.0
def tet_volume(o,a,b,c):
return numpy.abs(numpy.dot(a-o,numpy.cross(b-o,c-o))) / 6.0
if verbose:
print(' Computing point areas/volumes...', end ='')
sys.stdout.flush()
mtimer = Timer()
self.svolumes = numpy.zeros(self.nsimplices,)
self.pvolumes = numpy.zeros(self.nvertices,)
self.cvolumes = numpy.zeros((self.nsimplices,self.dim+1))
# triangulation
if self.dim == 2:
for sidx in xrange(self.nsimplices):
simp = self.simplices[sidx]
verts = self.vertices[simp]
self.svolumes[sidx] = tri_area(verts[0], verts[1], verts[2])
# edge weights
e = [ verts[2]-verts[1], verts[0]-verts[2], verts[1]-verts[0] ]
l2 = [ numpy.dot(e[0],e[0]), numpy.dot(e[1],e[1]), numpy.dot(e[2],e[2]) ]
ew = [ l2[0]*(l2[1]+l2[2]-l2[0]), l2[1]*(l2[2]+l2[0]-l2[1]),l2[2]*(l2[0]+l2[1]-l2[2]) ]
# corner areas
if (ew[0] <= 0):
self.cvolumes[sidx,1] = -0.25 * l2[2] * self.svolumes[sidx] / numpy.dot(e[0], e[2])
self.cvolumes[sidx,2] = -0.25 * l2[1] * self.svolumes[sidx] / numpy.dot(e[0], e[1])
self.cvolumes[sidx,0] = self.svolumes[sidx] - self.cvolumes[sidx,1] - self.cvolumes[sidx,2]
elif (ew[1] <= 0):
self.cvolumes[sidx,2] = -0.25 * l2[0] * self.svolumes[sidx] / numpy.dot(e[1], e[0]);
self.cvolumes[sidx,0] = -0.25 * l2[2] * self.svolumes[sidx] / numpy.dot(e[1], e[2]);
self.cvolumes[sidx,1] = self.svolumes[sidx] - self.cvolumes[sidx,2] - self.cvolumes[sidx,0];
elif (ew[2] <= 0):
self.cvolumes[sidx,0] = -0.25 * l2[1] * self.svolumes[sidx] / numpy.dot(e[2], e[1]);
self.cvolumes[sidx,1] = -0.25 * l2[0] * self.svolumes[sidx] / numpy.dot(e[2], e[0]);
self.cvolumes[sidx,2] = self.svolumes[sidx] - self.cvolumes[sidx,0] - self.cvolumes[sidx,1];
else:
ewscale = 0.5 * self.svolumes[sidx] / (ew[0] + ew[1] + ew[2])
for d in xrange(3):
self.cvolumes[sidx,d] = ewscale * (ew[(d+1)%3] + ew[(d+2)%3])
self.pvolumes[simp[0]] += self.cvolumes[sidx,0]
self.pvolumes[simp[1]] += self.cvolumes[sidx,1]
self.pvolumes[simp[2]] += self.cvolumes[sidx,2]
# tetrahedralization
elif self.sdim == 3:
raise ValueError('TODO: pvolumes for 3D')
for sidx in xrange(self.nsimplices):
simp = self.simplices[sidx]
verts = self.vertices[simp]
self.svolumes[sidx] = tet_volume(verts[0], verts[1], verts[2], verts[3])
for v in simp:
self.pvolumes[v] += self.svolumes[sidx] / 4.0
if verbose:
print(' Done!', end ='')
mtimer.end()
return self.pvolumes
def need_adjacentfaces(self, verbose=False):
'''
Find adjacent faces for each vertex
as list of lists
'''
if len(self.adjacent_faces) != 0:
return self.adjacent_faces
if verbose:
print(' Computing adjacent_faces...', end ='')
sys.stdout.flush()
mtimer = Timer()
numadjacentfaces = numpy.zeros(self.nvertices, dtype=int)
for f in self.simplices:
for i in xrange(3):
numadjacentfaces[f[i]] += 1
# can be optimized further by avoiding "append"?
self.adjacent_faces = [[] for _ in xrange(self.nvertices)]
for fidx in xrange(self.nsimplices):
for i in xrange(3):
self.adjacent_faces[self.simplices[fidx, i]].append(fidx)
if verbose:
print('Done!', end ='')
mtimer.end()
return self.adjacent_faces
def need_acrossedge(self, verbose=False):
'''
Find adjacent faces for each face (across each edge)
as ndarray of ints: shape (nsimplex, 3)
-1 denotes a face on the boundary (no face across edge)
'''
if self.across_edge.shape != (0,0):
return self.across_edge
self.need_adjacentfaces(verbose)
if verbose:
print(' Computing across_edge...', end ='')
sys.stdout.flush()
mtimer = Timer()
self.across_edge = -1 * numpy.ones((self.nsimplices, 3), dtype=numpy.int)
for fidx in xrange(self.nsimplices):
for i in xrange(3):
if self.across_edge[fidx, i] != -1:
continue
v1 = self.simplices[fidx, (i+1)%3]
v2 = self.simplices[fidx, (i+2)%3]
for other in self.adjacent_faces[v1]:
if other == fidx:
continue
if other not in self.adjacent_faces[v2]:
continue
oface = self.simplices[other]
j = numpy.where(oface == v1)[0]
j = (j+1)%3
if oface[(j+1)%3] != v2:
continue
self.across_edge[fidx, i] = other
self.across_edge[other, j] = fidx
if verbose:
print(' Done!', end ='')
mtimer.end()
return self.across_edge
def need_boundary(self, verbose=False):
'''
Find boundary of the triangulation
as collection of boundary edeges as an array of [face, k]
'''
if self.bedges.shape != (0,0):
return self.bedges
self.need_acrossedge(verbose)
if verbose:
print(' Computing the boundary...', end ='')
sys.stdout.flush()
mtimer = Timer()
# find all boundary faces and edges
bfaces = [fidx for fidx in xrange(self.nsimplices) if -1 in self.across_edge[fidx]]
bedges = []
for fidx in bfaces:
face = self.simplices[fidx]
nbrs = self.across_edge[fidx]
bedges.extend( [[fidx, k] for k in xrange(3) if nbrs[k] == -1] )
self.bedges = numpy.array(bedges)
if verbose:
print(' Done! found', self.bedges.shape[0], 'boundary edges', end ='')
mtimer.end()
return self.bedges
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
def __init__(self, **kwargs):
'''
kwargs:
vertices: ndarray of shape (nverts, dim) # dim = 2,3
simplices: ndarray of shape (nfaces, dim+1)
verbose: verbosity level
'''
args = kwargs.keys()
if 'vertices' not in args:
raise SyntaxError("Mesh object needs vertex data")
verbose = 2
if 'verbose' in args:
verbose = kwargs['verbose']
self.vertices = kwargs['vertices']
self.dim = self.vertices.shape[1]
self.nvertices = self.vertices.shape[0]
if self.dim != 2 and self.dim != 3:
raise SyntaxError("Mesh object works for 2D and 3D only")
if verbose > 0:
print(' Initializing', self.dim, 'D mesh with', self.nvertices, 'vertices...', end ='')
sys.stdout.flush()
mtimer = Timer()
if verbose > 1:
print('')
# create simplices if needed
if 'simplices' in args:
self.Delaunay = None
self.simplices = kwargs['simplices']
if verbose > 1:
print(' got', self.simplices.shape[0], 'simplices')
else:
if verbose > 1:
print(' creating Delaunay mesh...')
sys.stdout.flush()
self.Delaunay = spatial.Delaunay(self.vertices)
self.simplices = self.Delaunay.simplices
if verbose > 1:
print(' Done! created', self.simplices.shape[0], 'simplices')
sys.stdout.flush()
self.nsimplices = self.simplices.shape[0]
if self.dim != self.simplices.shape[1]-1:
raise SyntaxError("Dimension mismatch! pdim = "+str(pdim)+" and sdim = "+str(sdim)+" do not match!")
self.adjacent_faces = []
self.across_edge = numpy.empty((0,0))
self.bedges = numpy.empty((0,0))
self.pvolumes = numpy.empty(0)
self.need_volumes(verbose > 1)
self.need_boundary(verbose > 1)
#self.need_meshmatrices(verbose > 1)
if verbose > 0:
print(' Done!', end ='')
mtimer.end()
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
def divcurl(self, vfield, verbose=False):
if vfield.shape != (self.nsimplices, self.dim):
print('UnstructuredGrid.divcurl', vfield.shape, self.dim, self.vertices.shape, self.simplices.shape)
raise ValueError("UnstructuredGrid requires a valid-dimensional vector field")
if verbose:
print(' Computing divcurl...', end ='')
sys.stdout.flush()
mtimer = Timer()
div = numpy.zeros(self.nvertices)
curlw = numpy.zeros(self.nvertices)
# for each face
for sidx in xrange(self.nsimplices):
simp = self.simplices[sidx]
for k in xrange(3):
v = simp[k]
a = simp[ (k+1)%3 ]
b = simp[ (k+2)%3 ]
# normal and tangent vectors
tvec = self.vertices[b] - self.vertices[a] # counterclockwise
nvec = numpy.array([-tvec[1], tvec[0]]) # inward
dn = numpy.dot(nvec, vfield[sidx])
tn = numpy.dot(tvec, vfield[sidx])
div[v] += dn
curlw[v] += tn
# fix for boundary edges
for bedge in self.bedges:
sidx = bedge[0]
eidx = bedge[1]
a = self.simplices[sidx][(eidx+1)%3]
b = self.simplices[sidx][(eidx+2)%3]
tvec = self.vertices[b] - self.vertices[a] # counterclockwise
nvec = numpy.array([-tvec[1], tvec[0]]) # inward
dn = numpy.dot(nvec, vfield[sidx])
dt = numpy.dot(tvec, vfield[sidx])
div[a] += dn
div[b] += dn
curlw[a] += tn
curlw[b] += tn
div = -0.5 * div
curlw = 0.5 * curlw
if verbose:
print(' Done!', end ='')
mtimer.end()
#print('\tdiv =', div.shape, div.min(), div.max())
#print('\tcurlw =', curlw.shape, curlw.min(), curlw.max())
return (div, curlw)
def gradient(self, sfield, verbose=False):
if sfield.shape[0] != self.nvertices:
print('UnstructuredGrid.gradient', self.dim, sfield.shape, self.vertices.shape, self.simplices.shape)
raise ValueError("UnstructuredGrid requires a valid-dimensional vector field")
if verbose:
print(' Computing gradient...', end ='')
sys.stdout.flush()
mtimer = Timer()
grad = numpy.zeros((self.nsimplices, self.dim))
# for 2D
for sidx in xrange(self.nsimplices):
deb = sidx==0
simp = self.simplices[sidx]
f = 0.5 / self.svolumes[sidx]
if deb:
print(sidx, simp, self.svolumes[sidx], f)
for k in xrange(3):
v = simp[k]
a = simp[ (k+1)%3 ]
b = simp[ (k+2)%3 ]
# normal and tangent vectors
tvec = self.vertices[b] - self.vertices[a] # counterclockwise
nvec = numpy.array([-tvec[1], tvec[0]]) # inward
grad[sidx] += f * sfield[v] * nvec
if deb:
print('\t', k, nvec, sfield[v],grad[sidx])
if verbose:
print(' Done!', end ='')
mtimer.end()
return grad
def rotated_gradient(self, sfield):
rgrad = self.gradient(sfield)
rgrad[:,[0, 1]] = rgrad[:,[1, 0]]
rgrad[:,0] *= -1.0
return rgrad
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
+13
View File
@@ -0,0 +1,13 @@
"""
Orisys - Optical Tactile Sensor SDK
A Python library for processing and analyzing optical tactile sensor data,
including image stitching, optical flow tracking, and deformation decomposition.
"""
__version__ = "0.1.0"
__author__ = "Your Name"
from .sensor import Sensor
__all__ = ['Sensor']
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+288
View File
@@ -0,0 +1,288 @@
'''
Copyright (c) 2015, Harsh Bhatia (bhatia4@llnl.gov)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
import numpy
from .nPoisson import PoissonSolver
from .StructuredGrid import StructuredGrid
from .UnstructuredGrid import UnstructuredGrid
# import matplotlib.pyplot as plt
# ---------------------------------------------------------------------------
class nHHD(object):
# constructor
def __init__(self, **kwargs):
args = kwargs.keys()
if ('grid' in args) == ('points' in args):
raise SyntaxError("nHHD Solver needs either shape of a regular grid or the points in an rectilinear/unstructured grid")
if ('grid' in args) != ('spacings' in args):
raise SyntaxError("nHHD needs spacings for regular grid")
if 'grid' in args:
self.mesh = StructuredGrid(grid=kwargs['grid'], spacings=kwargs['spacings'])
self.psolver = PoissonSolver(grid=self.mesh.dims, spacings=self.mesh.dx)
elif 'points' in args:
if 'simplices' in args:
self.mesh = UnstructuredGrid(vertices=kwargs['points'], simplices=kwargs['simplices'])
else:
self.mesh = UnstructuredGrid(vertices=kwargs['points'])
self.psolver = PoissonSolver(points=self.mesh.vertices, pvolumes=self.mesh.pvolumes)
self.dim = self.mesh.dim
if (self.dim != 2) and (self.dim != 3):
raise ValueError("nHHD Solver works only for 2 and 3 dimensions")
self.psolver.prepare(False)
# create the 3-component decomposition
def decompose(self, vfield, verbose=0):
if vfield.shape[-1] != self.dim:
raise ValueError("nHHD.decompose requires a valid-dimensional vector field")
if verbose > 0:
print( '\n ==> nhhd.decompose', vfield.shape)
if self.dim == 2:
# compute div curl
(self.div, self.curlw) = self.mesh.divcurl(vfield)
if verbose > 0:
print( 'div =', self.div.shape, self.div.min(), self.div.max())
print( 'curlw =', self.curlw.shape, self.curlw.min(), self.curlw.max())
# compute potentials
self.nD = self.psolver.solve_gpu(self.div, verbose > 0) #1)
if verbose > 0:
print( 'nD =', self.nD.shape, self.nD.min(), self.nD.max())
self.nRu = self.psolver.solve_gpu(self.curlw, verbose > 0) #1)
if verbose > 0:
print( 'nR =', self.nRu.shape, self.nRu.min(), self.nRu.max())
# compute fields as gradients of potentials
self.d = self.mesh.gradient(self.nD)
if verbose > 0:
print( 'd =', self.d.shape, self.d.min(), self.d.max())
self.r = self.mesh.rotated_gradient(self.nRu)
if verbose > 0:
print( 'r =', self.r.shape, self.r.min(), self.r.max())
else:
# compute div curl
(self.div, self.curlu, self.curlv, self.curlw) = self.mesh.divcurl(vfield)
if verbose > 0:
print( 'div =', self.div.shape, self.div.min(), self.div.max())
print( 'curlu =', self.curlu.shape, self.curlu.min(), self.curlu.max())
print( 'curlv =', self.curlv.shape, self.curlv.min(), self.curlv.max())
print( 'curlw =', self.curlw.shape, self.curlw.min(), self.curlw.max())
# compute potentials
self.nD = self.psolver.solve_gpu(self.div, verbose > 0) #1)
if verbose > 0:
print( 'nD =', self.nD.shape, self.nD.min(), self.nD.max())
self.nRu = self.psolver.solve_gpu(self.curlu, verbose > 0)
self.nRu *= -1.0
if verbose > 0:
print( 'nRu =', self.nRu.shape, self.nRu.min(), self.nRu.max())
self.nRv = self.psolver.solve_gpu(self.curlv, verbose > 0)
self.nRv *= -1.0
if verbose > 0:
print( 'nRv =', self.nRv.shape, self.nRv.min(), self.nRv.max())
self.nRw = self.psolver.solve_gpu(self.curlw, verbose > 0)
self.nRw *= -1.0
if verbose > 0:
print( 'nRw =', self.nRw.shape, self.nRw.min(), self.nRw.max())
# compute divergent field as gradient of D potential
self.d = self.mesh.gradient(self.nD)
if verbose > 0:
print( 'd =', self.d.shape, self.d.min(), self.d.max())
# temporary use of r
self.r = numpy.stack((self.nRu, self.nRv, self.nRw), axis = -1)
# compute rotational field as curl of R potential
(cu, cv, cw) = self.mesh.curl3D(self.r)
self.r = numpy.stack((cu, cv, cw), axis = -1)
if verbose > 0:
print( 'r =', self.r.shape, self.r.min(), self.r.max())
self.h = numpy.add(self.d, self.r)
numpy.subtract(vfield, self.h, self.h)
if verbose > 0:
print( 'h =', self.h.shape, self.h.min(), self.h.max())
def decompose_batch(self, vfield, verbose=0):
if vfield.shape[-1] != self.dim:
raise ValueError("nHHD.decompose requires a valid-dimensional vector field")
if verbose > 0:
print( '\n ==> nhhd.decompose', vfield.shape)
if self.dim == 2:
# compute div curl using GPU
(self.div, self.curlw) = self.mesh.divcurl(vfield)
if verbose > 0:
print( 'div =', self.div.shape, self.div.min(), self.div.max())
print( 'curlw =', self.curlw.shape, self.curlw.min(), self.curlw.max())
# compute potentials using batch GPU solver
batch_inputs = [self.div, self.curlw]
batch_results = self.psolver.solve_gpu_batch(batch_inputs, verbose > 0)
self.nD = batch_results[0]
self.nRu = batch_results[1]
if verbose > 0:
print( 'nD =', self.nD.shape, self.nD.min(), self.nD.max())
print( 'nR =', self.nRu.shape, self.nRu.min(), self.nRu.max())
# compute fields as gradients of potentials
self.d = self.mesh.gradient(self.nD)
if verbose > 0:
print( 'd =', self.d.shape, self.d.min(), self.d.max())
self.r = self.mesh.rotated_gradient(self.nRu)
if verbose > 0:
print( 'r =', self.r.shape, self.r.min(), self.r.max())
else:
# compute div curl
(self.div, self.curlu, self.curlv, self.curlw) = self.mesh.divcurl(vfield)
if verbose > 0:
print( 'div =', self.div.shape, self.div.min(), self.div.max())
print( 'curlu =', self.curlu.shape, self.curlu.min(), self.curlu.max())
print( 'curlv =', self.curlv.shape, self.curlv.min(), self.curlv.max())
print( 'curlw =', self.curlw.shape, self.curlw.min(), self.curlw.max())
# compute potentials using batch GPU solver
batch_inputs = [self.div, self.curlu, self.curlv, self.curlw]
batch_results = self.psolver.solve_gpu_batch(batch_inputs, verbose > 0)
self.nD = batch_results[0]
self.nRu = batch_results[1] * -1.0
self.nRv = batch_results[2] * -1.0
self.nRw = batch_results[3] * -1.0
if verbose > 0:
print( 'nD =', self.nD.shape, self.nD.min(), self.nD.max())
print( 'nRu =', self.nRu.shape, self.nRu.min(), self.nRu.max())
print( 'nRv =', self.nRv.shape, self.nRv.min(), self.nRv.max())
print( 'nRw =', self.nRw.shape, self.nRw.min(), self.nRw.max())
# compute divergent field as gradient of D potential
self.d = self.mesh.gradient(self.nD)
if verbose > 0:
print( 'd =', self.d.shape, self.d.min(), self.d.max())
# temporary use of r
self.r = numpy.stack((self.nRu, self.nRv, self.nRw), axis = -1)
# compute rotational field as curl of R potential
(cu, cv, cw) = self.mesh.curl3D(self.r)
self.r = numpy.stack((cu, cv, cw), axis = -1)
if verbose > 0:
print( 'r =', self.r.shape, self.r.min(), self.r.max())
self.h = numpy.add(self.d, self.r)
numpy.subtract(vfield, self.h, self.h)
if verbose > 0:
print( 'h =', self.h.shape, self.h.min(), self.h.max())
def get_force(self, vfield, verbose=False):
mgrid = numpy.mgrid[0: vfield.shape[0], 0: vfield.shape[1]]
grid = numpy.stack((mgrid[0], mgrid[1]), axis =2)
# print(grid.shape)
self.decompose(vfield, verbose)
self.Sn = numpy.sum(numpy.absolute(self.d))
# print("Sn is:", self.Sn)
self.Stx = abs(numpy.sum(self.h[:, :, 0]))
self.Sty = abs(numpy.sum(self.h[:, :, 1]))
# print("Stx is:", self.Stx)
# print("Sty is:", self.Sty)
nR_max_loc = numpy.unravel_index(numpy.argmax(self.nRu, axis = None), self.nRu.shape)
nR_min_loc = numpy.unravel_index(numpy.argmin(self.nRu, axis = None), self.nRu.shape)
# print(nR_max_loc)
r_p = grid - nR_max_loc
r_n = grid - nR_min_loc
self.Stor = numpy.sum(numpy.cross(r_p, self.r)) + numpy.sum(numpy.cross(r_n, self.r))
# print("Stor is:", self.Stor)
output = numpy.array([self.Sn, self.Stx, self.Sty, self.Stor])
flag = False
if self.Sn >=300 and self.Stx >= 300 and self.Sty >= 300 and numpy.abs(self.Stor) >= 1000:
flag = True
# print(flag)
return flag,output
# return self.d, self.h, numpy.cross(r_p, self.r) , numpy.cross(r_n, self.r)
def get_shear(self):
return self.h
def get_normal(self):
return self.d
# def plot_2d_information(self,value):
# fig,ax = plt.subplots()
# x,y = [],[]
# for i in range (1000000000000):
# x.append(i)
# y.append(value)
# ax.cla()
# ax.plot(x,y,'b',lw=1)
# plt.pause(0.1)
# ---------------------------------------------------------------------------
+518
View File
@@ -0,0 +1,518 @@
'''
Copyright (c) 2015, Harsh Bhatia (bhatia4@llnl.gov)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
import numpy
from scipy import signal, spatial
from .timer import Timer
# Try to import CuPy for GPU acceleration
try:
import cupy as cp
import cupyx.scipy.signal as cp_signal
CUPY_AVAILABLE = True
except ImportError:
cp = None
cp_signal = None
CUPY_AVAILABLE = False
'''
# ------------------------------------------------------------------------------
Poisson Solver for 1D, 2D, and 3D scalar fields.
Data may be defined on a rectlinear grid or unstructured points.
1) create an object
PoissonSolver(args)
'solver' : type of solver
'S' for spatial convolution
only option for unstructured points
'F' for convolution in frequency space
default option for regular grids
--> for regular grid
'grid' : shape of the rectlinear grid
tuple of dimensions (Z,Y,X)
'spacings' : spacings of the rectlinear grid
tuple of spacings (dz,dy,dx)
--> for unstrucutred points
'points' : unstructured points
numpy array of shape (N, dim)
'pvolumes' : point volumes for each point
numpy array of shape (N, 1)
2) call prepare()
this initializes internal datastructures depending upon the mesh.
Once initialized, solve() can be called any number of times to
solve the Poisson equation on the same domain. If the domain
changes (e.g., a Lagrangian mesh), create a new object.
3) call solve(f), where, f is a numpy array defining a scalar field
for rectlinear grid, f.shape = grid.shape
for unstructured points, f.shape = (points.shape[0], 1)
# ------------------------------------------------------------------------------
'''
class PoissonSolver(object):
# constructor
def __init__(self, **kwargs):
args = kwargs.keys()
if ('grid' in args) == ('points' in args):
raise SyntaxError("Poisson Solver needs either shape of a regular grid or the points in an rectilinear/unstructured grid")
if ('grid' in args) != ('spacings' in args):
raise SyntaxError("Poisson Solver needs spacings for regular grid")
if ('points' in args) != ('pvolumes' in args):
raise SyntaxError("Poisson Solver needs point volumes for unstructured points")
# (default) data type
self.dtype = numpy.float32
if 'dtype' in args:
self.dtype = kwargs['dtype']
if 'grid' in args:
self.ptype = 'G'
self.stype = 'F' # default
self.gdims = kwargs['grid']
self.gdx = kwargs['spacings']
self.dim = len(self.gdims)
if self.dim != len(self.gdx):
raise ValueError("Dimensions of spacings should match that of the grid")
elif 'points' in args:
self.ptype = 'P'
self.stype = 'S' # only option
self.points = kwargs['points']
self.pvolumes = kwargs['pvolumes']
self.dim = len(self.points.shape)
if self.pvolumes.shape[0] != self.points.shape[0]:
raise ValueError("Number of pvolumes should match that of the points")
if 'solver' in args:
if (kwargs['solver'] != 'S' and kwargs['solver'] != 'F'):
raise SyntaxError('Solver can be only of type S(patial) or F(requency)')
if (kwargs['solver'] == 'F') and ('points' in args):
raise SyntaxError('Only Spatial solver supported for unstructured points')
if (kwargs['solver'] == 'S') and ('grid' in args):
raise SyntaxError('Frequncy solver strongly suggested for structured grids')
self.stype = kwargs['solver']
if (self.dim > 3):
raise ValueError("Poisson Solver works only for 1, 2, or 3 dimensions")
self.ready = False
self._gpu_cache = None # Cache for GPU arrays
# if (self.ptype == 'G'):
# print( 'PoissonSolver:', self.dim, 'D grid =', self.gdims, 'with spacings', self.gdx)
# elif (self.ptype == 'P'):
# print( 'PoissonSolver', self.dim, 'D points =', self.points.shape)
# --------------------------------------------------------------------------
# create the Green's function in appropriate representation
# --------------------------------------------------------------------------
# generate a grid -(N-1) to (N-1) to define distance function
# where N is the size of the original grid
def _create_radialGrid(self, verbose):
mtimer = Timer()
if verbose:
print( ' - creating distance kernel:', end ='')
rdims = tuple(2*d - 1 for d in self.gdims)
R = numpy.indices(rdims, dtype=self.dtype)
for d in range(self.dim):
R[d] += 1-self.gdims[d]
R[d] *= self.gdx[d]
if self.dim == 1:
numpy.absolute(R[0], R[0])
R = R[0]
elif self.dim == 2:
numpy.hypot(R[1], R[0], R[0])
R = R[0]
# half of the average grid spacing
zval = 0.5 * (self.gdx[0]+self.gdx[1]) / 2.0
R[self.gdims[0]-1, self.gdims[1]-1] = zval
elif self.dim == 3:
numpy.hypot(R[2], R[1], R[1])
numpy.hypot(R[1], R[0], R[0])
R = R[0]
# half of the average grid spacing
zval = 0.5 * (self.gdx[0]+self.gdx[1]+self.gdx[2]) / 3.0
R[self.gdims[0]-1, self.gdims[1]-1, self.gdims[2]-1] = zval
if verbose:
print( R.shape, R.min(), R.max())
mtimer.end()
return R
# --------------------------------------------------------------------------
# scale a function by point volumes for correct integration
def _scale(self, func):
if self.ptype == 'P':
return numpy.multiply(func, self.pvolumes)
elif self.ptype == 'G':
sfunc = func * numpy.prod(self.gdx)
'''
# volume of the boundary elements is only half the voxel
if self.dim == 1:
sfunc[ 0] *= 0.5
sfunc[-1] *= 0.5
elif self.dim == 2:
sfunc[ 0, :] *= 0.5
sfunc[-1, :] *= 0.5
sfunc[ :, 0] *= 0.5
sfunc[ :,-1] *= 0.5
elif self.dim == 3:
sfunc[ 0, :, :] *= 0.5
sfunc[-1, :, :] *= 0.5
sfunc[ :, 0, :] *= 0.5
sfunc[ :,-1, :] *= 0.5
sfunc[ :, :, 0] *= 0.5
sfunc[ :, :,-1] *= 0.5
'''
return sfunc
def prepare(self, verbose=False):
# ----------------------------------------------------------------------
gtimer = Timer()
if verbose:
print( '\nInitializing Poisson solver, type =', self.stype)
# ----------------------------------------------------------------------
# generate a grid to store pairwise distances to store Green's function
self.G = None # definition depends upon solver type
# ----------------------------------------------------------------------
# for Fourier solution,
if self.stype == 'F':
if self.ptype == 'G':
self.G = self._create_radialGrid(verbose)
elif self.ptype == 'P':
raise SyntaxError('Frequncy solver not supported for unstructured points')
#self.G = create_distArray()
# ----------------------------------------------------------------------
# for spatial solution,
# use the pairwise distance matrix, N x N
# where N is the number of points
elif self.stype == 'S':
if self.ptype == 'G':
raise SyntaxError('Spatial solver not suggested for structured grids')
#self.G = self.create_points(verbose)
elif self.ptype == 'P':
# for 1D points, add an extra axis
if (self.dim == 1) and (len(self.points.shape) == 1):
self.points = self.points[:,numpy.newaxis]
# create pairwise distance matrix now
ltimer = Timer()
self.G = spatial.distance.cdist(self.points, self.points)
# TODO: use a good approximation here
zval = numpy.power(10.0, -10.0)
self.G += zval * numpy.identity(self.G.shape[0])
if verbose:
print(' - created pairwise distance matrix:', self.G.shape, self.G.min(), self.G.max(), end = "")
ltimer.end()
# ----------------------------------------------------------------------
# compute the Green's function
ltimer = Timer()
if verbose:
print( ' - computing the Green\'s function:')
if self.dim == 1:
numpy.multiply(self.G, 0.5, self.G)
elif self.dim == 2:
numpy.log(self.G, self.G)
numpy.multiply(self.G, (0.5 / numpy.pi), self.G)
elif self.dim == 3:
numpy.reciprocal(self.G, self.G)
numpy.multiply(self.G, (-0.25 / numpy.pi), self.G)
if verbose:
print( self.G.shape, self.G.min(), self.G.max(), end = "")
ltimer.end()
# ----------------------------------------------------------------------
self.ready = True
if verbose:
print( 'Poisson solver initialized', end ='')
gtimer.end()
# --------------------------------------------------------------------------
# compute the integral solution
# --------------------------------------------------------------------------
def solve(self, f, verbose=False):
if not self.ready:
init(verbose)
fshape = f.shape
gtimer = Timer()
if verbose:
print( '\nSolving Poisson Eq.')
# ----------------------------------------------------------------------
# regular grid
if self.ptype == 'G':
#if (self.gdims - fshape).any():
if self.gdims != fshape:
print( self.gdims, fshape)
raise ValueError("Shape of function should match shape of grid")
# convolution in frequency domain
if self.stype == 'F':
p = signal.fftconvolve(f, self.G, mode='same')
numpy.multiply(p, numpy.prod(self.gdx), p)
# convolution in spatial domain
elif self.stype == 'S':
raise SyntaxError('Spatial solver not suggested for structured grids')
#p = (self.G).dot(f.reshape(-1)).reshape(fshape)
# ----------------------------------------------------------------------
# unstructured points
elif self.ptype == 'P':
if (self.points.shape[0] != fshape[0]) or (1 != len(fshape)):
raise ValueError("Shape of function should be (N,1) with N as the number of points")
# convolution in spatial domain
# Dec 13, 2017 -- pre scaling works for unstructured grids!
if self.stype == 'S':
p = numpy.multiply(f, self.pvolumes)
numpy.dot(self.G, p, p)
#numpy.multiply(p, self.pvolumes, p)
elif self.stype == 'F':
raise SyntaxError('Frequncy solver not supported for unstructured points')
# ----------------------------------------------------------------------
if verbose:
print( 'Poisson solver finished')
gtimer.end()
return p
# --------------------------------------------------------------------------
# GPU-accelerated solve using CuPy
# --------------------------------------------------------------------------
def solve_gpu(self, f, verbose=False):
"""
GPU-accelerated Poisson solver using CuPy.
Falls back to CPU if CuPy is not available.
Args:
f: Input field (numpy array)
verbose: Print timing information
Returns:
p: Solution (numpy array on CPU)
"""
if not CUPY_AVAILABLE:
if verbose:
print("CuPy not available, falling back to CPU solver")
return self.solve(f, verbose)
if not self.ready:
self.prepare(verbose)
fshape = f.shape
gtimer = Timer()
if verbose:
print('\nSolving Poisson Eq. on GPU')
# ----------------------------------------------------------------------
# regular grid
if self.ptype == 'G':
if self.gdims != fshape:
print(self.gdims, fshape)
raise ValueError("Shape of function should match shape of grid")
# convolution in frequency domain using GPU
if self.stype == 'F':
# Initialize GPU cache if needed
if self._gpu_cache is None:
if verbose:
print(' - Transferring Green\'s function to GPU...')
self._gpu_cache = {
'G_gpu': cp.asarray(self.G, dtype=cp.float32),
'gdx_prod': numpy.prod(self.gdx)
}
# Transfer input to GPU
f_gpu = cp.asarray(f, dtype=cp.float32)
# Perform FFT convolution on GPU
# CuPy's fftconvolve is GPU-accelerated
p_gpu = cp_signal.fftconvolve(f_gpu, self._gpu_cache['G_gpu'], mode='same')
# Scale by grid spacing (on GPU)
p_gpu *= self._gpu_cache['gdx_prod']
# Transfer result back to CPU
p = cp.asnumpy(p_gpu)
elif self.stype == 'S':
raise SyntaxError('Spatial solver not suggested for structured grids')
# ----------------------------------------------------------------------
# unstructured points - not optimized for GPU
elif self.ptype == 'P':
if verbose:
print(' - GPU solver not optimized for unstructured points, using CPU')
return self.solve(f, verbose)
# ----------------------------------------------------------------------
if verbose:
print('Poisson solver (GPU) finished')
gtimer.end()
return p
def solve_gpu_batch(self, f_list, verbose=False):
"""
Batch GPU-accelerated Poisson solver for multiple inputs.
Solves multiple Poisson equations in a single GPU call.
Args:
f_list: List of input fields (numpy arrays)
verbose: Print timing information
Returns:
List of solutions (numpy arrays on CPU)
"""
if not CUPY_AVAILABLE:
if verbose:
print("CuPy not available, falling back to CPU solver")
return [self.solve(f, verbose) for f in f_list]
if not self.ready:
self.prepare(verbose)
gtimer = Timer()
if verbose:
print(f'\nSolving {len(f_list)} Poisson Eq. on GPU (batched)')
# Verify all inputs have same shape
fshape = f_list[0].shape
for f in f_list:
if f.shape != fshape:
raise ValueError("All input fields must have the same shape for batch processing")
# ----------------------------------------------------------------------
# regular grid
if self.ptype == 'G':
if self.gdims != fshape:
raise ValueError("Shape of function should match shape of grid")
# convolution in frequency domain using GPU
if self.stype == 'F':
# Initialize GPU cache if needed
if self._gpu_cache is None:
if verbose:
print(' - Transferring Green\'s function to GPU...')
self._gpu_cache = {
'G_gpu': cp.asarray(self.G, dtype=cp.float32),
'gdx_prod': numpy.prod(self.gdx)
}
results = []
# Process all inputs in batch
for i, f in enumerate(f_list):
# Transfer input to GPU
f_gpu = cp.asarray(f, dtype=cp.float32)
# Perform FFT convolution on GPU
p_gpu = cp_signal.fftconvolve(f_gpu, self._gpu_cache['G_gpu'], mode='same')
# Scale by grid spacing (on GPU)
p_gpu *= self._gpu_cache['gdx_prod']
# Transfer result back to CPU
results.append(cp.asnumpy(p_gpu))
if verbose:
print(f' - Solved {i+1}/{len(f_list)}')
if verbose:
print('Poisson solver (GPU batch) finished')
gtimer.end()
return results
elif self.stype == 'S':
raise SyntaxError('Spatial solver not suggested for structured grids')
# ----------------------------------------------------------------------
# unstructured points - not optimized for GPU
elif self.ptype == 'P':
if verbose:
print(' - GPU solver not optimized for unstructured points, using CPU')
return [self.solve(f, verbose) for f in f_list]
return results
def clear_gpu_cache(self):
"""Clear GPU cache to free GPU memory"""
if self._gpu_cache is not None:
self._gpu_cache = None
if CUPY_AVAILABLE:
cp.get_default_memory_pool().free_all_blocks()
+486
View File
@@ -0,0 +1,486 @@
"""
Stch Calibration OK
"""
import os
import numpy as np
from scipy import ndimage
import cv2
from .Stitcher import Stitcher
from .tracker import Tracker
from .nHHD import *
import time
from scipy.interpolate import griddata
from enum import Enum, auto
from typing import Optional, Tuple, Union, Any
class Sensor:
"""
Sensor helper that can decompose optical flow via Fourier methods.
Stores the decomposed components and global norms as attributes after compute().
"""
def __init__(self, vid_src, isstitch=False, verbose=True, cuda=True):
"""
Initialize Sensor with video source and configuration.
Args:
vid_src: Video source (int for camera ID, str for file path, None for default camera)
isstitch: Whether to use stitching mode for multi-tile sensor (default: False)
verbose: Whether to print initialization logs (default: True)
cuda: Whether to use CUDA acceleration for optical flow & decomposition (default: True)
"""
try:
import cupy as cp
import cupyx.scipy.signal as cp_signal
self.CUPY_AVAILABLE = True
except ImportError:
self.CUPY_AVAILABLE = False
# Initialize settings
if vid_src is not None:
vid_src = vid_src
else:
print("User must specify a video source.")
self.vid_src, self.isstitch, self.cuda, self.verbose = vid_src, isstitch, cuda, verbose
self.index = np.array([1,2,3,6,7,8,11,12,13])
# Initialize components
self.__stitcher = Stitcher(isstitch=isstitch, index=self.index)
self.tracker = Tracker(adaptive=True, cuda=cuda)
self.hhd = None
if self.verbose:
print("Components initialized")
# Setup video capture
self.cap = cv2.VideoCapture(vid_src)
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
self.cap.set(cv2.CAP_PROP_FPS, 30)
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
if not self.cap.isOpened():
raise RuntimeError("Could not open video source")
# Wait for first valid frame
if self.verbose:
print("Sensor connecting to video source...")
frame = None
while frame is None:
ret, frame = self.cap.read()
if not ret:
time.sleep(1)
if self.verbose:
print("Video source connected.")
# Initialize and calibrate
self.__find_stitch_boxes(frame, show=False)
self.__get_tiles(frame)
self.stitch_cal()
if self.verbose:
print("Sensor initialized and calibrated.")
# Initialize state variables
self.stitch_valid = False
self.n_boxes = None
self.stitch_calibration = []
self.img = self.flow = self.normal = self.shear = self.force = None
self.fnormal=self.fshearx=self.fsheary=self.ftor=0.0
self.imgupdate = self.vupdate = False
self.pos_default = [(260, 56), (577, 62), (897, 69), (1214, 75), (1530, 81), (256, 391), (570, 397), (889, 403), (1208, 409), (1523, 416), (248, 724), (563, 730), (882, 738), (1201, 743), (1516, 751)]
# Logging state
self.last_log_time = time.time()
self.fps = 0
if self.verbose:
print("Settings:")
print(f" Video source: {vid_src}")
print(f" Stitch mode: {isstitch}")
print(f" CUDA acceleration: {cuda}")
print(f" Cupy available: {self.CUPY_AVAILABLE}")
print(f" OpenCV CUDA devices: {cv2.cuda.getCudaEnabledDeviceCount()}")
class info(Enum):
VRAW = auto()
VNORMAL = auto()
VSHEAR = auto()
FPS = auto()
TIMESTAMP = auto()
FNORMAL = auto()
FSHEARX = auto()
FSHEARY = auto()
def get_img(self):
"""
Capture frame from video source, convert to grayscale, and perform stitching.
Returns:
np.ndarray: Stitched grayscale image (contiguous array) or None if capture fails
"""
ret, frame = self.cap.read()
if not ret:
print("Failed to get_img from video source.")
self.cap.release()
exit(1)
return None
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
self.__stitcher.get_stitch_unit(gray_frame)
self.__get_tiles(gray_frame)
img = np.ascontiguousarray(self.__stitcher.stitch_rt_optimized())
self.img = cv2.resize(img, (400, 400), interpolation=cv2.INTER_CUBIC)
self.imgupdate = True
return self.img
def compute_deformation(self):
"""
Track optical flow and decompose into force field.
Returns:
tuple: (flow, force) where:
- flow: Optical flow field (2D vector field)
- force: Decomposed force components from nHHD, ([normal, shear_x, shear_y, torque]). Note: torque is experimental.
"""
flow = self.tracker.track(self.img)
if hasattr(flow, 'download'):
flow = flow.download()
if self.hhd is None:
self.hhd = nHHD(grid=(flow.shape[0], flow.shape[1]), spacings=(18, 18))
self.hhd.decompose(flow)
self.imgupdate = False
self.vupdate = True
self.flow = flow
self.normal = self.hhd.get_normal()
self.shear = self.hhd.get_shear()
flag, [self.fnormal,self.fshearx,self.fsheary,self.ftor] = self.hhd.get_force(flow)
self.fps = 1/(time.time() - self.last_log_time)
self.last_log_time = time.time()
return self.flow
def read_info(self, *args: info) -> Tuple:
"""
Get sensor data according to specified info types.
Args:
*args: Variable number of info enum values specifying desired data
Returns:
Tuple: Data in the order specified by args. Each element corresponds to:
- VRAW: Raw optical flow field (np.ndarray)
- VNORMAL: Normal component of flow (np.ndarray)
- VSHEAR: Shear component of flow (np.ndarray)
- FNORMAL: Normal force magnitude (float)
- FSHEAR: Shear force magnitude (float)
- FPS: Current frames per second (float)
- TIMESTAMP: Current timestamp string (str)
Example:
normal, shear, fps = sensor.read_info(sensor.info.VNORMAL, sensor.info.VSHEAR, sensor.info.FPS)
Note:
Must call compute_deformation() before reading flow/force data
"""
if not self.vupdate:
print("Warning: Update field first via compute_deformation() before reading flow/force info.")
results = []
for arg in args:
if not isinstance(arg, self.info):
print(f"Warning: Invalid argument type {type(arg)}, skipping")
continue
if arg == self.info.VRAW:
results.append(self.flow if self.flow is not None else np.array([]))
elif arg == self.info.VNORMAL:
results.append(self.normal if self.normal is not None else np.array([]))
elif arg == self.info.VSHEAR:
results.append(self.shear if self.shear is not None else np.array([]))
elif arg == self.info.FNORMAL:
results.append(self.fnormal)
elif arg == self.info.FSHEARX:
results.append(self.fshearx)
elif arg == self.info.FSHEARY:
results.append(self.fsheary)
elif arg == self.info.FPS:
results.append(self.fps)
elif arg == self.info.TIMESTAMP:
results.append(time.strftime('%H:%M:%S'))
return tuple(results) if results else ()
def disconnect(self):
"""
Release video capture resources.
"""
if self.cap.isOpened():
self.cap.release()
if self.verbose:
print("Sensor disconnected.")
cv2.destroyAllWindows()
def __get_tiles(self, img_src):
"""
Extract individual tiles from source image for stitching.
Args:
img_src: Source image (grayscale) containing multiple vision tiles
Returns:
bool: True (always succeeds)
"""
self.__stitcher.get_stitch_unit(img_src)
self.__stitcher.extract_tiles()
return True
def __find_stitch_boxes(self, img_src, show=False):
"""
Detect vision tile boxes in the frame, waiting for camera stabilization.
Args:
img_src: Initial source image (not actively used, boxes detected from live capture)
show: Whether to display detected boxes visually (default: False)
Returns:
int: Number of boxes detected (should be 15 for valid stitching)
"""
n_boxes, v_mean0, timeout = 0, 0, 10
start_time = cv2.getTickCount()
# print("Waiting for camera stabilization...")
while n_boxes < 15:
ret, frame = self.cap.read()
v_mean = np.sum(cv2.mean(frame))
if abs(v_mean - v_mean0) < 5:
n_boxes = self.__stitcher.find_stitch_boxes(frame, show=show)
if (cv2.getTickCount() - start_time) / cv2.getTickFrequency() > timeout:
print("Timeout for finding valid vision units.")
self.isstitch = False
self.__stitcher.isstitch = False
break
v_mean0 = v_mean
# print("Camera stabilized.")
# print("Checking validabity for stitching...")
self.n_boxes = n_boxes
self.stitch_valid = self.isstitch and n_boxes == 15
if self.isstitch:
print("Sensor is valid for stithcing." if self.stitch_valid else "Sensor not valid to stitching, switching to non-stitch mode")
else:
print("Using non-stitch mode.")
return n_boxes
def stitch_cal(self):
"""
Load or create stitch calibration for aligning multiple tiles.
Returns:
np.ndarray: Stitched image after calibration
Note:
- Loads calibration from ./config/cal.npy if exists
- Creates new calibration and saves to ./config/cal.npy if not found
- Calibration contains transformation parameters for tile alignment
"""
cal_path = "./config/cal.npy"
if os.path.exists(cal_path):
print("Loading existing calibration from", cal_path)
self.stitch_calibration = np.load(cal_path, allow_pickle=True)
self.__stitcher.stitch_calibration = self.stitch_calibration
img_stitched_height = self.__stitcher.stitch_rt_optimized()
else:
print("Calibration file not found, creating new calibration...")
img_stitched_height, self.stitch_calibration = self.__stitcher.stitch_cal()
os.makedirs("./config", exist_ok=True)
np.save(cal_path, self.stitch_calibration)
print("Calibration saved to", cal_path)
# print("Image stitched with shape:", img_stitched_height.shape)
return img_stitched_height
def draw_arrows(self, info_type: info,fps=True, threshold=5, grid_spacing=40, arrow_scale=1.0) -> np.ndarray:
"""
Draw colored optical flow arrows on sensor image based on flow magnitude.
Args:
threshold: Minimum flow magnitude to display arrow (default: 10.0)
grid_spacing: Spacing between arrows in pixels (default: 20)
arrow_scale: Scale factor for arrow length (default: 3.0)
Returns:
np.ndarray: 400x400 BGR image with colored arrows (JET colormap)
"""
# Create working copies to avoid modifying originals
image = self.img.copy()
if info_type is None:
print("Flow field type not specified, defaulting to show raw vector field.")
optical_flow = self.flow.copy()
elif info_type == self.info.VRAW:
optical_flow = self.flow.copy()
elif info_type == self.info.VNORMAL:
optical_flow = self.normal.copy()
elif info_type == self.info.VSHEAR:
optical_flow = self.shear.copy()
# Convert grayscale to BGR to enable colored arrow drawing
if len(image.shape) == 2 or image.shape[2] == 1:
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Calculate scaling factors between image and flow dimensions
img_height, img_width = image.shape[:2]
flow_height, flow_width = optical_flow.shape[:2]
scale_x, scale_y = img_width / flow_width, img_height / flow_height
# Create sampling grid for flow field (used for interpolation source points)
sample_step = 19 # Spacing between sample points
flow_start = np.stack(np.meshgrid(range(0, flow_width, sample_step), range(0, flow_height, sample_step)), 2)
flow_end = (optical_flow[flow_start[:, :, 1], flow_start[:, :, 0], :] * 2.0 + flow_start).astype(np.int32)
# Create regular grid for arrow placement in image coordinates
grid_x, grid_y = np.mgrid[grid_spacing//2:flow_height:grid_spacing, grid_spacing//2:flow_width:grid_spacing]
x_axis = (grid_y.reshape(-1) * scale_x).astype(np.int64) # Scale to image width
y_axis = (grid_x.reshape(-1) * scale_y).astype(np.int64) # Scale to image height
# Interpolate flow vectors from sample grid to regular arrow grid using cubic interpolation
grid_vector = griddata(
flow_start.reshape(-1, 2),
(flow_end - flow_start).reshape(-1, 2), # Flow from start to end (original to new position)
(grid_y, grid_x), method='cubic', fill_value=0
)
grid_vector[np.isnan(grid_vector)] = 0 # Replace NaN with zero for stability
grid_flat_scaled = grid_vector.reshape(-1, 2) * np.array([scale_x, scale_y])
# Compute arrow endpoints and ensure they stay within image bounds
end_x = (grid_flat_scaled[:, 0] / arrow_scale + x_axis).astype(np.int64)
end_y = (grid_flat_scaled[:, 1] / arrow_scale + y_axis).astype(np.int64)
valid_mask = (y_axis >= 0) & (y_axis < img_height) & (x_axis >= 0) & (x_axis < img_width)
valid_mask &= (end_x >= 0) & (end_x < img_width) & (end_y >= 0) & (end_y < img_height)
# Filter arrows by magnitude threshold and compute colors using JET colormap
magnitudes = np.linalg.norm(grid_flat_scaled, axis=1)
valid_mask &= (magnitudes >= 0) # Only show arrows above threshold
valid_indices = np.where(valid_mask)[0]
mag_range = magnitudes.max() - magnitudes.min()
# Normalize magnitudes to 0-255 range for colormap application, but set below-threshold values to 0 (blue)
norm_magnitudes = ((magnitudes - magnitudes.min()) / mag_range * 255.0).astype(np.uint8) if mag_range > 0 else np.zeros_like(magnitudes, dtype=np.uint8)
norm_magnitudes[magnitudes < threshold] = 0 # Keep lowest color (blue) for below-threshold magnitudes
colors_array = cv2.applyColorMap(norm_magnitudes.reshape(-1, 1), cv2.COLORMAP_JET) # JET: blue (low) -> red (high)
# Draw arrows at each valid grid point with magnitude-based coloring
for i in valid_indices:
color = tuple(map(int, colors_array[i, 0]))
# Scale arrow length and thickness based on threshold
if magnitudes[i] < threshold:
# Below threshold: draw as small dot (zero-length arrow with larger tip)
cv2.circle(image, (x_axis[i], y_axis[i]), radius=1, color=color, thickness=-1)
else:
# Above threshold: draw full arrow with thickness proportional to magnitude
# Arrow rooted at original position (x_axis, y_axis), head points to new position (end_x, end_y)
thickness = max(1, int(2 * magnitudes[i] / magnitudes.max())) if magnitudes.max() > 0 else 2
cv2.arrowedLine(image, (x_axis[i], y_axis[i]), (end_x[i], end_y[i]), color, thickness=thickness, tipLength=0.3)
out_img = cv2.resize(image, (400, 400), interpolation=cv2.INTER_CUBIC)
# if fps:
# out_img = self.draw_fps(image=out_img)
return out_img
def draw_magnitude_map(self, info_type: info = None, threshold=3.0, colormap=cv2.COLORMAP_JET) -> np.ndarray:
"""
Draw color map of pointwise magnitude of velocity field.
Args:
info_type: Type of flow field to visualize (VRAW, VNORMAL, VSHEAR, or None for total)
threshold: Minimum magnitude to display (below threshold shows as zero/blue)
colormap: OpenCV colormap to use (default: COLORMAP_JET)
Returns:
np.ndarray: 400x400 BGR image with colored magnitude map
"""
# Get the appropriate flow field
if info_type is None or info_type == self.info.VRAW:
flow_field = self.flow.copy() if self.flow is not None else None
elif info_type == self.info.VNORMAL:
flow_field = self.normal.copy() if self.normal is not None else None
elif info_type == self.info.VSHEAR:
flow_field = self.shear.copy() if self.shear is not None else None
else:
# For combined normal + shear magnitude
if self.normal is not None and self.shear is not None:
flow_field = self.normal + self.shear
else:
flow_field = None
if flow_field is None:
# Return blank image if no flow data
return np.zeros((400, 400, 3), dtype=np.uint8)
# Compute magnitude at each point
magnitude = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
# Smooth by downsampling and interpolating back
downsample_factor = 10
h, w = magnitude.shape
magnitude_downsampled = magnitude[::downsample_factor, ::downsample_factor]
magnitude_smoothed = cv2.resize(magnitude_downsampled, (w, h), interpolation=cv2.INTER_CUBIC)
# Apply threshold - set below-threshold values to zero
magnitude_thresholded = magnitude_smoothed.copy()
magnitude_thresholded[magnitude_smoothed < threshold] = 0
# Normalize to 0-255 range
mag_min, mag_max = magnitude_thresholded.min(), magnitude_thresholded.max()
if mag_max > mag_min:
magnitude_norm = ((magnitude_thresholded - mag_min) / (mag_max - mag_min) * 255).astype(np.uint8)
else:
magnitude_norm = np.zeros_like(magnitude_thresholded, dtype=np.uint8)
# Apply colormap
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
# Resize to output size
out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
return out_img
def draw_fps(self, image=None):
"""
Draw FPS counter on the top left corner of the image.
"""
font_scale=0.5
thickness=2
color=(0, 255, 0)
image = image.copy()
fps = self.fps
# Draw FPS text
fps_text = f"FPS: {fps:.1f}"
position = (10, 30) # Top left corner with margin
font = cv2.FONT_HERSHEY_SIMPLEX
# Draw FPS text
cv2.putText(image, fps_text, position, font, font_scale, color, thickness, cv2.LINE_AA)
return image
+59
View File
@@ -0,0 +1,59 @@
'''
Copyright (c) 2015, Harsh Bhatia (bhatia4@llnl.gov)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
import numpy
import timeit
class Timer(object):
def display(self):
tseconds = self.etime - self.stime
print(' [[ elapsed time:', end = "")
if tseconds < numpy.power(10.,-6.):
print("%.3f micro-sec." % (tseconds*numpy.power(10,6)),end = "")
elif tseconds < numpy.power(10.,-3.):
print("%.3f milli-sec." % (tseconds*numpy.power(10.,3.)),end = "")
elif tseconds < 60.0:
print("%.3f sec." % (tseconds),end = "")
else:
m = int(tseconds/ 60.0)
print("%d min. %.3f sec." % (m, (tseconds - 60*m)),end = "")
print(']]')
def start(self):
self.stime = timeit.default_timer()
def end(self, print_time=True):
self.etime = timeit.default_timer()
if print_time:
self.display()
def __init__(self):
self.start()
+235
View File
@@ -0,0 +1,235 @@
import cv2
import numpy as np
from .nHHD import *
# import src.utils as utils
class Tracker:
def __init__(self,
adaptive,
cuda,
lower_threshold=12.0,
upper_threshold=18.0,
loop_closure_interval=1,
dis_preset='medium'):
# Check if OpenCV is built with CUDA support
if cuda and not cv2.cuda.getCudaEnabledDeviceCount():
# print("Warning: CUDA requested but OpenCV not built with CUDA support or no CUDA device found. Falling back to CPU.")
cuda = False
self.cuda = cuda
self.__loop_closure_interval = loop_closure_interval
self.__frame_count = 0
if not cuda:
# DIS method - optimize for speed
if dis_preset == 'ultrafast':
preset = cv2.DISOPTICAL_FLOW_PRESET_ULTRAFAST
elif dis_preset == 'fast':
preset = cv2.DISOPTICAL_FLOW_PRESET_FAST
else:
preset = cv2.DISOPTICAL_FLOW_PRESET_MEDIUM
self.__tracking_inst = cv2.DISOpticalFlow_create(preset)
# Aggressive optimization for CPU
# self.__tracking_inst.setFinestScale(1) # Skip finest pyramid level (2x faster)
# self.__tracking_inst.setPatchSize(8) # Smaller patches (default 8)
# self.__tracking_inst.setPatchStride(4) # Larger stride = fewer patches
# self.__tracking_inst.setGradientDescentIterations(12) # Reduce iterations (default 25)
# self.__tracking_inst.setVariationalRefinementIterations(3) # Reduce refinement (default 5)
else:
# CUDA flow method with stream support
self.__tracking_inst = cv2.cuda.FarnebackOpticalFlow_create()
# Create CUDA stream for async operations
self.__cuda_stream = cv2.cuda.Stream()
self.__fine_base_frame_queue = []
self.__fine_flow_queue = []
self.__prev_fine_flow = None
self.__adaptive = adaptive
self.__lower_threshold = lower_threshold # error lower than threshold will close the loop
self.__upper_threshold = upper_threshold # error greater than threshold will add new reference frame
def track(self, frame):
"""Main tracking function that dispatches to CUDA or CPU implementation"""
if self.cuda:
return self.track_cuda(frame)
else:
return self.track_cpu(frame)
def track_cuda(self, frame):
"""CUDA-accelerated optical flow tracking"""
frame_height, frame_width = frame.shape[:2]
frame = cv2.cuda_GpuMat(frame)
if self.__adaptive:
if not self.__fine_base_frame_queue:
self.__fine_base_frame_queue.append(frame)
# Pre-compute meshgrid for remap operations
x = np.linspace(0, frame_width - 1, frame_width, dtype='float32')
y = np.linspace(0, frame_height - 1, frame_height, dtype='float32')
self.map = np.array(np.meshgrid(x, y)).transpose((1, 2, 0))
cuMat = cv2.cuda_GpuMat()
cuMat.upload(self.map)
self.map = cuMat
self.__fine_flow_queue.append(
cv2.cuda_GpuMat(frame.size(), cv2.CV_32FC2, (0.0, 0.0)))
self.__prev_fine_flow = cv2.cuda_GpuMat(
frame.size(), cv2.CV_32FC2, (0.0, 0.0))
return self.__fine_flow_queue[-1]
else:
self.__frame_count += 1
# Only check loop closure periodically (expensive operation)
if self.__frame_count % self.__loop_closure_interval == 0:
self.__checkLoopClosure(frame)
# Use CUDA stream for async optical flow computation
self.__prev_fine_flow = self.__tracking_inst.calc(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow, self.__cuda_stream)
final_flow = cv2.cuda.add(self.__fine_flow_queue[-1],
self.__prev_fine_flow,
stream=self.__cuda_stream)
# Use fine flow for error estimation
error = self.__getForwardBackwardError(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow)
if error > self.__upper_threshold:
self.__fine_base_frame_queue.append(frame)
self.__fine_flow_queue.append(final_flow)
self.__prev_fine_flow = None
return final_flow
else:
if not self.__fine_base_frame_queue:
self.__fine_base_frame_queue.append(frame)
self.__prev_fine_flow = self.__tracking_inst.calc(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow, self.__cuda_stream)
return self.__prev_fine_flow
def track_cpu(self, frame):
"""CPU-based optical flow tracking"""
frame_height, frame_width = frame.shape[:2]
if self.__adaptive:
if not self.__fine_base_frame_queue:
self.__fine_base_frame_queue.append(frame)
# Pre-compute meshgrid for remap operations
x = np.linspace(0, frame_width - 1, frame_width, dtype='float32')
y = np.linspace(0, frame_height - 1, frame_height, dtype='float32')
self.map = np.array(np.meshgrid(x, y)).transpose((1, 2, 0))
# Pre-allocate with correct dtype
self.__fine_flow_queue.append(
np.zeros((frame_height, frame_width, 2), dtype=np.float32))
self.__prev_fine_flow = np.zeros(
(frame_height, frame_width, 2), dtype=np.float32)
return self.__fine_flow_queue[-1]
else:
self.__frame_count += 1
# Only check loop closure periodically (expensive operation)
if self.__frame_count % self.__loop_closure_interval == 0:
self.__checkLoopClosure(frame)
# CPU optical flow computation
self.__prev_fine_flow = self.__tracking_inst.calc(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow)
# Direct numpy addition is faster than cv2.add
final_flow = self.__fine_flow_queue[-1] + self.__prev_fine_flow
# Use fine flow for error estimation
error = self.__getForwardBackwardError(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow)
if error > self.__upper_threshold:
self.__fine_base_frame_queue.append(frame)
self.__fine_flow_queue.append(final_flow)
self.__prev_fine_flow = np.zeros(
(frame_height, frame_width, 2), dtype=np.float32)
return final_flow
else:
if not self.__fine_base_frame_queue:
self.__fine_base_frame_queue.append(frame)
self.__prev_fine_flow = self.__tracking_inst.calc(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow)
return self.__prev_fine_flow
def track_2dto3d(self, flow):
decompose_obj = nHHD(grid=(flow.shape[0], flow.shape[1]), spacings=(18, 18))
flag,output = decompose_obj.get_force(flow, verbose=0)
return output
def __getForwardBackwardError(self, I0, I1, flow):
if self.cuda:
flow_map = cv2.cuda.add(self.map, flow)
map_x, map_y = cv2.cuda.split(flow_map)
z = cv2.cuda.remap(I1,
map_x,
map_y,
interpolation=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE)
t = I0.clone()
t = t.convertTo(rtype=cv2.CV_32FC1, dst=t)
z = z.convertTo(rtype=cv2.CV_32FC1, dst=z)
rev = cv2.cuda.absSum(cv2.cuda.subtract(
t, z))[0] / t.size()[0] / t.size()[1]
else:
# Optimized CPU path - avoid redundant type conversions
flow_map = self.map + flow
z = cv2.remap(I1,
flow_map[:, :, 0],
flow_map[:, :, 1],
interpolation=cv2.INTER_CUBIC, # INTER_LINEAR faster than INTER_CUBIC
borderMode=cv2.BORDER_REPLICATE)
# Use in-place operations when possible
if I0.dtype != np.float32:
diff = z.astype('float32') - I0.astype('float32')
else:
diff = z - I0
rev = np.mean(np.abs(diff))
return rev
def __checkLoopClosure(
self, frame
): # update (prune) the frame queue with loop closure detection
for i in range(len(self.__fine_base_frame_queue) - 2, -1, -1):
f = self.__tracking_inst.calc(
self.__fine_base_frame_queue[i], frame, None)
error = self.__getForwardBackwardError(
self.__fine_base_frame_queue[i], frame, f)
if error < self.__lower_threshold:
print("loop closed at frame ", str(i))
self.__fine_base_frame_queue = self.__fine_base_frame_queue[:
i +
1]
self.__fine_flow_queue = self.__fine_flow_queue[:i + 1]
return True
return False
def reset(self):
self.__fine_base_frame_queue = []
self.__fine_flow_queue = []
self.__prev_fine_flow = None
self.__frame_count = 0
self.__vpi_prev_frame = None
self.__vpi_prev_pyramid = None
def getFrameQueueLength(self):
return len(self.__fine_base_frame_queue)