This commit is contained in:
unknown
2026-01-08 14:26:45 +08:00
parent 56bb77e1fd
commit 73b9ef10fc
7 changed files with 0 additions and 1585 deletions
-520
View File
@@ -1,520 +0,0 @@
"""
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
# Package version
VERSION = "0.2.0"
# 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)
# Use --obf-code 1 to enable code obfuscation (default is 1, but explicit is better)
# Use --exclude to exclude __init__.py files that don't need obfuscation (optional)
if IS_WINDOWS:
cmd = 'pyarmor gen --output dist_obfuscated/src --recursive --obf-code 1 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',
'--obf-code', '1',
'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)
# Verify obfuscation: check that obfuscated files exist and original .py files are obfuscated
obfuscated_dir = Path('dist_obfuscated/src/orisys')
if obfuscated_dir.exists():
py_files = list(obfuscated_dir.rglob('*.py'))
print(f"Found {len(py_files)} obfuscated .py files")
# Check a sample file to verify it's obfuscated (should contain PyArmor markers)
if py_files:
sample_file = py_files[0]
try:
with open(sample_file, 'rb') as f:
content = f.read(100) # Read first 100 bytes
# PyArmor obfuscated files typically start with specific markers
if b'__pyarmor__' in content or b'pyarmor' in content.lower():
print(f"✓ Verified: {sample_file.name} is obfuscated")
else:
print(f"⚠ Warning: {sample_file.name} may not be properly obfuscated")
except Exception as e:
print(f"Could not verify obfuscation: {e}")
return True
def copy_additional_files():
"""Copy non-Python files that don't need obfuscation."""
print("\n=== Copying additional files ===")
# Ensure configs directory exists in obfuscated output
# IMPORTANT: Only copy JSON files, NOT .py files (those should be obfuscated by PyArmor)
configs_src = Path('src/orisys/configs')
configs_dst = Path('dist_obfuscated/src/orisys/configs')
if configs_src.exists():
print(f"Ensuring configs directory is copied (JSON files only)...")
if not configs_dst.exists():
configs_dst.mkdir(parents=True, exist_ok=True)
# Only copy JSON files, not any .py files
json_files_src = list(configs_src.glob('*.json'))
json_files_dst = list(configs_dst.glob('*.json'))
if len(json_files_dst) < len(json_files_src):
print(f"Copying JSON files from configs directory ({len(json_files_src)} files)")
for json_file in json_files_src:
shutil.copy(json_file, configs_dst / json_file.name)
else:
print(f"Configs directory already present with {len(json_files_dst)} JSON files")
# Copy config directory (if exists at root level)
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', '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}')
# Verify that no original .py files are in dist_obfuscated (except obfuscated ones)
print("\n=== Verifying obfuscated output ===")
obfuscated_py_files = list(Path('dist_obfuscated/src/orisys').rglob('*.py'))
print(f"Found {len(obfuscated_py_files)} .py files in obfuscated output")
# Check if any files look like original source (not obfuscated)
original_like_files = []
for py_file in obfuscated_py_files:
# Skip __init__.py files as they might be simple
if py_file.name == '__init__.py':
continue
try:
with open(py_file, 'rb') as f:
content = f.read(500) # Read first 500 bytes
# Check for common Python source patterns that shouldn't be in obfuscated code
if b'def ' in content and b'__pyarmor__' not in content:
# Might be original source
if b'import ' in content or b'from ' in content:
# Check if it has readable imports (obfuscated code has different structure)
text_content = content.decode('utf-8', errors='ignore')
if 'import ' in text_content and '__pyarmor__' not in text_content:
original_like_files.append(py_file)
except Exception as e:
print(f"Warning: Could not check {py_file}: {e}")
if original_like_files:
print(f"⚠️ Warning: Found {len(original_like_files)} files that may not be obfuscated:")
for f in original_like_files[:5]: # Show first 5
print(f" - {f}")
if len(original_like_files) > 5:
print(f" ... and {len(original_like_files) - 5} more")
else:
print("✓ All .py files appear to be obfuscated")
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 and configs
package_data = {{}}
for pkg in packages:
pkg_path = Path('src') / pkg.replace('.', '/')
if pkg_path.exists():
data_files = []
# Include all .pyd, .so, .dll files (PyArmor runtime)
for pattern in ['*.pyd', '*.so', '*.dll', '*.dylib']:
data_files.extend([str(f.relative_to(pkg_path)) for f in pkg_path.glob(pattern)])
# Include JSON files from configs directory (for orisys.configs package)
if pkg == 'orisys.configs':
# Include all JSON files in the configs package directory
for json_file in pkg_path.glob('*.json'):
rel_path = json_file.relative_to(pkg_path)
data_files.append(str(rel_path))
# For orisys package, include configs subdirectory JSON files
if pkg == 'orisys':
configs_dir = pkg_path / 'configs'
if configs_dir.exists() and configs_dir.is_dir():
for json_file in configs_dir.glob('*.json'):
rel_path = json_file.relative_to(pkg_path)
data_files.append(str(rel_path))
if data_files:
package_data[pkg] = data_files
setup(
name="orisys",
version="{VERSION}",
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,
# Exclude original source files - only include obfuscated files from dist_obfuscated
exclude_package_data={{'': ['*.pyc', '*.pyo', '__pycache__']}},
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 create_manifest_in():
"""Create MANIFEST.in to ensure JSON files in configs are included and exclude original source."""
print("\n=== Creating MANIFEST.in ===")
manifest_content = """# Include all JSON files in configs directory
include src/orisys/configs/*.json
recursive-include src/orisys/configs *.json
# Include PyArmor runtime files
recursive-include src *.pyd
recursive-include src *.so
recursive-include src *.dll
recursive-include src *.dylib
# Exclude original source files (only obfuscated .py files should be included)
# The obfuscated .py files from PyArmor will be included automatically
# But we want to make sure no original source files are accidentally included
prune src/orisys
recursive-include src/orisys *.py
"""
manifest_path = Path('dist_obfuscated/MANIFEST.in')
with open(manifest_path, 'w') as f:
f.write(manifest_content)
print("MANIFEST.in created successfully!")
print(manifest_content)
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-{VERSION}-{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 (including configs)
copy_additional_files()
# Step 3.5: Create MANIFEST.in to ensure JSON files are included
create_manifest_in()
# 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-{VERSION}-*.whl")
if __name__ == '__main__':
main()
-902
View File
@@ -1,902 +0,0 @@
"""
Build script to create wheel distributions with .pyd/.so files instead of .py files.
This script uses Cython to compile Python source files into extension modules (.pyd on Windows, .so on Linux),
making the source code completely hidden in the final distribution.
Requirements:
- Cython
- A C compiler (Visual Studio Build Tools on Windows, GCC on Linux)
"""
import os
import shutil
import subprocess
import sys
import platform
from pathlib import Path
import tempfile
# Package version
VERSION = "0.2.0"
# 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'
# Extension for compiled modules
EXT_EXTENSION = '.pyd' if IS_WINDOWS else '.so'
def clean_build_dirs():
"""Remove previous build artifacts."""
dirs_to_clean = ['build', 'dist', 'orisys.egg-info', 'dist_pyd', 'build_pyd']
for dir_name in dirs_to_clean:
if os.path.exists(dir_name):
print(f"Cleaning {dir_name}...")
try:
# Try to remove with error handling
shutil.rmtree(dir_name, ignore_errors=False, onerror=_handle_remove_error)
except (PermissionError, OSError) as e:
print(f" ⚠️ Warning: Could not fully remove {dir_name}: {e}")
print(f" Some files may be locked. Continuing anyway...")
# Try again with ignore_errors=True
try:
shutil.rmtree(dir_name, ignore_errors=True)
except Exception:
pass
# Ensure dist directory exists with platform subdirectories
os.makedirs(f'dist/{PLATFORM_NAME}', exist_ok=True)
def _handle_remove_error(func, path, exc_info):
"""Handle errors during directory removal."""
# If it's a permission error, try to change permissions and retry
if func is os.rmdir:
try:
# Try to change permissions and remove
os.chmod(path, 0o777)
os.rmdir(path)
except Exception:
# If that fails, just skip it
pass
elif func is os.remove:
try:
os.chmod(path, 0o777)
os.remove(path)
except Exception:
pass
def check_cython():
"""Check if Cython is installed."""
try:
import Cython
print(f"✓ Cython {Cython.__version__} is available")
return True
except ImportError:
print("❌ Cython is not installed!")
print(" Install it with: pip install cython")
return False
def check_c_compiler():
"""Check if a C compiler is available."""
if IS_WINDOWS:
# On Windows, check for cl.exe (Microsoft Visual C++)
# Note: cl.exe might not be in PATH even if installed
# We'll do a basic check but the actual compilation will provide better errors
cl_path = shutil.which('cl.exe')
if cl_path:
print("✓ C compiler (cl.exe) found in PATH")
return True
else:
print("⚠️ C compiler (cl.exe) not found in PATH")
print(" (This is OK if Visual Studio is installed but not in PATH)")
print(" The build will attempt to use the compiler anyway...")
return True # Return True to allow build attempt - it will fail with better error if missing
else:
# On Linux/Mac, check for gcc
try:
result = subprocess.run(
['gcc', '--version'], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
print(f"✓ C compiler (gcc) is available")
return True
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
print("❌ C compiler (gcc) not found!")
print("\n Install with:")
if IS_LINUX:
print(" sudo apt-get install build-essential # Ubuntu/Debian")
print(" sudo yum install gcc gcc-c++ # CentOS/RHEL")
else:
print(" Install Xcode Command Line Tools:")
print(" xcode-select --install")
return False
def get_python_executable(version):
"""Get the Python executable for a specific version based on platform."""
current_version = f"{sys.version_info.major}.{sys.version_info.minor}"
if current_version == version:
return [sys.executable]
if IS_WINDOWS:
username = os.environ.get("USERNAME", "")
possible_paths = [
f'py -{version}',
f'python{version}',
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',
f'C:\\Python{version.replace(".", "")}\\python.exe',
f'C:\\Program Files\\Python{version.replace(".", "")}\\python.exe',
]
else:
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:
if path.startswith('~'):
path = os.path.expanduser(path)
if IS_WINDOWS and path.startswith('py -'):
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:
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 create_cython_setup_files():
"""Create setup.py files for Cython compilation."""
print("\n=== Creating Cython setup files ===")
# Find all Python files to compile (excluding __init__.py and configs)
src_dir = Path('src/orisys')
py_files = []
for py_file in src_dir.rglob('*.py'):
# Skip __init__.py files (they need to stay as .py for package structure)
if py_file.name == '__init__.py':
continue
# Skip getDensity.py (excluded from compilation)
if py_file.name == 'getDensity.py':
continue
# Skip configs directory (JSON files only)
if 'configs' in py_file.parts and py_file.name != '__init__.py':
continue
# Get relative path from src/orisys
rel_path = py_file.relative_to(src_dir)
py_files.append(rel_path)
print(f"Found {len(py_files)} Python files to compile")
# Create dist_pyd directory structure
dist_dir = Path('dist_pyd/src/orisys')
dist_dir.mkdir(parents=True, exist_ok=True)
# Copy ALL Python source files to dist_pyd (needed for Cython compilation)
# Cython needs the source files to compile them
print("Copying Python source files for compilation...")
for py_file in src_dir.rglob('*.py'):
rel_path = py_file.relative_to(src_dir)
# dst_file should be in dist_pyd/src/orisys/ (not dist_pyd/src/)
dst_file = dist_dir / rel_path
dst_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(py_file, dst_file)
if py_file.name == '__init__.py':
print(f" Copied: {rel_path} (will remain as .py)")
else:
print(f" Copied: {rel_path} (will be compiled to .pyd/.so)")
# Copy configs directory (JSON files)
configs_src = src_dir / 'configs'
configs_dst = dist_dir / 'configs'
if configs_src.exists():
shutil.copytree(configs_src, configs_dst, dirs_exist_ok=True)
print(f" Copied: configs/ directory")
# Create setup.py for Cython compilation
# Build the py_files list as a string
py_files_str = '[\n'
for py_file in py_files:
# Convert path to module name (e.g., "sensor.py" -> "orisys.sensor")
module_path = str(py_file).replace('\\', '/').replace('.py', '').replace('/', '.')
file_path = f"src/orisys/{py_file}"
py_files_str += f' ("orisys.{module_path}", "{file_path}"),\n'
py_files_str += ']'
# Build setup.py content using string concatenation to avoid f-string escaping issues
# Note: We need to handle Windows multiprocessing properly
# The if __name__ == '__main__': guard is required for Windows multiprocessing support
setup_content = '''from setuptools import setup, Extension
from Cython.Build import cythonize
from pathlib import Path
import numpy
import sys
import platform
import multiprocessing
import os
# Package version
VERSION = "''' + VERSION + '''"
# Get include directories
numpy_include = numpy.get_include()
# On Windows, find Python library for explicit linking if needed
if platform.system() == 'Windows':
import os
python_libs_dir = os.path.join(sys.base_prefix, 'libs')
python_version = f"{sys.version_info.major}{sys.version_info.minor}"
python_lib_name = f"python{python_version}"
# Check if library file exists
python_lib_path = os.path.join(python_libs_dir, f"{python_lib_name}.lib")
has_python_lib = os.path.exists(python_lib_path) if python_libs_dir and os.path.exists(python_libs_dir) else False
else:
python_libs_dir = None
python_lib_name = None
python_lib_path = None
has_python_lib = False
# List of Python files to compile (excluding __init__.py)
py_files = ''' + py_files_str + '''
# Create Extension objects
extensions = []
for module_name, file_path in py_files:
# Setup library directories and libraries for Windows
library_dirs = []
libraries = []
extra_link_args = []
if platform.system() == 'Windows' and has_python_lib:
# Explicitly link Python library on Windows
library_dirs = [python_libs_dir]
# Use libraries parameter to link Python library
# This is the standard way setuptools expects
libraries = [python_lib_name]
ext = Extension(
module_name,
[file_path],
include_dirs=[numpy_include],
library_dirs=library_dirs,
libraries=libraries,
# Add any additional compiler flags here
extra_compile_args=[],
extra_link_args=extra_link_args,
)
extensions.append(ext)
# Determine number of threads for parallel compilation
# On Windows, disable parallel compilation to avoid multiprocessing spawn issues
# On Linux/Mac, use parallel compilation for faster builds
if platform.system() == 'Windows':
nthreads = 1 # Single-threaded on Windows to avoid multiprocessing issues
else:
nthreads = 4 # Parallel compilation on Linux/Mac
# Windows multiprocessing requires this guard
if __name__ == '__main__':
# Required for Windows multiprocessing support
multiprocessing.freeze_support()
# On Windows, ensure 64-bit build if Python is 64-bit
if platform.system() == 'Windows':
import sysconfig
# Check if Python is 64-bit
if sys.maxsize > 2**32:
# Force 64-bit build by setting distutils environment
import distutils.util
# This should force 64-bit build
try:
# Try to set the platform to win-amd64
import distutils._msvccompiler
# The compiler should automatically detect 64-bit Python
pass
except:
pass
setup(
name="orisys",
version=VERSION,
description="Orisys - Optical Tactile Sensor SDK (Compiled)",
author="Your Name",
ext_modules=cythonize(
extensions,
compiler_directives={
'language_level': "3",
'embedsignature': True,
# Optimize for size and speed
'optimize.use_switch': True,
'optimize.unpack_method_calls': True,
},
build_dir="build_pyd",
# Create .pyd/.so files instead of .py
nthreads=nthreads, # Parallel compilation (disabled on Windows)
),
zip_safe=False,
include_package_data=True,
package_data={
"orisys": ["configs/*.json"],
},
)
'''
setup_file = Path('dist_pyd/setup.py')
with open(setup_file, 'w') as f:
f.write(setup_content)
print(f"Created setup.py with {len(py_files)} modules to compile")
return py_files
def compile_with_cython(python_version):
"""Compile Python files to .pyd/.so using Cython."""
print(f"\n{'='*60}")
print(f"Compiling with Cython 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())
# Check if Cython is installed in this Python
result = subprocess.run(
python_cmd + ['-c', 'import Cython; print(f"Cython {Cython.__version__}")'],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"❌ Cython not installed for Python {python_version}")
print(f" Install with: {' '.join(python_cmd)} -m pip install cython")
return False
print(result.stdout.strip())
# Check if numpy is installed (required for compilation)
result = subprocess.run(
python_cmd + ['-c', 'import numpy; print(f"NumPy {numpy.__version__}")'],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"❌ NumPy not installed for Python {python_version}")
print(f" Install with: {' '.join(python_cmd)} -m pip install numpy")
return False
print(result.stdout.strip())
os.chdir('dist_pyd')
# On Windows, ensure we use the correct architecture (64-bit)
# Check Python architecture first
if IS_WINDOWS:
arch_check = subprocess.run(
python_cmd + ['-c', 'import sys; print("64bit" if sys.maxsize > 2**32 else "32bit")'],
capture_output=True, text=True
)
if arch_check.returncode == 0:
python_arch = arch_check.stdout.strip()
print(f"Python architecture: {python_arch}")
if python_arch == "64bit":
# Force 64-bit build on Windows by setting environment variable
# This tells distutils to use the 64-bit toolchain
os.environ['DISTUTILS_USE_SDK'] = '1'
# Also try to find and use vcvars64.bat if available
# But setuptools should handle this automatically
print(" Using 64-bit build configuration")
# Build extensions
# Don't use --inplace to avoid directory issues
# Build to build/lib directory, then copy to src/orisys
cmd = python_cmd + ['setup.py', 'build_ext']
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
os.chdir('..')
if result.returncode != 0:
print(f"❌ Error compiling with Cython")
# Check for common compiler errors
error_output = result.stderr + "\n" + result.stdout
if "cl.exe" in error_output and "failed" in error_output:
print("\n ⚠️ C compiler (cl.exe) error detected!")
print(" This usually means:")
print(" 1. Visual Studio Build Tools are not installed")
print(" 2. The compiler is not in your PATH")
print(" 3. The Visual Studio environment is not set up")
print("\n Solutions:")
print(" - Install Visual Studio Build Tools with C++ support")
print(" - Or open 'Developer Command Prompt for VS' and run from there")
print(" - Or run: vcvars64.bat before building")
print(f"\n Full error output:\n{result.stderr}")
if result.stdout and result.stderr != result.stdout:
print(f"\n stdout:\n{result.stdout}")
return False
print(f"✅ Compilation successful for Python {python_version}!")
# Copy compiled files from build directory to src/orisys
build_lib_dir = Path('build')
target_dir = Path('src/orisys')
# Find the build directory (it may be build/lib.win-amd64-cpython-310 or similar)
compiled_files = []
if build_lib_dir.exists():
# Find the actual build output directory
for lib_dir in build_lib_dir.glob('lib.*'):
orisys_build_dir = lib_dir / 'orisys'
if orisys_build_dir.exists():
# Copy all .pyd/.so files to src/orisys
print(f" Copying compiled files from {orisys_build_dir} to {target_dir}...")
target_dir.mkdir(parents=True, exist_ok=True)
for compiled_file in orisys_build_dir.glob(f'*{EXT_EXTENSION}'):
# Copy with original filename (e.g., sensor.cp310-win_amd64.pyd)
target_file = target_dir / compiled_file.name
shutil.copy(compiled_file, target_file)
compiled_files.append(target_file)
# Also create a symlink/copy with base name (e.g., sensor.pyd)
# Python can import from either name, but base name is more reliable
base_name = compiled_file.stem.split('.')[0] # Get 'sensor' from 'sensor.cp310-win_amd64'
base_file = target_dir / f"{base_name}{EXT_EXTENSION}"
if not base_file.exists():
shutil.copy(compiled_file, base_file)
compiled_files.append(base_file)
print(f" Copied: {compiled_file.name} -> {target_file} and {base_file.name}")
else:
print(f" Copied: {compiled_file.name} -> {target_file}")
break
# Verify files are in the target location
if not compiled_files:
compiled_files = list(target_dir.rglob(f'*{EXT_EXTENSION}'))
if compiled_files:
print(f" Found {len(compiled_files)} existing {EXT_EXTENSION} files in target directory")
print(f" Created {len(compiled_files)} {EXT_EXTENSION} files")
# List some example files
if compiled_files:
print(f" Examples: {', '.join([f.name for f in compiled_files[:3]])}")
if len(compiled_files) > 3:
print(f" ... and {len(compiled_files) - 3} more")
else:
print(" ⚠️ Warning: No compiled files found in expected locations")
print(" This may indicate a problem with the build process")
# Remove original .py files (except __init__.py) after successful compilation
# This ensures only compiled .pyd/.so files remain
print("\n=== Removing original .py source files (keeping only compiled extensions) ===")
removed_count = 0
for py_file in Path('dist_pyd/src/orisys').rglob('*.py'):
# Keep __init__.py files
if py_file.name == '__init__.py':
continue
# Check if corresponding .pyd/.so file exists
compiled_file = py_file.with_suffix(EXT_EXTENSION)
if compiled_file.exists():
try:
py_file.unlink()
removed_count += 1
print(f" Removed: {py_file.relative_to('dist_pyd')}")
except Exception as e:
print(f" Warning: Could not remove {py_file.name}: {e}")
if removed_count > 0:
print(f"Removed {removed_count} original .py files (compiled versions exist)")
else:
print("No .py files removed (compiled files may not have been created)")
# Return True if compilation was successful (returncode == 0)
# Even if file verification had issues, compilation itself succeeded
return True
def create_package_setup(python_version=None):
"""Create setup.py for packaging the compiled .pyd/.so files."""
print(f"\n=== Creating package setup.py (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'
# Before packaging, ensure .pyd files are in src/orisys
# They might still be in build directory, so copy them if needed
print("=== Ensuring .pyd files are in src/orisys before packaging ===")
orisys_dir = Path('dist_pyd/src/orisys')
build_dir = Path('dist_pyd/build')
# Check if files are in build directory and copy them
copied_count = 0
if build_dir.exists():
for lib_dir in build_dir.glob('lib.*'):
orisys_build_dir = lib_dir / 'orisys'
if orisys_build_dir.exists():
print(f" Found .pyd files in {orisys_build_dir}, copying to src/orisys...")
orisys_dir.mkdir(parents=True, exist_ok=True)
for compiled_file in orisys_build_dir.glob(f'*{EXT_EXTENSION}'):
target_file = orisys_dir / compiled_file.name
# Always copy to ensure we have the latest version
shutil.copy(compiled_file, target_file)
copied_count += 1
print(f" Copied: {compiled_file.name} -> {target_file}")
if copied_count > 0:
print(f" Successfully copied {copied_count} .pyd files to src/orisys")
else:
print(" ⚠️ No .pyd files found in build directory to copy")
# Before packaging, ensure all .py files (except __init__.py) are removed
print("=== Final cleanup: Removing any remaining .py source files ===")
removed_count = 0
if orisys_dir.exists():
for py_file in orisys_dir.rglob('*.py'):
# Keep __init__.py files
if py_file.name == '__init__.py':
continue
# Check if corresponding .pyd/.so file exists
compiled_file = py_file.with_suffix(EXT_EXTENSION)
# Check for any .pyd file with this base name
base_name = py_file.stem
has_compiled = False
if compiled_file.exists():
has_compiled = True
else:
# Check for versioned .pyd files (e.g., sensor.cp310-win_amd64.pyd)
for pyd_file in orisys_dir.glob(f'{base_name}.*{EXT_EXTENSION}'):
has_compiled = True
break
if has_compiled:
try:
py_file.unlink()
removed_count += 1
print(f" Removed: {py_file.relative_to('dist_pyd')}")
except Exception as e:
print(f" Warning: Could not remove {py_file.name}: {e}")
else:
# If no compiled version exists, this is a problem
print(f" ⚠️ Warning: {py_file.name} has no compiled version, removing anyway")
try:
py_file.unlink()
removed_count += 1
except Exception as e:
print(f" Warning: Could not remove {py_file.name}: {e}")
if removed_count > 0:
print(f"Removed {removed_count} .py files before packaging")
# Find all compiled .pyd/.so files in src/orisys
compiled_files = list(orisys_dir.rglob(f'*{EXT_EXTENSION}')) if orisys_dir.exists() else []
print(f"Found {len(compiled_files)} compiled {EXT_EXTENSION} files in src/orisys")
if len(compiled_files) == 0:
print(" ⚠️ WARNING: No .pyd files found in src/orisys!")
print(" This will cause the wheel to be missing compiled modules.")
# Create MANIFEST.in to ensure .pyd files are included
manifest_content = f"""# Include all compiled extensions
include src/orisys/*{EXT_EXTENSION}
recursive-include src/orisys *{EXT_EXTENSION}
# Include JSON config files
recursive-include src/orisys/configs *.json
# Exclude all .py files except __init__.py
global-exclude *.py
include src/orisys/__init__.py
recursive-include src/orisys/configs __init__.py
"""
manifest_file = Path('dist_pyd/MANIFEST.in')
with open(manifest_file, 'w') as f:
f.write(manifest_content)
print("Created MANIFEST.in to ensure .pyd files are included")
setup_content = f'''from setuptools import setup, find_packages
from setuptools.command.build_py import build_py as _build_py
from pathlib import Path
import os
import shutil
import glob
# 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
packages = find_packages(where="src")
# Collect package data (JSON files and .pyd/.so files)
# IMPORTANT: .pyd/.so files with version tags (e.g., sensor.cp310-win_amd64.pyd)
# are NOT automatically included by setuptools, so we must explicitly list them
package_data = {{}}
for pkg in packages:
pkg_path = Path('src') / pkg.replace('.', '/')
if pkg_path.exists():
data_files = []
# Explicitly include compiled extensions (.pyd/.so) using glob pattern
# This ensures all .pyd files are included regardless of version tags
for pattern in ['*{EXT_EXTENSION}']:
for ext_file in pkg_path.glob(pattern):
rel_path = ext_file.relative_to(pkg_path)
data_files.append(str(rel_path))
# Also check subdirectories
for ext_file in pkg_path.rglob(pattern):
rel_path = ext_file.relative_to(pkg_path)
if str(rel_path) not in data_files:
data_files.append(str(rel_path))
# Include JSON files from configs directory
if pkg == 'orisys.configs':
for json_file in pkg_path.glob('*.json'):
rel_path = json_file.relative_to(pkg_path)
data_files.append(str(rel_path))
# For orisys package, include configs subdirectory JSON files
if pkg == 'orisys':
configs_dir = pkg_path / 'configs'
if configs_dir.exists() and configs_dir.is_dir():
for json_file in configs_dir.glob('*.json'):
rel_path = json_file.relative_to(pkg_path)
data_files.append(str(rel_path))
if data_files:
package_data[pkg] = data_files
print(f"Package {{pkg}} package_data: {{len(data_files)}} files")
# Custom build_py to ensure .pyd files are copied
class build_py(_build_py):
def run(self):
"""Override to ensure .pyd files are included"""
super().run()
# After building, verify .pyd files are in the build directory
for pkg in packages:
pkg_path = Path('src') / pkg.replace('.', '/')
build_pkg_path = Path(self.build_lib) / pkg.replace('.', '/')
if pkg_path.exists() and build_pkg_path.exists():
# Copy any .pyd files that might have been missed
for ext_file in pkg_path.glob('*{EXT_EXTENSION}'):
build_ext_file = build_pkg_path / ext_file.name
if not build_ext_file.exists():
shutil.copy(ext_file, build_ext_file)
print(f" Copied {{ext_file.name}} to build directory")
setup(
name="orisys",
version="{VERSION}",
description="Orisys - Optical Tactile Sensor SDK (Compiled)",
author="Your Name",
package_dir={{"": "src"}},
packages=packages,
package_data=package_data,
include_package_data=True,
install_requires=requirements,
python_requires="{version_constraint}",
zip_safe=False,
cmdclass={{"build_py": build_py}},
options={{
'bdist_wheel': {{
'python_tag': '{py_tag}',
}}
}},
)
'''
with open('dist_pyd/setup.py', 'w') as f:
f.write(setup_content)
print("Package setup.py created successfully!")
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, skipping...")
return False
# Get platform tag
plat_tag = get_platform_tag()
py_tag = f'py{python_version.replace(".", "")}'
os.chdir('dist_pyd')
# Build the wheel
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:\n{result.stderr}")
if result.stdout:
print(f"stdout:\n{result.stdout}")
return False
print(f"✅ Wheel for Python {python_version} built successfully!")
print(f" Expected: orisys-{VERSION}-{py_tag}-none-{plat_tag}.whl")
return True
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 copy_additional_files():
"""Copy non-Python files."""
print("\n=== Copying additional files ===")
# Copy requirements.txt
if os.path.exists('requirements.txt'):
shutil.copy('requirements.txt', 'dist_pyd/requirements.txt')
print("Copied requirements.txt")
# Copy README and LICENSE if they exist
for file in ['README.md', 'LICENSE']:
if os.path.exists(file):
shutil.copy(file, f'dist_pyd/{file}')
print(f"Copied {file}")
def main():
"""Main build process."""
import argparse
parser = argparse.ArgumentParser(description='Build orisys package with Cython (.pyd/.so files)')
parser.add_argument('--versions', nargs='+', default=PYTHON_VERSIONS,
help='Python versions to build for')
args = parser.parse_args()
current_py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
print("=" * 60)
print("Building Orisys Package with Cython (.pyd/.so files)")
print(f"Platform: {PLATFORM_NAME} ({platform.system()})")
print(f"Current Python: {current_py_version} ({sys.executable})")
print(f"Python Versions: {', '.join(args.versions)}")
print("=" * 60)
# Check Cython
if not check_cython():
print("\n❌ Build failed: Cython is required!")
print(" Install with: pip install cython")
sys.exit(1)
# Check C compiler
if not check_c_compiler():
print("\n❌ Build failed: C compiler is required!")
print(" Please install the required build tools (see above)")
sys.exit(1)
# 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: Create Cython setup files
py_files = create_cython_setup_files()
if not py_files:
print("\n❌ No Python files found to compile!")
sys.exit(1)
# Step 3: Copy additional files
copy_additional_files()
# Step 4: Compile for each Python version
success_count = 0
for version in args.versions:
# Compile with Cython
if compile_with_cython(version):
# Create package setup.py
create_package_setup(version)
# Build wheel
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"Compiled source: dist_pyd/")
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-{VERSION}-*.whl")
print("\nNote: The wheel contains .pyd/.so files instead of .py files!")
if __name__ == '__main__':
main()

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

BIN
View File
Binary file not shown.
-163
View File
@@ -1,163 +0,0 @@
import numpy as np
import cv2
from matplotlib import pyplot as plt
import orisys
sensor = orisys.Sensor(vid_src='stereo2.mp4',config_name='./config/stereo.json',mirror=False)
print(sensor.boxes)
sensor.get_img()
tiles = sensor.stitcher.tile_list
print(tiles)
imgL = tiles[0]
imgR = tiles[1]
# imgL = cv.imread('tsukuba_l.png', cv.IMREAD_GRAYSCALE)
# imgR = cv.imread('tsukuba_r.png', cv.IMREAD_GRAYSCALE)
plt.subplot(1,2,1)
plt.imshow(imgL,'gray')
plt.subplot(1,2,2)
plt.imshow(imgR,'gray')
plt.show()
# Feature matching for stereo computation
def match_features(img1, img2, method='SIFT', max_matches=50):
"""
Detect and match features between two images.
Args:
img1: Left image (grayscale)
img2: Right image (grayscale)
method: 'SIFT' or 'ORB'
max_matches: Maximum number of matches to show
Returns:
img_matches: Image with matches drawn
matches: List of matches
kp1, kp2: Keypoints in img1 and img2
"""
# Convert to grayscale if needed
if len(img1.shape) == 3:
img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
else:
img1_gray = img1.copy()
if len(img2.shape) == 3:
img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
else:
img2_gray = img2.copy()
# Initialize detector
if method == 'SIFT':
detector = cv2.SIFT_create()
elif method == 'ORB':
detector = cv2.ORB_create()
else:
raise ValueError("method must be 'SIFT' or 'ORB'")
# Detect keypoints and descriptors
kp1, des1 = detector.detectAndCompute(img1_gray, None)
kp2, des2 = detector.detectAndCompute(img2_gray, None)
if des1 is None or des2 is None or len(des1) == 0 or len(des2) == 0:
print(f"Warning: No features detected with {method}")
return None, [], kp1, kp2
# Match features
if method == 'SIFT':
# Check if we have enough descriptors for knnMatch (need at least k=2)
min_descriptors = min(len(des1), len(des2))
if min_descriptors < 2:
print(f"Warning: Not enough descriptors ({min_descriptors}) for knnMatch, using regular matching")
# Fallback to BFMatcher
matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
matches = matcher.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)[:max_matches]
else:
# Use FLANN matcher for SIFT
try:
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
matcher = cv2.FlannBasedMatcher(index_params, search_params)
matches = matcher.knnMatch(des1, des2, k=2)
# Apply Lowe's ratio test
good_matches = []
for match_pair in matches:
if len(match_pair) == 2:
m, n = match_pair
if m.distance < 0.7 * n.distance:
good_matches.append(m)
matches = sorted(good_matches, key=lambda x: x.distance)[:max_matches]
except cv2.error:
# Fallback to BFMatcher if FLANN fails
print("Warning: FLANN matcher failed, using BFMatcher instead")
matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
matches = matcher.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)[:max_matches]
else: # ORB
# Use BF matcher for ORB
matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = matcher.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)[:max_matches]
# Draw matches
img_matches = cv2.drawMatches(
img1_gray, kp1, img2_gray, kp2, matches, None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
print(f"Found {len(matches)} good matches using {method}")
return img_matches, matches, kp1, kp2
# Perform feature matching
print("\n=== Feature Matching ===")
img_matches, matches, kp1, kp2 = match_features(imgL, imgR, method='SIFT', max_matches=50)
if img_matches is not None:
plt.figure(figsize=(15, 6))
plt.imshow(img_matches)
plt.title(f'Feature Matches (SIFT) - {len(matches)} matches')
plt.axis('off')
plt.tight_layout()
plt.show()
# Also create a side-by-side visualization with lines connecting matches
h1, w1 = imgL.shape[:2]
h2, w2 = imgR.shape[:2]
h_max = max(h1, h2)
combined = np.zeros((h_max, w1 + w2), dtype=np.uint8)
combined[:h1, :w1] = imgL
combined[:h2, w1:w1+w2] = imgR
combined_color = combined
# Draw lines connecting matched features
for match in matches[:30]: # Show top 30 matches
pt1 = tuple(map(int, kp1[match.queryIdx].pt))
pt2 = tuple(map(int, kp2[match.trainIdx].pt))
pt2 = (pt2[0] + w1, pt2[1])
cv2.line(combined_color, pt1, pt2, (0, 255, 0), 1)
cv2.circle(combined_color, pt1, 3, (0, 0, 255), -1)
cv2.circle(combined_color, pt2, 3, (255, 0, 0), -1)
plt.figure(figsize=(15, 6))
plt.imshow(cv2.cvtColor(combined_color, cv2.COLOR_BGR2RGB))
plt.title(f'Stereo Feature Matching - {len(matches[:30])} matches shown')
plt.axis('off')
plt.tight_layout()
plt.show()
# Stereo disparity computation
print("\n=== Stereo Disparity Computation ===")
stereo = cv2.StereoBM.create(numDisparities=32, blockSize=15)
disparity = stereo.compute(imgL, imgR)
plt.figure(figsize=(12, 5))
plt.imshow(disparity, cmap='jet')
plt.colorbar(label='Disparity')
plt.title('Stereo Disparity Map')
plt.axis('off')
plt.tight_layout()
plt.show()