Adding a New Interface
This guide provides step-by-step instructions for creating a new user interface for Granny. Interface modules provide different ways for users to interact with Granny’s analysis capabilities.
Overview
An interface module in Granny:
Inherits from the
GrannyUIabstract base classIntegrates with Python’s
argparsefor argument handlingDynamically discovers and exposes all available analyses
Automatically generates UI elements based on analysis parameters
Manages the execution of analyses with user-provided values
Existing interfaces include:
GrannyCLI - Command-line interface for batch processing
GrannyPyQt - Graphical desktop interface (PyQt5)
Mobile - Mobile interface (placeholder for future development)
Use Cases for New Interfaces
You might want to create a new interface for:
Web-based interface (Flask/Django)
REST API for programmatic access
Jupyter notebook integration
Desktop GUI with different framework (Tkinter, wxPython)
Mobile applications (iOS/Android)
Voice-controlled interface
Batch processing scripts with configuration files
The Interface Architecture
Interfaces in Granny follow a specific workflow:
Argument Setup Phase:
Interface is instantiated with an
ArgumentParserInterface adds its own specific arguments via
addProgramArgs()Interface discovers available analyses
Interface adds analysis-specific arguments to the parser
Execution Phase:
The
run()method is calledInterface parses command-line arguments (or gets user input)
Interface instantiates the selected analysis
Interface sets analysis parameters from user input
Interface calls
analysis.performAnalysis()Interface handles the results
Step-by-Step Guide
Step 1: Create Your Interface File
Create a new Python file in Granny/Interfaces/UI/ (or appropriate subdirectory):
touch Granny/Interfaces/UI/GrannyWeb.py
Step 2: Import Required Modules
Start your file with necessary imports:
"""
Web-based interface for Granny using Flask.
Author: Your Name
Date: YYYY-MM-DD
"""
import argparse
from typing import Optional
from Granny.Analyses.Analysis import Analysis
from Granny.Interfaces.UI.GrannyUI import GrannyUI
# Import all available analyses
from Granny.Analyses.Segmentation import Segmentation
from Granny.Analyses.BlushColor import BlushColor
from Granny.Analyses.PeelColor import PeelColor
from Granny.Analyses.StarchArea import StarchArea
from Granny.Analyses.SuperficialScald import SuperficialScald
Step 3: Define Your Interface Class
Create your class inheriting from GrannyUI:
class GrannyWeb(GrannyUI):
"""
Web-based interface for Granny analysis.
This interface provides a web server that allows users to
upload images and run analyses through a browser.
Attributes:
analysis_name (str): Name of the selected analysis
port (int): Port number for the web server
"""
def __init__(self, parser: argparse.ArgumentParser):
"""
Initialize the web interface.
Args:
parser: ArgumentParser instance for handling command-line arguments
"""
super().__init__(parser)
self.analysis_name: str = ""
self.port: int = 5000
Step 4: Implement addProgramArgs()
This method adds interface-specific arguments to the argument parser:
def addProgramArgs(self) -> None:
"""
Add web interface specific arguments to the argument parser.
This method is called during initialization to set up all
command-line arguments that this interface needs.
"""
# Add interface-specific arguments
iface_grp = self.parser.add_argument_group("Web interface args")
iface_grp.add_argument(
"--port",
dest="port",
type=int,
default=5000,
help="Port number for the web server (default: 5000)",
)
iface_grp.add_argument(
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address for the web server (default: 127.0.0.1)",
)
iface_grp.add_argument(
"--debug",
dest="debug",
action="store_true",
help="Run server in debug mode",
)
Note: Unlike the CLI interface, a web interface might not need to specify analysis types in command-line arguments, as users will select analyses through the web UI. However, you may want to add arguments for server configuration.
Step 5: Implement run()
This is the main entry point for your interface:
def run(self):
"""
Start the web interface.
This method launches the Flask web server and handles
user interactions through HTTP requests.
"""
# Parse command-line arguments
args, _ = self.parser.parse_known_args()
# Import Flask here to avoid dependency if not using this interface
try:
from flask import Flask, render_template, request, jsonify
except ImportError:
print("Error: Flask is not installed.")
print("Install it with: pip install flask")
return
# Create Flask app
app = Flask(__name__)
# Store reference to self for route handlers
interface = self
@app.route('/')
def index():
"""Serve the main page."""
# Get list of available analyses
analyses = Analysis.__subclasses__()
analysis_info = [
{
"name": cls.__analysis_name__,
"class": cls.__name__
}
for cls in analyses
]
return render_template('index.html', analyses=analysis_info)
@app.route('/run_analysis', methods=['POST'])
def run_analysis():
"""Handle analysis execution requests."""
try:
# Get analysis name from request
analysis_name = request.form.get('analysis')
if not analysis_name:
return jsonify({"error": "No analysis specified"}), 400
# Find and instantiate the analysis
analysis = interface._get_analysis(analysis_name)
if not analysis:
return jsonify({"error": f"Unknown analysis: {analysis_name}"}), 400
# Set parameters from form data
interface._set_analysis_params(analysis, request.form)
# Run the analysis
results = analysis.performAnalysis()
return jsonify({
"success": True,
"message": f"Processed {len(results)} images",
"results": [img.getFileName() for img in results]
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# Start the server
print(f"Starting Granny web interface on {args.host}:{args.port}")
print(f"Open your browser to: http://{args.host}:{args.port}")
app.run(host=args.host, port=args.port, debug=args.debug)
def _get_analysis(self, analysis_name: str) -> Optional[Analysis]:
"""
Get an analysis instance by name.
Args:
analysis_name: Machine-readable name of the analysis
Returns:
Analysis instance or None if not found
"""
analyses = Analysis.__subclasses__()
for cls in analyses:
if cls.__analysis_name__ == analysis_name:
return cls()
return None
def _set_analysis_params(self, analysis: Analysis, form_data: dict) -> None:
"""
Set analysis parameters from form data.
Args:
analysis: Analysis instance to configure
form_data: Dictionary of form parameters
"""
params = analysis.getInParams()
analysis.resetInParams()
for param in params.values():
# Get value from form data
form_key = param.getLabel()
if form_key in form_data:
value = form_data[form_key]
# Convert to appropriate type
param_type = param.getType()
if param_type == int:
value = int(value)
elif param_type == float:
value = float(value)
param.setValue(value)
print(f" {param.getLabel()}: {value}")
else:
# Use default value
print(f" {param.getLabel()}: (default) {param.getValue()}")
analysis.addInParam(param)
Step 6: Register Your Interface
Edit Granny/GrannyBase.py to register your new interface:
# At the top, import your interface
from Granny.Interfaces.UI.GrannyWeb import GrannyWeb
# In the run() function, add your interface to the choices
parser.add_argument(
"-i",
dest="interface",
type=str,
required=True,
choices=["cli", "gui", "web"], # Add "web" here
help="Indicates the user interface to use.",
)
# Add elif branch for your interface
elif interface == "web":
iface = GrannyWeb(parser)
Step 7: Create Required Assets (if applicable)
For a web interface, you’ll need HTML templates:
Create templates/index.html:
<!DOCTYPE html>
<html>
<head>
<title>Granny - Fruit Quality Analysis</title>
</head>
<body>
<h1>Granny Analysis</h1>
<form id="analysisForm" action="/run_analysis" method="POST">
<label for="analysis">Select Analysis:</label>
<select name="analysis" id="analysis" required>
{% for analysis in analyses %}
<option value="{{ analysis.name }}">{{ analysis.class }}</option>
{% endfor %}
</select>
<label for="input">Input Directory:</label>
<input type="text" name="input" id="input" required>
<button type="submit">Run Analysis</button>
</form>
<div id="results"></div>
<script>
document.getElementById('analysisForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const response = await fetch('/run_analysis', {
method: 'POST',
body: formData
});
const result = await response.json();
document.getElementById('results').innerHTML =
JSON.stringify(result, null, 2);
};
</script>
</body>
</html>
Example: Alternative CLI Interface
Here’s an example of a simpler alternative interface that reads from a configuration file:
import json
import argparse
from Granny.Interfaces.UI.GrannyUI import GrannyUI
from Granny.Analyses.Analysis import Analysis
class GrannyConfig(GrannyUI):
"""
Configuration file-based interface for Granny.
Reads analysis parameters from a JSON configuration file
and executes batch analyses.
"""
def __init__(self, parser: argparse.ArgumentParser):
super().__init__(parser)
def addProgramArgs(self) -> None:
"""Add config-specific arguments."""
iface_grp = self.parser.add_argument_group("Config interface args")
iface_grp.add_argument(
"--config",
dest="config_file",
type=str,
required=True,
help="Path to JSON configuration file",
)
def run(self):
"""Execute analyses based on config file."""
args, _ = self.parser.parse_known_args()
# Load configuration
with open(args.config_file, 'r') as f:
config = json.load(f)
# Process each analysis in config
for analysis_config in config.get('analyses', []):
analysis_name = analysis_config.get('type')
params = analysis_config.get('parameters', {})
# Find and instantiate analysis
analysis = self._get_analysis(analysis_name)
if not analysis:
print(f"Error: Unknown analysis '{analysis_name}'")
continue
# Set parameters
self._set_params(analysis, params)
# Run analysis
print(f"Running {analysis_name}...")
results = analysis.performAnalysis()
print(f"Completed: {len(results)} images processed\n")
def _get_analysis(self, name: str) -> Analysis:
"""Get analysis by name."""
for cls in Analysis.__subclasses__():
if cls.__analysis_name__ == name:
return cls()
return None
def _set_params(self, analysis: Analysis, params: dict):
"""Set analysis parameters from dict."""
analysis_params = analysis.getInParams()
analysis.resetInParams()
for param in analysis_params.values():
label = param.getLabel()
if label in params:
param.setValue(params[label])
analysis.addInParam(param)
Example configuration file (config.json):
{
"analyses": [
{
"type": "segmentation",
"parameters": {
"input": "./images/raw",
"model": "pome_fruit-v1_0",
"confidence": 0.3
}
},
{
"type": "starch",
"parameters": {
"input": "./results/segmentation",
"starch_threshold": 175,
"blur_kernel": 9
}
}
]
}
Best Practices
Argument Parsing
Use
add_argument_group()to organize interface-specific argumentsProvide sensible defaults for optional arguments
Include helpful descriptions in the
helpparameterUse appropriate types (int, str, float, bool) for type checking
Error Handling
Validate user input before passing to analyses
Provide clear error messages for common mistakes
Gracefully handle missing dependencies
Log errors appropriately for the interface type
User Feedback
Provide progress updates during long operations
Display parameter values being used (especially defaults)
Show clear success/failure messages
For GUIs, use progress bars and status messages
Parameter Discovery
Use
Analysis.__subclasses__()to discover available analysesUse
analysis.getInParams()to discover analysis parametersDynamically generate UI elements based on parameter types
Respect parameter constraints (min/max, required/optional)
Testing
Create test file tests/test_Interfaces/test_GrannyWeb.py:
from Granny.Interfaces.UI.GrannyWeb import GrannyWeb
import argparse
def test_init():
"""Test interface initialization."""
parser = argparse.ArgumentParser()
interface = GrannyWeb(parser)
assert interface is not None
def test_addProgramArgs():
"""Test argument addition."""
parser = argparse.ArgumentParser()
interface = GrannyWeb(parser)
interface.addProgramArgs()
# Verify arguments were added
args = parser.parse_args(['--port', '8080'])
assert args.port == 8080
Advanced Topics
Dynamic Parameter UI Generation
For GUI or web interfaces, generate form fields based on parameter types:
def generate_parameter_fields(analysis: Analysis) -> list:
"""
Generate UI field specifications for analysis parameters.
Returns:
List of field specifications for UI rendering
"""
fields = []
params = analysis.getInParams()
for param in params.values():
field = {
"name": param.getLabel(),
"label": param.getLabel().replace('_', ' ').title(),
"type": param.getType().__name__,
"default": param.getValue(),
"required": param.getIsRequired(),
"help": param.getHelp()
}
# Add constraints for numeric types
if hasattr(param, 'getMin'):
field["min"] = param.getMin()
field["max"] = param.getMax()
fields.append(field)
return fields
Asynchronous Processing
For web or GUI interfaces, run analyses asynchronously:
import threading
from queue import Queue
def run_analysis_async(analysis: Analysis, result_queue: Queue):
"""Run analysis in background thread."""
try:
results = analysis.performAnalysis()
result_queue.put({"success": True, "results": results})
except Exception as e:
result_queue.put({"success": False, "error": str(e)})
Multi-Analysis Workflows
For advanced interfaces, implement workflow management:
from Granny.Interfaces.Scheduler.Scheduler import Scheduler
def run_workflow(workflow: list) -> list:
"""
Execute a multi-step analysis workflow.
Args:
workflow: List of (analysis, dependencies) tuples
Returns:
List of all results
"""
scheduler = Scheduler()
for analysis, deps in workflow:
scheduler.add_analysis(analysis, deps)
return scheduler.schedule()
Troubleshooting
Interface not available in CLI:
Verify you added your interface to the choices in
GrannyBase.pyCheck the import statement is correct
Ensure the
elifbranch for your interface exists
Parameters not being set:
Verify you’re calling
resetInParams()before setting parametersCheck that parameter names match between UI and analysis
Ensure type conversion is correct (str → int/float where needed)
Verify you’re calling
addInParam()after setting values
Analysis not found:
Check that you’re importing all analysis classes
Verify
Analysis.__subclasses__()returns expected classesEnsure analysis
__analysis_name__matches what user provides
Next Steps
Study
Granny/Interfaces/UI/GrannyCLI.pyas a reference implementationReview
Granny/Interfaces/UI/GrannyPyQt.pyfor GUI patternsRefer to API Reference for detailed API documentation
Test your interface with all available analyses