diff --git a/Atif/Gsoc-HealingStones/.gitignore b/Atif/Gsoc-HealingStones/.gitignore new file mode 100644 index 0000000..9df89ae --- /dev/null +++ b/Atif/Gsoc-HealingStones/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*$py.class + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/Atif/Gsoc-HealingStones/README_ONBOARDING.md b/Atif/Gsoc-HealingStones/README_ONBOARDING.md new file mode 100644 index 0000000..f1a5e1f --- /dev/null +++ b/Atif/Gsoc-HealingStones/README_ONBOARDING.md @@ -0,0 +1,50 @@ +# Mayan Stele CLI Usage Guide + +This guide covers the new structured output capabilities for automation and CI/CD integration. + +## Machine-Readable Output (--json / --report) + +Use the `--json` flag (alias `--report`) to get a machine-readable JSON object on `stdout`. When enabled, all other logging is suppressed. + +### Example: Validation +```bash +python main_pipeline.py check-data data/input --json +``` + +**JSON Schema:** +```json +{ + "command": "check-data", + "status": "PASS", + "input_metadata": { + "input_path": "data/input", + "ply_count": 5 + }, + "errors": [], + "warnings": [], + "timestamp": "2026-01-27T10:00:00Z" +} +``` + +### Example: Dry Run (Simulation) +```bash +python batch_processor.py dry-run data/input data/output --report +``` + +--- + +## Command Reference + +### main_pipeline.py +- `reconstruct `: Run full reconstruction. +- `check-data `: Check input availability. +- `validate `: Verify data integrity. +- `dry-run `: Simulate pipeline initialization. + +### batch_processor.py +- `validate `: Comprehensive PLY mesh validation. +- `preprocess `: Clean, center, and normalize meshes. +- `dry-run [output]`: Validate paths without processing. + +> [!NOTE] +> **Exit Codes**: Every command returns `0` on `PASS` and `1` on `FAIL`, regardless of whether `--json` is used. diff --git a/Atif/Gsoc-HealingStones/batch_processor.py b/Atif/Gsoc-HealingStones/batch_processor.py index c0d5625..35473c8 100644 --- a/Atif/Gsoc-HealingStones/batch_processor.py +++ b/Atif/Gsoc-HealingStones/batch_processor.py @@ -6,8 +6,25 @@ from sklearn.cluster import DBSCAN import json import argparse +import sys from typing import List, Dict, Tuple, Optional import cv2 +import cli_utils +from contextlib import contextmanager + +@contextmanager +def suppress_stdout(enable=True): + """Context manager to suppress stdout prints.""" + if not enable: + yield + return + with open(os.devnull, 'w') as fnull: + old_stdout = sys.stdout + sys.stdout = fnull + try: + yield + finally: + sys.stdout = old_stdout class PLYValidator: """ @@ -502,94 +519,155 @@ def main(): parser = argparse.ArgumentParser(description="PLY Preprocessing and Validation Tools") subparsers = parser.add_subparsers(dest='command', help='Available commands') + # helper + def add_common_args(sub_parser): + sub_parser.add_argument('--json', action='store_true', help='Output results as JSON') + sub_parser.add_argument('--report', action='store_true', help='Alias for --json') + # Validation command validate_parser = subparsers.add_parser('validate', help='Validate PLY files') + add_common_args(validate_parser) validate_parser.add_argument('input', help='PLY file or directory to validate') validate_parser.add_argument('--output', '-o', help='Output validation report file') - validate_parser.add_argument('--json', action='store_true', help='Output results as JSON') + # check-data command (alias for validate) + check_parser = subparsers.add_parser('check-data', help='Alias for validate') + add_common_args(check_parser) + check_parser.add_argument('input', help='PLY file or directory to check') + check_parser.add_argument('--output', '-o', help='Output validation report file') + # Preprocessing command preprocess_parser = subparsers.add_parser('preprocess', help='Preprocess PLY files') + add_common_args(preprocess_parser) preprocess_parser.add_argument('input', help='Input PLY file or directory') preprocess_parser.add_argument('output', help='Output PLY file or directory') preprocess_parser.add_argument('--no-clean', action='store_true', help='Skip mesh cleaning') preprocess_parser.add_argument('--no-normalize', action='store_true', help='Skip scale normalization') preprocess_parser.add_argument('--no-center', action='store_true', help='Skip centering') preprocess_parser.add_argument('--no-enhance-colors', action='store_true', help='Skip color enhancement') + + # dry-run command + dry_parser = subparsers.add_parser('dry-run', help='Dry run of preprocessing/validation') + add_common_args(dry_parser) + dry_parser.add_argument('input', help='Input path') + dry_parser.add_argument('output', nargs='?', help='Output path (optional)') args = parser.parse_args() - if args.command == 'validate': - validator = PLYValidator() - - input_path = Path(args.input) - - if input_path.is_file(): - # Validate single file - result = validator.validate_ply_file(args.input) - - if args.json: - print(json.dumps(result, indent=2)) - else: - print(f"File: {result['filepath']}") - print(f"Valid: {result['valid']}") - if result['warnings']: - print("Warnings:") - for warning in result['warnings']: - print(f" - {warning}") - if result['errors']: - print("Errors:") - for error in result['errors']: - print(f" - {error}") - - elif input_path.is_dir(): - # Validate directory - results = validator.validate_directory(args.input) - - if args.json: - if args.output: - with open(args.output, 'w') as f: - json.dump(results, f, indent=2) + if not args.command: + parser.print_help() + sys.exit(1) + + json_mode = cli_utils.is_json_mode(args) + + with suppress_stdout(enable=json_mode): + try: + input_path = Path(args.input) if hasattr(args, 'input') else None + output_path = Path(args.output) if (hasattr(args, 'output') and args.output) else None + + if args.command in ['validate', 'check-data']: + validator = PLYValidator() + + if input_path.is_file(): + # Validate single file + result = validator.validate_ply_file(args.input) + status = "PASS" if result['valid'] else "FAIL" + if json_mode: + metadata = { + "input_path": result['filepath'], + "statistics": result['statistics'] + } + cli_utils.format_json_output(args.command, status, metadata, result['errors'], result['warnings']) + else: + print(f"File: {result['filepath']}") + print(f"Valid: {result['valid']}") + if result['warnings']: + print("Warnings:") + for warning in result['warnings']: + print(f" - {warning}") + if result['errors']: + print("Errors:") + for error in result['errors']: + print(f" - {error}") + sys.exit(0 if status == "PASS" else 1) + + elif input_path.is_dir(): + # Validate directory + results = validator.validate_directory(args.input) + status = "PASS" if results['files_with_errors'] == 0 else "FAIL" + if json_mode: + metadata = { + "input_path": results['directory'], + "total_files": results['total_files'], + "valid_files": results['valid_files'] + } + cli_utils.format_json_output(args.command, status, metadata) + else: + validator.generate_validation_report(results, args.output) + sys.exit(0 if status == "PASS" else 1) else: - print(json.dumps(results, indent=2)) + msg = f"Error: {args.input} is not a valid file or directory" + if json_mode: + cli_utils.format_json_output(args.command, "FAIL", errors=[msg]) + else: + print(msg) + sys.exit(1) + + elif args.command == 'preprocess': + preprocessor = PLYPreprocessor() + options = { + 'clean': not args.no_clean, + 'normalize_scale': not args.no_normalize, + 'center': not args.no_center, + 'enhance_colors': not args.no_enhance_colors + } + + success = False + if input_path.is_file(): + success = preprocessor.preprocess_file(str(input_path), str(output_path), **options) + elif input_path.is_dir(): + results = preprocessor.preprocess_directory(str(input_path), str(output_path), **options) + success = results['failed'] == 0 + + status = "PASS" if success else "FAIL" + if json_mode: + metadata = { + "input_path": str(input_path), + "output_path": str(output_path) + } + cli_utils.format_json_output(args.command, status, metadata) + + sys.exit(0 if success else 1) + + elif args.command == 'dry-run': + # Dry run: check if input exists and is valid PLY + status = "PASS" if input_path.exists() else "FAIL" + errors = [] if status == "PASS" else [f"Path {args.input} does not exist"] + metadata = { + "input_path": str(input_path), + "output_path": str(output_path) if output_path else None + } + + if input_path.is_dir(): + ply_files = list(input_path.glob("*.ply")) + metadata["ply_count"] = len(ply_files) + if not ply_files: + status = "FAIL" + errors.append("No PLY files found") + + if json_mode: + cli_utils.format_json_output(args.command, status, metadata, errors) + else: + print(f"Dry run: {status}") + for e in errors: print(f"Error: {e}") + sys.exit(0 if status == "PASS" else 1) + + except Exception as e: + if json_mode: + cli_utils.format_json_output(args.command, "FAIL", errors=[str(e)]) else: - validator.generate_validation_report(results, args.output) - - else: - print(f"Error: {args.input} is not a valid file or directory") - - elif args.command == 'preprocess': - preprocessor = PLYPreprocessor() - - input_path = Path(args.input) - output_path = Path(args.output) - - # Set preprocessing options - options = { - 'clean': not args.no_clean, - 'normalize_scale': not args.no_normalize, - 'center': not args.no_center, - 'enhance_colors': not args.no_enhance_colors - } - - if input_path.is_file(): - # Preprocess single file - success = preprocessor.preprocess_file(str(input_path), str(output_path), **options) - if not success: - exit(1) - - elif input_path.is_dir(): - # Preprocess directory - results = preprocessor.preprocess_directory(str(input_path), str(output_path), **options) - if results['failed'] > 0: - exit(1) - - else: - print(f"Error: {args.input} is not a valid file or directory") - exit(1) - - else: - parser.print_help() + print(f"Error: {e}") + sys.exit(1) if __name__ == "__main__": main() \ No newline at end of file diff --git a/Atif/Gsoc-HealingStones/cli_utils.py b/Atif/Gsoc-HealingStones/cli_utils.py new file mode 100644 index 0000000..29012dc --- /dev/null +++ b/Atif/Gsoc-HealingStones/cli_utils.py @@ -0,0 +1,32 @@ +import json +from datetime import datetime +import sys + +def format_json_output(command, status, metadata=None, errors=None, warnings=None): + """ + Format and print a deterministic JSON object for CLI output. + + Args: + command (str): The CLI command executed (e.g., check-data, validate, dry-run). + status (str): Outcome status (e.g., PASS, FAIL). + metadata (dict, optional): Input metadata like paths, counts, etc. + errors (list, optional): List of error messages. + warnings (list, optional): List of warning messages. + """ + output = { + "command": command, + "status": status, + "input_metadata": metadata or {}, + "errors": errors or [], + "warnings": warnings or [], + "timestamp": datetime.utcnow().isoformat() + "Z" + } + + # Use sys.__stdout__ directly to ensure output even if sys.stdout is redirected + json.dump(output, sys.__stdout__, indent=2, sort_keys=True) + sys.__stdout__.write('\n') + sys.__stdout__.flush() + +def is_json_mode(args): + """Check if JSON mode is enabled based on argparse arguments.""" + return getattr(args, 'json', False) or getattr(args, 'report', False) diff --git a/Atif/Gsoc-HealingStones/main_pipeline.py b/Atif/Gsoc-HealingStones/main_pipeline.py index 151706d..476a28f 100644 --- a/Atif/Gsoc-HealingStones/main_pipeline.py +++ b/Atif/Gsoc-HealingStones/main_pipeline.py @@ -19,6 +19,8 @@ from pathlib import Path import numpy as np import time +import os +from contextlib import contextmanager # Import our custom modules from ply_loader import PLYColorExtractor @@ -26,6 +28,21 @@ from surface_matcher import SurfaceMatcher from fragment_aligner import FragmentAligner from reconstruction_visualizer import ReconstructionVisualizer +import cli_utils + +@contextmanager +def suppress_stdout(enable=True): + """Context manager to suppress stdout prints.""" + if not enable: + yield + return + with open(os.devnull, 'w') as fnull: + old_stdout = sys.stdout + sys.stdout = fnull + try: + yield + finally: + sys.stdout = old_stdout class ReconstructionPipeline: """ @@ -904,75 +921,143 @@ def main(): description="Mayan Stele Fragment Reconstruction Pipeline" ) - parser.add_argument( - "input_dir", - help="Directory containing PLY files with colored break surfaces" - ) - - parser.add_argument( - "output_dir", - help="Directory to save reconstruction results" - ) - - parser.add_argument( - "--min-similarity", - type=float, - default=0.6, - help="Minimum similarity threshold for surface matching (default: 0.6)" - ) - - parser.add_argument( - "--color-tolerance", - type=float, - default=0.3, - help="Color matching tolerance (default: 0.3)" - ) + # Use subparsers for multiple commands + subparsers = parser.add_subparsers(dest='command', help='Available commands') - parser.add_argument( - "--visualize-steps", - action="store_true", - help="Show step-by-step visualizations" - ) - - parser.add_argument( - "--no-reports", - action="store_true", - help="Skip generating detailed reports" - ) - - parser.add_argument( - "--config", - help="JSON configuration file" - ) - - parser.add_argument( - "--contact-distance", - type=float, - default=0.001, - help="Target contact distance between surfaces in meters (default: 0.001 = 1mm)" - ) + # Helper to add common arguments to subcommands + def add_common_args(sub_parser): + sub_parser.add_argument("--json", action="store_true", help="Output results as JSON") + sub_parser.add_argument("--report", action="store_true", help="Alias for --json") + sub_parser.add_argument("--min-similarity", type=float, default=0.6, help="Minimum similarity threshold for surface matching (default: 0.6)") + sub_parser.add_argument("--color-tolerance", type=float, default=0.3, help="Color matching tolerance (default: 0.3)") + sub_parser.add_argument("--config", help="JSON configuration file") + # reconstruct command + reconstruct_parser = subparsers.add_parser('reconstruct', help='Run full reconstruction pipeline') + add_common_args(reconstruct_parser) + reconstruct_parser.add_argument("input_dir", help="Directory containing PLY files") + reconstruct_parser.add_argument("output_dir", help="Directory to save reconstruction results") + reconstruct_parser.add_argument("--visualize-steps", action="store_true", help="Show step-by-step visualizations") + reconstruct_parser.add_argument("--no-reports", action="store_true", help="Skip generating detailed reports") + reconstruct_parser.add_argument("--contact-distance", type=float, default=0.001, help="Target contact distance (meters)") + + # check-data command + check_parser = subparsers.add_parser('check-data', help='Check input data without full processing') + add_common_args(check_parser) + check_parser.add_argument("input_dir", help="Directory or file to check") + + # validate command (alias/similar to check-data) + validate_parser = subparsers.add_parser('validate', help='Validate input data') + add_common_args(validate_parser) + validate_parser.add_argument("input_dir", help="Directory or file to validate") + + # dry-run command + dry_parser = subparsers.add_parser('dry-run', help='Execute a dry run of the pipeline') + add_common_args(dry_parser) + dry_parser.add_argument("input_dir", help="Directory containing PLY files") + dry_parser.add_argument("output_dir", help="Directory to save reconstruction results") + + # For backward compatibility, if the first argument is a directory and not a command + if len(sys.argv) > 1 and sys.argv[1] not in subparsers.choices and not sys.argv[1].startswith("-"): + sys.argv.insert(1, "reconstruct") + args = parser.parse_args() + if not args.command: + parser.print_help() + sys.exit(1) + + json_mode = cli_utils.is_json_mode(args) + # Load configuration config = { - 'min_similarity': args.min_similarity, - 'color_tolerance': args.color_tolerance, - 'visualize_steps': args.visualize_steps, - 'output_reports': not args.no_reports, - 'assembly_contact_distance': args.contact_distance + 'min_similarity': getattr(args, 'min_similarity', 0.6), + 'color_tolerance': getattr(args, 'color_tolerance', 0.3), + 'visualize_steps': getattr(args, 'visualize_steps', False), + 'output_reports': not getattr(args, 'no_reports', False), + 'assembly_contact_distance': getattr(args, 'contact_distance', 0.001) } if args.config: - with open(args.config, 'r') as f: - file_config = json.load(f) - config.update(file_config) - - # Run pipeline - pipeline = ReconstructionPipeline(config) - success = pipeline.run_full_pipeline(args.input_dir, args.output_dir) - - sys.exit(0 if success else 1) + try: + with open(args.config, 'r') as f: + config.update(json.load(f)) + except Exception as e: + if json_mode: + cli_utils.format_json_output(args.command, "FAIL", errors=[f"Failed to load config: {str(e)}"]) + sys.exit(1) + else: + print(f"Error loading config: {e}") + sys.exit(1) + + with suppress_stdout(enable=json_mode): + try: + input_path = Path(args.input_dir) if hasattr(args, 'input_dir') else None + output_path = Path(args.output_dir) if hasattr(args, 'output_dir') else None + + if args.command in ['check-data', 'validate']: + errors = [] + warnings = [] + status = "PASS" + metadata = {"input_path": str(input_path)} + + if not input_path or not input_path.exists(): + status = "FAIL" + errors.append(f"Input path {input_path} does not exist") + else: + ply_files = list(input_path.glob("*.ply")) if input_path.is_dir() else ([input_path] if input_path.suffix == '.ply' else []) + metadata["ply_count"] = len(ply_files) + if not ply_files: + status = "FAIL" + errors.append("No PLY files found") + + if json_mode: + cli_utils.format_json_output(args.command, status, metadata, errors, warnings) + else: + print(f"Command: {args.command}, Status: {status}") + for e in errors: print(f"Error: {e}") + sys.exit(0 if status == "PASS" else 1) + + elif args.command == 'dry-run': + pipeline = ReconstructionPipeline(config) + ply_files = list(Path(args.input_dir).glob("*.ply")) + status = "PASS" if ply_files else "FAIL" + metadata = { + "input_path": str(args.input_dir), + "output_path": str(args.output_dir), + "ply_count": len(ply_files) + } + errors = [] if ply_files else ["No PLY files found"] + + if json_mode: + cli_utils.format_json_output(args.command, status, metadata, errors) + else: + print(f"Dry run complete. Status: {status}") + sys.exit(0 if status == "PASS" else 1) + + elif args.command == 'reconstruct': + pipeline = ReconstructionPipeline(config) + success = pipeline.run_full_pipeline(args.input_dir, args.output_dir) + status = "PASS" if success else "FAIL" + + if json_mode: + metadata = { + "input_path": str(args.input_dir), + "output_path": str(args.output_dir), + "fragments_aligned": len(pipeline.transformations) if hasattr(pipeline, 'transformations') else 0 + } + cli_utils.format_json_output(args.command, status, metadata) + + sys.exit(0 if success else 1) + + except Exception as e: + if json_mode: + cli_utils.format_json_output(args.command, "FAIL", errors=[str(e)]) + else: + print(f"Error: {e}") + import traceback + if not json_mode: traceback.print_exc() + sys.exit(1) if __name__ == "__main__": main() \ No newline at end of file