initial commit
This commit is contained in:
@@ -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
|
||||||
@@ -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()
|
||||||
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 |
@@ -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.
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -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,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
@@ -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.
@@ -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.
@@ -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
|
||||||
@@ -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.
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
numpy
|
||||||
|
opencv-python
|
||||||
|
scipy
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
orisys
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
@@ -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
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
@@ -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.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
|
|
||||||
Reference in New Issue
Block a user