diff --git a/config/compound_eye.png b/config/compound_eye.png index 2e4f334..ef3b17b 100644 Binary files a/config/compound_eye.png and b/config/compound_eye.png differ diff --git a/config/prototype.json b/config/prototype.json new file mode 100644 index 0000000..187a8ea --- /dev/null +++ b/config/prototype.json @@ -0,0 +1,29 @@ +{ + "name": "prototype", + "version": "1.0", + "camera": { + "resolution_raw": [1920, 1080], + "fps": 60, + "units": 15, + "units_used": 9, + "resolution_stitch": [400, 400], + "index": [1,2,3,6,7,8,11,12,13], + "pos_default": [[260, 56], [577, 62], [897, 69], [1214, 75], [1530, 81], [256, 391], [570, 397], [889, 403], [1208, 409], [1523, 416], [248, 724], [563, 730], [882, 738], [1201, 743], [1516, 751]] + }, + "stitcher": { + "min_area_frac": 0.018, + "max_area_frac": 0.035, + "max_aspect_ratio": 20 + }, + "contact": { + "min_contour_area": 3000, + "flow_threshold":1, + "div_threshold":0.02, + "expansion_threshold": 0.01, + "downsample_factor": 1, + "kernel_size": [5,5], + "denoise_sigma": 15 + } +} + + diff --git a/example_contact.py b/example_contact.py index 8ec27ac..21f14c6 100644 --- a/example_contact.py +++ b/example_contact.py @@ -7,10 +7,10 @@ import time def main(): # 方式1: 从视频文件读取 - # sensor = orisys.Sensor("./s1.mp4", isstitch=True, cuda=True, verbose=True) + sensor = orisys.Sensor("./s1.mp4", isstitch=True, cuda=True, verbose=True,config_name="./config/prototype.json") # 方式2: 从摄像头读取(取消注释使用) - sensor = orisys.Sensor(0, isstitch=True, cuda=False, verbose=True) + # sensor = orisys.Sensor(0, isstitch=True, cuda=False, verbose=True,config_name="./config/prototype.json") print("\n按 'q' 退出程序,按 'r' 重置光流追踪器") diff --git a/src/orisys/configs/__init__.py b/src/orisys/configs/__init__.py index dbe4f9a..130edfc 100644 --- a/src/orisys/configs/__init__.py +++ b/src/orisys/configs/__init__.py @@ -3,30 +3,108 @@ from pathlib import Path CONFIG_DIR = Path(__file__).parent -def load_config(config_name: str): +def load_config(config_name_or_path): """ Load sensor configuration from JSON file. + Accepts either: + 1. A file path (absolute or relative) to a JSON config file + 2. A config name (without .json extension) to load from package configs directory + Args: - config_name: Name of the config file (without .json extension) + config_name_or_path: Either: + - A file path string (e.g., "/path/to/config.json" or "./config.json") + - A config name string (e.g., "prototype") to load from package configs Returns: Dictionary containing the configuration Raises: FileNotFoundError: If the config file doesn't exist + ValueError: If the file path is not a JSON file """ - config_path = CONFIG_DIR / f"{config_name}.json" + # Convert to Path object for easier handling + config_path = Path(config_name_or_path) - if not config_path.exists(): + # Check if it's an existing file (absolute or relative path) + if config_path.exists() and config_path.is_file(): + # User provided a file path + if config_path.suffix.lower() != '.json': + raise ValueError(f"Config file must be a JSON file: {config_path}") + + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + + # If it's an absolute path but doesn't exist, raise error immediately + if config_path.is_absolute(): + raise FileNotFoundError(f"Config file not found at absolute path: {config_path}") + + # Otherwise, treat it as a config name and look in package directory + package_config_path = CONFIG_DIR / f"{config_name_or_path}.json" + + if not package_config_path.exists(): # List available configs for better error message available = [f.stem for f in CONFIG_DIR.glob("*.json")] available_str = ", ".join(available) if available else "none" raise FileNotFoundError( - f"Config file not found: {config_path}\n" - f"Available configs: {available_str}" + f"Config file not found: {config_name_or_path}\n" + f"Tried as file path: {config_path} (not found)\n" + f"Tried as package config: {package_config_path} (not found)\n" + f"Available package configs: {available_str}" ) - with open(config_path, 'r') as f: + with open(package_config_path, 'r', encoding='utf-8') as f: return json.load(f) + +def _format_json_value(value, indent_level=0, indent_str=" "): + """ + Recursively format JSON value with arrays/lists on single line. + + Args: + value: The value to format (dict, list, or primitive) + indent_level: Current indentation level + indent_str: String used for indentation (default: 4 spaces) + + Returns: + Formatted string representation + """ + if isinstance(value, dict): + if not value: + return "{}" + items = [] + for k, v in value.items(): + key_str = json.dumps(k, ensure_ascii=False) + value_str = _format_json_value(v, indent_level + 1, indent_str) + items.append(f"{indent_str * (indent_level + 1)}{key_str}: {value_str}") + return "{\n" + ",\n".join(items) + "\n" + indent_str * indent_level + "}" + elif isinstance(value, list): + if not value: + return "[]" + # Format arrays/lists on a single line + items = [json.dumps(item, ensure_ascii=False) for item in value] + return "[" + ", ".join(items) + "]" + else: + # Primitive types (string, number, bool, None) + return json.dumps(value, ensure_ascii=False) + +def export_config(config, path="./config/exported_config.json"): + """ + Export configuration to JSON file with readable formatting. + Arrays/lists are kept on a single line, while nested objects are indented. + + Args: + config: Configuration dictionary to export + path: Output file path (default: "./config/exported_config.json") + + Returns: + The exported config dictionary + """ + Path(path).parent.mkdir(parents=True, exist_ok=True) + + with open(path, 'w', encoding='utf-8') as f: + formatted = _format_json_value(config) + f.write(formatted) + f.write('\n') # Add trailing newline + + return config \ No newline at end of file diff --git a/src/orisys/sensor.py b/src/orisys/sensor.py index 5c7f992..460ea66 100644 --- a/src/orisys/sensor.py +++ b/src/orisys/sensor.py @@ -417,9 +417,12 @@ class Sensor: # Public API - Configuration & Calibration # ========================================================================= - def get_config(self): + def export_config(self, path="./config/exported_config.json"): """Return current configuration dict.""" - return self.config + configs.export_config(self.config, path) + if self.verbose: + print(f"Config exported to {path}") + return True def stitch_cal(self): """ @@ -462,8 +465,6 @@ class Sensor: self.config_stitcher = self.config["stitcher"] self.config_contact = self.config["contact"] - - if self.verbose: print(f"Config: {self.config_name}") print(f" Resolution: {self.config_resolution_raw} → {self.config_resolution_stitch}")