Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import yaml | |
| import os | |
| from datetime import datetime | |
| from typing import Dict, Any, Union, List | |
| from config.path_config import ensure_directories_exist, get_config_download_path, WORKING_DIR | |
| class ConfigEditor: | |
| """Simplified Configuration Editor for Petri Net Algorithm Parameters""" | |
| def __init__(self): | |
| self.config_data = {} | |
| self.error_message = "" | |
| # Parameter definitions with validation rules | |
| self.param_groups = { | |
| "Image Processing": { | |
| "min_dimension_threshold": { | |
| "type": "number", "default": 800, "min": 100, "max": 2000, | |
| "label": "Min Dimension Threshold", | |
| "help": "Minimum image dimension before upscaling (pixels)" | |
| }, | |
| "upscale_factor": { | |
| "type": "number", "default": 2.0, "min": 1.0, "max": 5.0, "step": 0.1, | |
| "label": "Upscale Factor", | |
| "help": "Factor to upscale small images" | |
| } | |
| }, | |
| "Text Detection": { | |
| "bin_thresh": { | |
| "type": "slider", "default": 0.3, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Binary Threshold", | |
| "help": "Binarization threshold for text detection" | |
| }, | |
| "box_thresh": { | |
| "type": "slider", "default": 0.1, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Box Threshold", | |
| "help": "Confidence threshold for text box detection" | |
| } | |
| }, | |
| "Shape Detection": { | |
| "fill_circle_enclosing_threshold": { | |
| "type": "slider", "default": 0.8, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Circle Fill Threshold", | |
| "help": "Threshold for filling detected circles" | |
| }, | |
| "fill_rect_enclosing_threshold": { | |
| "type": "slider", "default": 0.95, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Rectangle Fill Threshold", | |
| "help": "Threshold for filling detected rectangles" | |
| }, | |
| "erosion_kernel_size": { | |
| "type": "text", "default": "[3, 3]", | |
| "label": "Erosion Kernel Size", | |
| "help": "Kernel size for erosion [width, height]" | |
| }, | |
| "min_stable_length": { | |
| "type": "number", "default": 3, "min": 1, "max": 10, | |
| "label": "Min Stable Length", | |
| "help": "Minimum iterations for shape stability" | |
| }, | |
| "max_erosion_iterations": { | |
| "type": "number", "default": 30, "min": 5, "max": 100, | |
| "label": "Max Erosion Iterations", | |
| "help": "Maximum erosion iterations" | |
| }, | |
| "classify_circle_overlap_threshold": { | |
| "type": "slider", "default": 0.8, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Circle Classification Threshold", | |
| "help": "Overlap threshold for circle classification" | |
| }, | |
| "classify_rect_overlap_threshold": { | |
| "type": "slider", "default": 0.85, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Rectangle Classification Threshold", | |
| "help": "Overlap threshold for rectangle classification" | |
| }, | |
| "remove_nodes_dilation_kernel_size": { | |
| "type": "text", "default": "[3, 3]", | |
| "label": "Dilation Kernel Size", | |
| "help": "Kernel size for node removal [width, height]" | |
| }, | |
| "remove_nodes_dilation_iterations": { | |
| "type": "number", "default": 3, "min": 1, "max": 10, | |
| "label": "Dilation Iterations", | |
| "help": "Number of dilation iterations for node removal" | |
| } | |
| }, | |
| "Connection Processing - Hough Transform": { | |
| "hough_rho": { | |
| "type": "number", "default": 1, "min": 1, "max": 5, | |
| "label": "Rho (distance resolution)", | |
| "help": "Distance resolution in pixels" | |
| }, | |
| "hough_theta_degrees": { | |
| "type": "number", "default": 1.0, "min": 0.1, "max": 5.0, "step": 0.1, | |
| "label": "Theta (angle resolution)", | |
| "help": "Angle resolution in degrees" | |
| }, | |
| "hough_threshold": { | |
| "type": "number", "default": 15, "min": 5, "max": 50, | |
| "label": "Threshold", | |
| "help": "Accumulator threshold for line detection" | |
| }, | |
| "hough_min_line_length": { | |
| "type": "number", "default": 10, "min": 1, "max": 50, | |
| "label": "Min Line Length", | |
| "help": "Minimum length of line segments" | |
| }, | |
| "hough_max_line_gap": { | |
| "type": "number", "default": 25, "min": 1, "max": 100, | |
| "label": "Max Line Gap", | |
| "help": "Maximum gap to merge line segments" | |
| } | |
| }, | |
| "Connection Processing - Advanced": { | |
| "hough_bundler_min_distance": { | |
| "type": "number", "default": 10.0, "min": 1.0, "max": 50.0, "step": 0.5, | |
| "label": "Bundler Min Distance", | |
| "help": "Minimum distance for line bundling" | |
| }, | |
| "hough_bundler_min_angle": { | |
| "type": "number", "default": 3.0, "min": 1.0, "max": 15.0, "step": 0.5, | |
| "label": "Bundler Min Angle", | |
| "help": "Minimum angle for line bundling" | |
| }, | |
| "arrowhead_confidence_threshold_percent": { | |
| "type": "number", "default": 10.0, "min": 1.0, "max": 100.0, "step": 1.0, | |
| "label": "Arrowhead Confidence (%)", | |
| "help": "Confidence threshold for arrowhead detection" | |
| }, | |
| "proximity_thres_place": { | |
| "type": "number", "default": 1.5, "min": 0.5, "max": 5.0, "step": 0.1, | |
| "label": "Place Proximity Threshold", | |
| "help": "Proximity threshold for places (×radius)" | |
| }, | |
| "proximity_thres_trans_height": { | |
| "type": "number", "default": 1.4, "min": 0.5, "max": 5.0, "step": 0.1, | |
| "label": "Transition Height Threshold", | |
| "help": "Height proximity threshold for transitions" | |
| }, | |
| "proximity_thres_trans_width": { | |
| "type": "number", "default": 3.0, "min": 0.5, "max": 10.0, "step": 0.1, | |
| "label": "Transition Width Threshold", | |
| "help": "Width proximity threshold for transitions" | |
| }, | |
| "arrowhead_proximity_threshold": { | |
| "type": "number", "default": 40, "min": 10, "max": 100, | |
| "label": "Arrowhead Proximity", | |
| "help": "Distance threshold for arrowhead linking" | |
| }, | |
| "text_linking_threshold": { | |
| "type": "number", "default": 25.0, "min": 5.0, "max": 100.0, "step": 1.0, | |
| "label": "Text Linking Threshold", | |
| "help": "Distance threshold for text linking" | |
| } | |
| }, | |
| "Path Finding": { | |
| "proximity_threshold": { | |
| "type": "number", "default": 30.0, "min": 5.0, "max": 100.0, "step": 1.0, | |
| "label": "Proximity Threshold", | |
| "help": "Distance threshold for path segment connection" | |
| }, | |
| "dot_product_weight": { | |
| "type": "slider", "default": 0.6, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Dot Product Weight", | |
| "help": "Weight for direction similarity in scoring" | |
| }, | |
| "distance_to_line_weight": { | |
| "type": "slider", "default": 0.2, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Distance to Line Weight", | |
| "help": "Weight for distance to line in scoring" | |
| }, | |
| "endpoint_distance_weight": { | |
| "type": "slider", "default": 0.2, "min": 0.0, "max": 1.0, "step": 0.05, | |
| "label": "Endpoint Distance Weight", | |
| "help": "Weight for endpoint distance in scoring" | |
| } | |
| } | |
| } | |
| self.components = {} | |
| def create_component(self, param_name: str, param_def: Dict) -> Any: | |
| """Create appropriate Gradio component for parameter""" | |
| if param_def["type"] == "slider": | |
| return gr.Slider( | |
| minimum=param_def["min"], | |
| maximum=param_def["max"], | |
| step=param_def["step"], | |
| value=param_def["default"], | |
| label=param_def["label"], | |
| info=param_def["help"] | |
| ) | |
| elif param_def["type"] == "number": | |
| return gr.Number( | |
| minimum=param_def.get("min"), | |
| maximum=param_def.get("max"), | |
| step=param_def.get("step", 1), | |
| value=param_def["default"], | |
| label=param_def["label"], | |
| info=param_def["help"] | |
| ) | |
| elif param_def["type"] == "text": | |
| return gr.Textbox( | |
| value=param_def["default"], | |
| label=param_def["label"], | |
| info=param_def["help"] | |
| ) | |
| def validate_parameter(self, param_name: str, value: Any) -> tuple[Any, str]: | |
| """Validate parameter value, return (validated_value, error_message)""" | |
| # Find parameter definition | |
| param_def = None | |
| for group_params in self.param_groups.values(): | |
| if param_name in group_params: | |
| param_def = group_params[param_name] | |
| break | |
| if not param_def: | |
| return value, f"Unknown parameter: {param_name}" | |
| try: | |
| # Handle list parameters (kernel sizes) | |
| if param_def["type"] == "text" and "kernel" in param_name.lower(): | |
| if isinstance(value, str): | |
| # Parse string representation of list | |
| parsed = eval(value.strip()) | |
| if not isinstance(parsed, list) or len(parsed) != 2: | |
| return None, f"{param_def['label']}: Must be a list of 2 numbers [w, h]" | |
| if not all(isinstance(x, (int, float)) and x > 0 for x in parsed): | |
| return None, f"{param_def['label']}: Values must be positive numbers" | |
| return parsed, "" | |
| return value, "" | |
| # Validate numeric ranges | |
| if param_def["type"] in ["number", "slider"]: | |
| if not isinstance(value, (int, float)): | |
| return None, f"{param_def['label']}: Must be a number" | |
| min_val = param_def.get("min") | |
| max_val = param_def.get("max") | |
| if min_val is not None and value < min_val: | |
| return None, f"{param_def['label']}: Must be ≥ {min_val}" | |
| if max_val is not None and value > max_val: | |
| return None, f"{param_def['label']}: Must be ≤ {max_val}" | |
| return value, "" | |
| except Exception as e: | |
| return None, f"{param_def['label']}: {str(e)}" | |
| def load_config_file(self, file_obj): | |
| """Load configuration from uploaded file""" | |
| if file_obj is None: | |
| return self._get_current_values() + ("No file selected",) | |
| try: | |
| with open(file_obj.name, 'r') as f: | |
| self.config_data = yaml.safe_load(f) | |
| # Update component values | |
| values = [] | |
| for group_name, group_params in self.param_groups.items(): | |
| for param_name, param_def in group_params.items(): | |
| value = self._get_config_value(param_name, param_def["default"]) | |
| values.append(value) | |
| return tuple(values) + (f"Loaded: {os.path.basename(file_obj.name)}",) | |
| except Exception as e: | |
| return self._get_current_values() + (f"Error loading file: {str(e)}",) | |
| def _get_config_value(self, param_name: str, default: Any) -> Any: | |
| """Get parameter value from loaded config with proper path handling""" | |
| # Handle path finding parameters specially | |
| if param_name in ["proximity_threshold", "dot_product_weight", "distance_to_line_weight", "endpoint_distance_weight"]: | |
| return self.config_data.get("connection_processing", {}).get("path_finding", {}).get(param_name, default) | |
| # Handle other connection_processing parameters | |
| elif param_name.startswith(("hough_", "arrowhead_", "proximity_", "text_")): | |
| return self.config_data.get("connection_processing", {}).get(param_name, default) | |
| # Handle shape detection parameters | |
| elif param_name in ["fill_circle_enclosing_threshold", "fill_rect_enclosing_threshold", "erosion_kernel_size", | |
| "min_stable_length", "max_erosion_iterations", "classify_circle_overlap_threshold", | |
| "classify_rect_overlap_threshold", "remove_nodes_dilation_kernel_size", "remove_nodes_dilation_iterations"]: | |
| value = self.config_data.get("shape_detection", {}).get(param_name, default) | |
| # Format lists as strings for text inputs | |
| if isinstance(value, list): | |
| return str(value) | |
| return value | |
| # Handle text detection parameters | |
| elif param_name in ["bin_thresh", "box_thresh"]: | |
| return self.config_data.get("text_detection", {}).get(param_name, default) | |
| # Handle image processing parameters | |
| elif param_name in ["min_dimension_threshold", "upscale_factor"]: | |
| return self.config_data.get("image_processing", {}).get(param_name, default) | |
| return default | |
| def _get_current_values(self) -> tuple: | |
| """Get current values from all components""" | |
| values = [] | |
| for group_params in self.param_groups.values(): | |
| for param_name, param_def in group_params.items(): | |
| values.append(param_def["default"]) | |
| return tuple(values) | |
| def save_config(self, *param_values) -> tuple: | |
| """Save configuration to temporary file for download""" | |
| try: | |
| # Validate all parameters | |
| config = { | |
| "image_processing": {}, | |
| "text_detection": {}, | |
| "shape_detection": {}, | |
| "connection_processing": {"path_finding": {}} | |
| } | |
| errors = [] | |
| value_index = 0 | |
| for group_name, group_params in self.param_groups.items(): | |
| for param_name, param_def in group_params.items(): | |
| value = param_values[value_index] | |
| validated_value, error = self.validate_parameter(param_name, value) | |
| if error: | |
| errors.append(error) | |
| else: | |
| # Place value in correct config section | |
| if param_name in ["proximity_threshold", "dot_product_weight", "distance_to_line_weight", "endpoint_distance_weight"]: | |
| config["connection_processing"]["path_finding"][param_name] = validated_value | |
| elif param_name.startswith(("hough_", "arrowhead_", "proximity_", "text_")): | |
| config["connection_processing"][param_name] = validated_value | |
| elif param_name in ["fill_circle_enclosing_threshold", "fill_rect_enclosing_threshold", "erosion_kernel_size", | |
| "min_stable_length", "max_erosion_iterations", "classify_circle_overlap_threshold", | |
| "classify_rect_overlap_threshold", "remove_nodes_dilation_kernel_size", "remove_nodes_dilation_iterations"]: | |
| config["shape_detection"][param_name] = validated_value | |
| elif param_name in ["bin_thresh", "box_thresh"]: | |
| config["text_detection"][param_name] = validated_value | |
| elif param_name in ["min_dimension_threshold", "upscale_factor"]: | |
| config["image_processing"][param_name] = validated_value | |
| value_index += 1 | |
| if errors: | |
| return "Validation errors:\n" + "\n".join(errors), gr.update(visible=False) | |
| # Generate filename for display purposes | |
| # Use centralized path management | |
| ensure_directories_exist() | |
| temp_filepath = get_config_download_path() | |
| # Save file to temporary location in working directory (overwrites previous version) | |
| with open(temp_filepath, 'w') as f: | |
| yaml.dump(config, f, default_flow_style=False, sort_keys=False) | |
| return f"Configuration prepared for download: {temp_filepath.split('/')[-1]}", gr.update(value=temp_filepath, visible=True) | |
| except Exception as e: | |
| return f"Error saving configuration: {str(e)}", gr.update(visible=False) | |
| def create_interface(self) -> gr.TabItem: | |
| """Create the Configuration Editor interface""" | |
| with gr.TabItem("Simple Config Editor") as tab: | |
| gr.Markdown("## Algorithm Configuration Editor") | |
| gr.Markdown("Upload an existing config file or modify parameters below, then save.") | |
| # File upload | |
| with gr.Row(): | |
| file_upload = gr.File( | |
| label="Upload Configuration File (.yaml/.yml)", | |
| file_types=[".yaml", ".yml"] | |
| ) | |
| status = gr.Textbox(label="Status", interactive=False) | |
| # Create parameter sections | |
| components_list = [] | |
| with gr.Accordion("Configuration Parameters", open=True): | |
| for group_name, group_params in self.param_groups.items(): | |
| with gr.Accordion(group_name, open=False): | |
| for param_name, param_def in group_params.items(): | |
| component = self.create_component(param_name, param_def) | |
| self.components[param_name] = component | |
| components_list.append(component) | |
| # Save section | |
| with gr.Row(): | |
| save_btn = gr.Button("Save Configuration", variant="primary") | |
| download_file = gr.File(label="Download", visible=False) | |
| # Event handlers | |
| file_upload.upload( | |
| fn=self.load_config_file, | |
| inputs=[file_upload], | |
| outputs=components_list + [status] | |
| ) | |
| save_btn.click( | |
| fn=self.save_config, | |
| inputs=components_list, | |
| outputs=[status, download_file] | |
| ) | |
| return tab |