diff --git a/build_multi_version.py b/build_multi_version.py deleted file mode 100644 index 40a1a9b..0000000 --- a/build_multi_version.py +++ /dev/null @@ -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() diff --git a/build_pyd.py b/build_pyd.py deleted file mode 100644 index 4a015cd..0000000 --- a/build_pyd.py +++ /dev/null @@ -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() - diff --git a/orisys_logo.png b/res/orisys_logo.png similarity index 100% rename from orisys_logo.png rename to res/orisys_logo.png diff --git a/orisys_logo_notext.png b/res/orisys_logo_notext.png similarity index 100% rename from orisys_logo_notext.png rename to res/orisys_logo_notext.png diff --git a/stereo_1.jpg b/stereo_1.jpg deleted file mode 100644 index e794021..0000000 Binary files a/stereo_1.jpg and /dev/null differ diff --git a/stereo_1.mp4 b/stereo_1.mp4 deleted file mode 100644 index e3611d0..0000000 Binary files a/stereo_1.mp4 and /dev/null differ diff --git a/streo.py b/streo.py deleted file mode 100644 index b7aab1f..0000000 --- a/streo.py +++ /dev/null @@ -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() -