diff --git a/docs/dev_guide/adding_analysis.rst b/docs/dev_guide/adding_analysis.rst
index b79687e..4f8abf7 100644
--- a/docs/dev_guide/adding_analysis.rst
+++ b/docs/dev_guide/adding_analysis.rst
@@ -10,11 +10,28 @@ An analysis module in Granny:
- Inherits from the ``Analysis`` abstract base class
- Defines input parameters using the Value system
-- Implements the ``performAnalysis()`` method
+- Implements three abstract methods: ``_preRun()``, ``_processImage()``, and ``_postRun()``
- Returns a list of processed ``Image`` objects
- Automatically integrates with all Granny interfaces (CLI, GUI)
+- Leverages built-in multiprocessing for parallel image processing
- Can be chained with other analyses using the Scheduler
+The Analysis Architecture
+-------------------------
+
+The base ``Analysis`` class provides a ``performAnalysis()`` method that:
+
+1. Loads images from the input directory via ``ImageListValue``
+2. Calls your ``_preRun()`` method for setup
+3. Processes images in parallel using ``multiprocessing.Pool``
+4. Calls your ``_postRun()`` method for post-processing and saving results
+
+You implement the three abstract methods to customize behavior:
+
+- ``_preRun()``: Setup before processing (initialize variables, load models, etc.)
+- ``_processImage(image)``: Process a single image (runs in parallel across CPU cores)
+- ``_postRun(results)``: Post-processing after all images are done (save CSV, cleanup)
+
The Value System
----------------
@@ -34,7 +51,7 @@ Granny uses a type-safe Value system for parameters. Each parameter is represent
- ``BoolValue`` - Boolean flags
- ``FileNameValue`` - File paths
- ``FileDirValue`` - Directory paths
-- ``ImageListValue`` - Lists of images (typically input/output directories)
+- ``ImageListValue`` - Directory containing images (handles loading/saving)
- ``MetaDataValue`` - Metadata storage for results
Step-by-Step Guide
@@ -65,7 +82,7 @@ Start your file with necessary imports:
import os
from datetime import datetime
- from typing import List
+ from typing import Dict, List, Tuple
import cv2
import numpy as np
@@ -74,10 +91,10 @@ Start your file with necessary imports:
from Granny.Analyses.Analysis import Analysis
from Granny.Models.Images.Image import Image
from Granny.Models.Images.RGBImage import RGBImage
- from Granny.Models.IO.ImageIO import ImageIO
from Granny.Models.IO.RGBImageFile import RGBImageFile
from Granny.Models.Values.IntValue import IntValue
from Granny.Models.Values.FloatValue import FloatValue
+ from Granny.Models.Values.StringValue import StringValue
from Granny.Models.Values.ImageListValue import ImageListValue
from Granny.Models.Values.MetaDataValue import MetaDataValue
@@ -98,7 +115,6 @@ Create your class inheriting from ``Analysis``:
images (List[Image]): List of loaded images for processing
input_images (ImageListValue): Input directory parameter
output_images (ImageListValue): Output directory parameter
- output_results (MetaDataValue): Results directory parameter
my_threshold (IntValue): Example threshold parameter
"""
@@ -118,6 +134,7 @@ Initialize your analysis with parameters:
super().__init__()
self.images: List[Image] = []
+ self.results_data: List[dict] = [] # For collecting CSV data
# Required: Input images parameter
self.input_images = ImageListValue(
@@ -143,14 +160,6 @@ Initialize your analysis with parameters:
self.output_images.setValue(result_dir) # Set default value
self.addInParam(self.output_images)
- # Output directory for CSV results
- self.output_results = MetaDataValue(
- "results",
- "results",
- "The output directory where analysis results are written."
- )
- self.output_results.setValue(result_dir)
-
# Analysis parameter: threshold
self.my_threshold = IntValue(
"threshold", # Machine-readable name
@@ -184,95 +193,130 @@ Initialize your analysis with parameters:
- Use ``addRetValue()`` for values that other analyses can use
- Set ``setIsRequired(True)`` for mandatory parameters
-Step 5: Implement performAnalysis()
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Step 5: Implement the Three Abstract Methods
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-This is the core method that executes your analysis:
+Instead of overriding ``performAnalysis()``, implement these three methods:
.. code-block:: python
- def performAnalysis(self) -> List[Image]:
+ def _preRun(self):
+ """
+ Setup before image processing begins.
+
+ This method is called once before any images are processed.
+ Use it to:
+ - Initialize result containers
+ - Load models or resources
+ - Print analysis parameters
"""
- Perform the analysis on all input images.
+ self.results_data = [] # Reset results for this run
+
+ # Get output directory
+ self.output_dir = self.output_images.getValue()
+
+ # Print analysis info
+ print(f"\n{'='*60}")
+ print(f"MY NEW ANALYSIS")
+ print(f"{'='*60}")
+ print(f"Input directory: {self.input_images.getValue()}")
+ print(f"Output directory: {self.output_dir}")
+ print(f"Threshold: {self.my_threshold.getValue()}")
+ print(f"Mask alpha: {self.mask_alpha.getValue()}")
+ print(f"Processing {len(self.images)} images...")
+ print(f"{'='*60}\n")
+
+ def _processImage(self, image: Image) -> Image:
+ """
+ Process a single image.
+
+ This method runs in parallel across multiple CPU cores.
+ Each call receives one Image instance and should return a processed Image.
+
+ Args:
+ image: The input Image instance to process
Returns:
- List[Image]: List of processed Image objects with analysis results
+ Image: The processed Image with results
"""
- # Step 1: Load input images
- input_dir = self.input_images.getValue()
- output_dir = self.output_images.getValue()
- results_dir = self.output_results.getValue()
+ # Get parameter values
+ threshold = self.my_threshold.getValue()
+ alpha = self.mask_alpha.getValue()
- print(f"Loading images from: {input_dir}")
- imageIO = ImageIO()
- self.images = imageIO.load(input_dir, RGBImageFile)
+ # Load the image data
+ image_io = RGBImageFile()
+ image_io.setFilePath(image.getFilePath())
+ image.loadImage(image_io)
- if not self.images:
- print("No images found to analyze.")
- return []
+ # Get the numpy array (BGR format from OpenCV)
+ img_array = image.getImage()
- print(f"Processing {len(self.images)} images...")
+ # Perform your analysis
+ result_array, metric_value = self._analyze_image(img_array, threshold, alpha)
- # Step 2: Process each image
- result_images = []
- results_data = [] # For CSV output
-
- for idx, image in enumerate(self.images, 1):
- print(f" Processing image {idx}/{len(self.images)}: {image.getFileName()}")
-
- # Get the numpy array
- img_array = image.getImageFile().getImage()
-
- # Perform your analysis
- result_array, metric_value = self._analyze_image(img_array)
-
- # Create result image
- result_image = RGBImage()
- result_file = RGBImageFile()
- result_file.setImage(result_array)
- result_file.setFileName(image.getFileName())
- result_file.setFilePath(output_dir)
- result_image.setImageFile(result_file)
-
- # Add metadata to the image
- result_image.addMetadata(self.metadata)
- result_image.addMetadata([
- {"name": "metric_value", "value": metric_value}
- ])
-
- result_images.append(result_image)
- results_data.append({
- "filename": image.getFileName(),
- "metric_value": metric_value
- })
+ # Update the image with the result
+ image.setImage(result_array)
+
+ # Add metadata to the image
+ metric_val = StringValue("metric", "metric", "Analysis metric value")
+ metric_val.setValue(str(metric_value))
+ image.addValue(metric_val)
- # Step 3: Save results
- print(f"Saving results to: {output_dir}")
- imageIO.save(result_images)
+ return image
+
+ def _postRun(self, results: List[Image]) -> List[Image]:
+ """
+ Post-processing after all images are processed.
+
+ This method is called once after all images have been processed.
+ Use it to:
+ - Save images to disk
+ - Generate CSV reports
+ - Print summary statistics
+
+ Args:
+ results: List of processed Image objects from _processImage()
+
+ Returns:
+ List[Image]: The final list of result images
+ """
+ print(f"\nSaving {len(results)} images to: {self.output_dir}")
+
+ # Save each image
+ image_io = RGBImageFile()
+ for image in results:
+ image.saveImage(image_io, self.output_dir)
+
+ # Collect data for CSV
+ csv_data = []
+ for image in results:
+ metadata = image.getMetaData()
+ csv_data.append({
+ "filename": image.getImageName(),
+ "metric": metadata.get("metric", StringValue("", "", "")).getValue()
+ })
# Save CSV
- self._save_csv(results_data, results_dir)
+ self._save_csv(csv_data, self.output_dir)
- print("Analysis complete!")
- return result_images
+ print(f"\n{'='*60}")
+ print(f"Analysis complete! Processed {len(results)} images.")
+ print(f"{'='*60}\n")
- def _analyze_image(self, img: NDArray) -> tuple[NDArray, float]:
+ return results
+
+ def _analyze_image(self, img: NDArray, threshold: int, alpha: float) -> Tuple[NDArray, float]:
"""
- Perform analysis on a single image.
+ Perform analysis on a single image array.
Args:
img: Input image as numpy array (BGR format)
+ threshold: Detection threshold
+ alpha: Mask transparency
Returns:
Tuple of (processed_image, metric_value)
"""
- # Get parameter values
- threshold = self.my_threshold.getValue()
- alpha = self.mask_alpha.getValue()
-
- # Your image processing logic here
- # This is a simple example - replace with your actual algorithm
-
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
@@ -319,18 +363,7 @@ This is the core method that executes your analysis:
print(f"Results saved to: {csv_path}")
-Step 6: Add Your Analysis to the Import Path
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Edit ``Granny/Analyses/__init__.py`` to include your new analysis:
-
-.. code-block:: python
-
- from .MyNewAnalysis import MyNewAnalysis
-
-This makes your analysis discoverable by the CLI interface.
-
-Step 7: Update the CLI Interface
+Step 6: Update the CLI Interface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Edit ``Granny/Interfaces/UI/GrannyCLI.py`` to add your analysis to the choices list:
@@ -352,7 +385,7 @@ Also import your analysis class at the top of the file:
from Granny.Analyses.MyNewAnalysis import MyNewAnalysis
-Step 8: Create Tests
+Step 7: Create Tests
~~~~~~~~~~~~~~~~~~~~~
Create a test file ``tests/test_Analyses/test_MyNewAnalysis.py``:
@@ -367,22 +400,19 @@ Create a test file ``tests/test_Analyses/test_MyNewAnalysis.py``:
assert analysis.__analysis_name__ == "myanalysis"
assert analysis.my_threshold.getValue() == 128
- def test_performAnalysis():
- """Test the analysis with sample data."""
+ def test_parameters():
+ """Test parameter constraints."""
analysis = MyNewAnalysis()
- # Set test parameters
- analysis.input_images.setValue("test-assets/sample_images")
- analysis.my_threshold.setValue(100)
+ # Test threshold bounds
+ assert analysis.my_threshold.getMin() == 0
+ assert analysis.my_threshold.getMax() == 255
- # Run analysis
- results = analysis.performAnalysis()
+ # Test alpha bounds
+ assert analysis.mask_alpha.getMin() == 0.0
+ assert analysis.mask_alpha.getMax() == 1.0
- # Verify results
- assert isinstance(results, list)
- # Add more specific assertions based on your analysis
-
-Step 9: Test Your Analysis
+Step 8: Test Your Analysis
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Run your analysis from the command line:
@@ -400,6 +430,16 @@ Run the test suite:
Best Practices
--------------
+Multiprocessing Considerations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Since ``_processImage()`` runs in parallel:
+
+- Don't modify shared state (use return values instead)
+- Each image should be processed independently
+- Heavy initialization belongs in ``_preRun()``
+- Aggregation and saving belongs in ``_postRun()``
+
Parameter Naming
~~~~~~~~~~~~~~~~
@@ -415,18 +455,10 @@ Documentation
- Include examples in the module docstring
- Explain the scientific/algorithmic basis of your analysis
-Performance
-~~~~~~~~~~~
-
-- Process images efficiently using NumPy operations
-- Consider multiprocessing for independent image processing (see ``BlushColor`` for example)
-- Minimize memory usage for large image sets
-- Provide progress feedback via print statements
-
Error Handling
~~~~~~~~~~~~~~
-- Validate input parameters in ``performAnalysis()``
+- Validate input parameters in ``_preRun()``
- Handle cases where no images are found
- Provide clear error messages to users
- Gracefully handle edge cases (empty images, invalid formats)
@@ -476,17 +508,37 @@ If your analysis produces values that other analyses might use:
)
self.addRetValue(self.fruit_coordinates)
- # In performAnalysis()
+ # In _postRun()
self.fruit_coordinates.setValue([(x1, y1, x2, y2), ...])
-Custom Image Types
-~~~~~~~~~~~~~~~~~~
+CPU Core Configuration
+~~~~~~~~~~~~~~~~~~~~~~
+
+The base ``Analysis`` class automatically handles CPU core allocation:
+
+- Default (0): Uses 80% of available cores
+- User can override via ``--cpu N`` CLI argument
+- Set ``self.cpu.setValue(4)`` in ``__init__`` to change default
+
+Working with Image Metadata
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add metadata to images using ``Value`` objects:
+
+.. code-block:: python
+
+ from Granny.Models.Values.StringValue import StringValue
+ from Granny.Models.Values.FloatValue import FloatValue
-If you need a specialized image type beyond ``RGBImage``:
+ # In _processImage()
+ score_val = FloatValue("score", "score", "Analysis score")
+ score_val.setValue(95.5)
+ image.addValue(score_val)
-1. Create a new class inheriting from ``Image`` in ``Granny/Models/Images/``
-2. Create a corresponding file class inheriting from ``ImageFile`` in ``Granny/Models/IO/``
-3. Implement required methods for loading and saving
+ # In _postRun(), retrieve metadata
+ for image in results:
+ metadata = image.getMetaData()
+ score = metadata.get("score").getValue()
Troubleshooting
---------------
@@ -495,7 +547,6 @@ Troubleshooting
- Verify ``__analysis_name__`` is set correctly
- Check that you added the analysis to GrannyCLI's choices list
-- Ensure the import statement is in ``Granny/Analyses/__init__.py``
**Parameters not showing in help:**
@@ -506,16 +557,14 @@ Troubleshooting
**Images not loading:**
- Verify the input directory path is correct
-- Check that images are in a supported format (JPG, PNG)
-- Ensure ``ImageIO`` is initialized correctly
-- Use ``RGBImageFile`` for standard RGB images
+- Check that images are in a supported format (JPG, PNG, JPEG, TIFF)
+- Ensure ``RGBImageFile`` is used correctly with ``setFilePath()``
-**Results not saving:**
+**Multiprocessing errors:**
-- Verify the output directory is writable
-- Check that ``imageIO.save()`` is called with the result images
-- Ensure each image has both an ImageFile and metadata set
-- Verify directory paths are created with ``os.makedirs(path, exist_ok=True)``
+- Ensure ``_processImage()`` doesn't access shared mutable state
+- Check that all objects passed between processes are picklable
+- Move file I/O to ``_preRun()`` or ``_postRun()`` if needed
Next Steps
----------
diff --git a/docs/dev_guide/adding_interface.rst b/docs/dev_guide/adding_interface.rst
index dbed692..332bc58 100644
--- a/docs/dev_guide/adding_interface.rst
+++ b/docs/dev_guide/adding_interface.rst
@@ -124,10 +124,13 @@ Create your class inheriting from ``GrannyUI``:
self.analysis_name: str = ""
self.port: int = 5000
-Step 4: Implement addProgramArgs()
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Step 4: Implement addProgramArgs() (Optional but Recommended)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This method adds interface-specific arguments to the argument parser.
-This method adds interface-specific arguments to the argument parser:
+**Note:** ``addProgramArgs()`` is NOT an abstract method in the base class - only ``run()``
+is required. However, most interfaces implement this method to add their own arguments:
.. code-block:: python
@@ -236,7 +239,7 @@ This is the main entry point for your interface:
return jsonify({
"success": True,
"message": f"Processed {len(results)} images",
- "results": [img.getFileName() for img in results]
+ "results": [img.getImageName() for img in results]
})
except Exception as e:
diff --git a/docs/dev_guide/api_reference.rst b/docs/dev_guide/api_reference.rst
index b3450ab..72defce 100644
--- a/docs/dev_guide/api_reference.rst
+++ b/docs/dev_guide/api_reference.rst
@@ -57,6 +57,16 @@ Analysis Base Class
- ``id`` - Unique analysis identifier (UUID)
- ``path`` - Current directory path
+ .. py:attribute:: cpu
+ :type: IntValue
+
+ Number of CPU cores for parallel processing. Default 0 = auto (80% of cores).
+
+ .. py:attribute:: images
+ :type: List[Image]
+
+ List of loaded images (populated by ``performAnalysis()``).
+
**Methods:**
.. py:method:: __init__()
@@ -111,23 +121,54 @@ Analysis Base Class
Clear all return values.
.. py:method:: performAnalysis() -> List[Image]
- :abstractmethod:
- **ABSTRACT:** Perform the analysis on input images.
+ Perform the analysis on input images. **Do not override this method.**
- This method must be implemented by all subclasses.
+ The default implementation:
+ 1. Loads images from ``input_images`` parameter via ``ImageListValue.readValue()``
+ 2. Calls ``_preRun()`` for setup
+ 3. Processes images in parallel using ``multiprocessing.Pool``
+ 4. Calls ``_postRun()`` with results
- :return: List of processed Image objects
+ :return: List of processed Image objects (from ``_postRun()``)
:rtype: List[Image]
- **Implementation guidelines:**
+ **Abstract Methods (must implement):**
+
+ .. py:method:: _preRun() -> None
+ :abstractmethod:
+
+ Setup before image processing begins. Called once before any images are processed.
+
+ Use this for:
+ - Initializing result containers
+ - Loading models or resources
+ - Printing analysis parameters
+
+ .. py:method:: _processImage(image: Image) -> Image
+ :abstractmethod:
+
+ Process a single image. Runs in parallel across CPU cores.
+
+ :param image: Input Image instance
+ :return: Processed Image instance
+ :rtype: Image
+
+ **Important:** This method runs in separate processes. Avoid modifying shared state.
+
+ .. py:method:: _postRun(results: List[Image]) -> List[Image]
+ :abstractmethod:
+
+ Post-processing after all images are done. Called once with all results.
- 1. Load images from ``input_images`` parameter
- 2. Process each image
- 3. Create result Image objects with results
- 4. Add metadata to result images
- 5. Save results (images and CSV)
- 6. Return list of result images
+ Use this for:
+ - Saving images to disk
+ - Generating CSV reports
+ - Computing aggregate statistics
+
+ :param results: List of processed Image objects from ``_processImage()``
+ :return: Final list of result images
+ :rtype: List[Image]
GrannyUI Base Class
~~~~~~~~~~~~~~~~~~~
@@ -171,19 +212,8 @@ GrannyUI Base Class
5. Call ``analysis.performAnalysis()``
6. Handle/display results
- .. py:method:: addProgramArgs() -> None
- :abstractmethod:
-
- **ABSTRACT:** Add interface-specific arguments to the argument parser.
-
- This method is called during initialization to set up command-line arguments
- that the interface needs.
-
- Example::
-
- def addProgramArgs(self):
- grp = self.parser.add_argument_group("My Interface Args")
- grp.add_argument("--my-option", type=str, help="...")
+ **Note:** ``addProgramArgs()`` is commonly implemented but not required by the base class.
+ See ``GrannyCLI`` for an example implementation.
Value Classes
-------------
@@ -241,11 +271,6 @@ Value Base Class
Whether the user has set this value.
- .. py:attribute:: required
- :type: bool
-
- Whether this parameter is required.
-
**Methods:**
.. py:method:: validate(value: Any) -> bool
@@ -433,12 +458,32 @@ FileDirValue
ImageListValue
~~~~~~~~~~~~~~
-.. py:class:: ImageListValue(Value)
+.. py:class:: ImageListValue(FileDirValue)
Image list/directory parameter type. Represents a directory containing images.
+ Handles loading and saving of image lists.
**Location:** ``Granny/Models/Values/ImageListValue.py``
+ **Additional Methods:**
+
+ .. py:method:: readValue() -> None
+
+ Load all images from the directory into the internal image list.
+ Called automatically by ``Analysis.performAnalysis()``.
+
+ .. py:method:: writeValue() -> None
+
+ Save all images in the internal list to the directory.
+
+ .. py:method:: getImageList() -> List[Image]
+
+ Get the list of loaded Image objects.
+
+ .. py:method:: setImageList(images: List[Image]) -> None
+
+ Set the list of Image objects.
+
**Example:**
.. code-block:: python
@@ -451,6 +496,9 @@ ImageListValue
input_images.setIsRequired(True)
input_images.setValue("./demo/images")
+ # After readValue() is called:
+ images = input_images.getImageList()
+
MetaDataValue
~~~~~~~~~~~~~
@@ -474,159 +522,246 @@ MetaDataValue
Image Classes
-------------
-Image
-~~~~~
+Image Base Class
+~~~~~~~~~~~~~~~~
.. py:class:: Image
- Base class for images in Granny.
+ Abstract base class for images in Granny.
**Location:** ``Granny/Models/Images/Image.py``
+ **Constructor:**
+
+ .. py:method:: __init__(filepath: str)
+
+ Initialize the Image with a file path.
+
+ :param filepath: Path to the image file
+
+ **Attributes:**
+
+ .. py:attribute:: filepath
+ :type: str
+
+ Absolute file path of the image.
+
+ .. py:attribute:: image
+ :type: NDArray[np.uint8]
+
+ The image data as a NumPy array.
+
+ .. py:attribute:: metadata
+ :type: Dict[str, Value]
+
+ Dictionary of metadata values attached to this image.
+
+ .. py:attribute:: results
+ :type: Any
+
+ Segmentation results (for use with YOLO models).
+
**Methods:**
- .. py:method:: setImageFile(image_file: ImageFile) -> None
+ .. py:method:: addValue(*values: Value) -> None
- Set the ImageFile object containing the actual image data.
+ Add metadata values to the image.
- .. py:method:: getImageFile() -> ImageFile
+ :param values: One or more Value objects to attach
- Get the ImageFile object.
+ Example::
+
+ score = FloatValue("score", "score", "Analysis score")
+ score.setValue(95.5)
+ image.addValue(score)
+
+ .. py:method:: getValue(key: str) -> Value
+
+ Get a metadata value by name.
+
+ :param key: The name of the metadata value
+ :return: The Value object
- .. py:method:: getFileName() -> str
+ .. py:method:: getFilePath() -> str
+
+ Get the absolute file path.
- Get the image filename.
+ .. py:method:: getImageName() -> str
- .. py:method:: addMetadata(metadata: List[dict]) -> None
+ Get the filename (without path).
- Add metadata to the image.
+ .. py:method:: getShape() -> tuple
- :param metadata: List of metadata dictionaries with "name" and "value" keys
+ Get the image dimensions (height, width, channels).
- .. py:method:: getMetadata() -> List[dict]
+ **Abstract Methods:**
+
+ .. py:method:: loadImage(image_io: ImageIO) -> None
+ :abstractmethod:
+
+ Load image data using the provided ImageIO instance.
+
+ .. py:method:: saveImage(image_io: ImageIO, folder: str) -> None
+ :abstractmethod:
+
+ Save image data to the specified folder.
+
+ .. py:method:: getImage() -> NDArray[np.uint8]
+ :abstractmethod:
+
+ Get the image data as a NumPy array.
+
+ .. py:method:: setImage(image: NDArray[np.uint8]) -> None
+ :abstractmethod:
+
+ Set the image data from a NumPy array.
+
+ .. py:method:: getMetaData() -> Dict[str, Value]
+ :abstractmethod:
Get all metadata attached to this image.
+ .. py:method:: setMetaData(metadata: Dict[str, Value]) -> None
+ :abstractmethod:
+
+ Set the metadata for the image.
+
RGBImage
~~~~~~~~
.. py:class:: RGBImage(Image)
- RGB color image type.
+ RGB color image type. The most common image type used in Granny analyses.
**Location:** ``Granny/Models/Images/RGBImage.py``
- This is the most common image type used in Granny analyses.
+ **Constructor:**
- **Example:**
+ .. py:method:: __init__(filepath: str)
- .. code-block:: python
+ Initialize with a file path.
- result_image = RGBImage()
- result_file = RGBImageFile()
- result_file.setImage(processed_array)
- result_file.setFileName("result.jpg")
- result_file.setFilePath("./output")
- result_image.setImageFile(result_file)
- result_image.addMetadata([{"name": "score", "value": 95.5}])
+ :param filepath: Path to the image file
-ImageFile Classes
------------------
+ **Additional Methods:**
-ImageFile
-~~~~~~~~~
+ .. py:method:: toRGB() -> None
-.. py:class:: ImageFile
+ Convert image from BGR to RGB format.
- Base class for image file handlers.
+ .. py:method:: toBGR() -> None
- **Location:** ``Granny/Models/IO/ImageIO.py``
+ Convert image from RGB to BGR format.
- **Methods:**
+ .. py:method:: rotateImage() -> None
- .. py:method:: setImage(image: NDArray) -> None
+ Rotate the image 90 degrees clockwise.
- Set the image data as a NumPy array.
+ **Example:**
- .. py:method:: getImage() -> NDArray
+ .. code-block:: python
- Get the image data as a NumPy array.
+ # Load an image
+ image = RGBImage("/path/to/image.jpg")
+ image_io = RGBImageFile()
+ image_io.setFilePath(image.getFilePath())
+ image.loadImage(image_io)
- .. py:method:: setFileName(name: str) -> None
+ # Process the image
+ img_array = image.getImage()
+ processed = cv2.cvtColor(img_array, cv2.COLOR_BGR2GRAY)
+ image.setImage(processed)
- Set the filename.
+ # Add metadata
+ score = FloatValue("score", "score", "Analysis score")
+ score.setValue(95.5)
+ image.addValue(score)
- .. py:method:: getFileName() -> str
+ # Save
+ image.saveImage(image_io, "./output")
- Get the filename.
+ImageIO Classes
+---------------
- .. py:method:: setFilePath(path: str) -> None
+ImageIO Base Class
+~~~~~~~~~~~~~~~~~~
- Set the directory path.
+.. py:class:: ImageIO
- .. py:method:: getFilePath() -> str
+ Abstract base class for image file handlers. Handles loading and saving individual images.
- Get the directory path.
+ **Location:** ``Granny/Models/IO/ImageIO.py``
-RGBImageFile
-~~~~~~~~~~~~
+ **Attributes:**
-.. py:class:: RGBImageFile(ImageFile)
+ .. py:attribute:: filepath
+ :type: str
- RGB image file handler. Uses OpenCV for loading/saving.
+ Full path to the image file.
- **Location:** ``Granny/Models/IO/RGBImageFile.py``
+ .. py:attribute:: image_dir
+ :type: str
- Automatically handles image I/O in BGR format (OpenCV convention).
+ Directory containing the image.
-I/O Classes
------------
+ .. py:attribute:: image_name
+ :type: str
-ImageIO
-~~~~~~~
+ Filename of the image.
-.. py:class:: ImageIO
+ **Methods:**
- Handles loading and saving of images.
+ .. py:method:: setFilePath(filepath: str) -> None
- **Location:** ``Granny/Models/IO/ImageIO.py``
+ Set the file path and extract directory/filename.
- **Methods:**
+ :param filepath: Full path to the image file
- .. py:method:: load(path: str, file_class: Type[ImageFile]) -> List[Image]
+ **Abstract Methods:**
- Load all images from a directory.
+ .. py:method:: loadImage() -> NDArray[np.uint8]
+ :abstractmethod:
- :param path: Directory path
- :param file_class: ImageFile class to use (e.g., RGBImageFile)
- :return: List of loaded Image objects
+ Load and return the image data.
- **Example:**
+ .. py:method:: saveImage(image: NDArray[np.uint8], output_path: str) -> None
+ :abstractmethod:
- .. code-block:: python
+ Save the image to the specified directory.
- imageIO = ImageIO()
- images = imageIO.load("./input", RGBImageFile)
+ .. py:method:: getType() -> str
+ :abstractmethod:
- .. py:method:: save(images: List[Image]) -> None
+ Get the image type (e.g., "rgb", "gray").
- Save all images to their designated paths.
+RGBImageFile
+~~~~~~~~~~~~
- :param images: List of Image objects to save
+.. py:class:: RGBImageFile(ImageIO)
- **Example:**
+ RGB image file handler. Uses OpenCV for loading/saving.
- .. code-block:: python
+ **Location:** ``Granny/Models/IO/RGBImageFile.py``
- imageIO = ImageIO()
- imageIO.save(result_images)
+ Images are loaded/saved in BGR format (OpenCV convention).
+
+ **Example:**
+
+ .. code-block:: python
+
+ from Granny.Models.IO.RGBImageFile import RGBImageFile
+
+ # Load an image
+ image_io = RGBImageFile()
+ image_io.setFilePath("/path/to/image.jpg")
+ img_array = image_io.loadImage() # Returns NDArray in BGR format
+
+ # Save an image
+ image_io.saveImage(img_array, "/output/directory")
Scheduler Class
---------------
-Scheduler
-~~~~~~~~~
-
.. py:class:: Scheduler
Manages dependencies between analyses and executes them in correct order.
@@ -660,11 +795,18 @@ Scheduler
scheduler.add_analysis(segmentation, [])
scheduler.add_analysis(starch, [segmentation])
- .. py:method:: schedule() -> List[Any]
+ .. py:method:: schedule() -> List[int]
+
+ Determine execution order based on dependencies.
+
+ :return: List of analysis IDs in the order they should be run
+ :raises ValueError: If there is a cycle in the dependencies
+
+ .. py:method:: run() -> None
Execute all analyses in dependency order.
- :return: List of results from all analyses
+ Calls ``schedule()`` internally, then runs each analysis via ``performAnalysis()``.
Utility Functions
-----------------
@@ -715,47 +857,63 @@ Loading and Processing Images
.. code-block:: python
- from Granny.Models.IO.ImageIO import ImageIO
+ from Granny.Models.Images.RGBImage import RGBImage
from Granny.Models.IO.RGBImageFile import RGBImageFile
- # Load images
- imageIO = ImageIO()
- images = imageIO.load("./input", RGBImageFile)
+ # In _processImage():
+ def _processImage(self, image: Image) -> Image:
+ # Load image data
+ image_io = RGBImageFile()
+ image_io.setFilePath(image.getFilePath())
+ image.loadImage(image_io)
+
+ # Get NumPy array (BGR format)
+ img_array = image.getImage()
+
+ # Process with OpenCV/NumPy
+ result = cv2.cvtColor(img_array, cv2.COLOR_BGR2GRAY)
- # Process each image
- for image in images:
- img_array = image.getImageFile().getImage() # Get NumPy array
- # Process img_array with OpenCV/NumPy
- result_array = cv2.cvtColor(img_array, cv2.COLOR_BGR2GRAY)
+ # Update image
+ image.setImage(result)
+ return image
-Creating Result Images
-~~~~~~~~~~~~~~~~~~~~~~
+Saving Results in _postRun()
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
- from Granny.Models.Images.RGBImage import RGBImage
from Granny.Models.IO.RGBImageFile import RGBImageFile
- # Create result image
- result_image = RGBImage()
- result_file = RGBImageFile()
- result_file.setImage(processed_array)
- result_file.setFileName("output.jpg")
- result_file.setFilePath("./results")
- result_image.setImageFile(result_file)
-
- # Add metadata
- result_image.addMetadata([
- {"name": "score", "value": 85.5},
- {"name": "threshold", "value": 128}
- ])
- result_image.addMetadata(self.metadata) # Add standard metadata
-
- # Save
- imageIO.save([result_image])
-
-Setting Analysis Parameters
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ def _postRun(self, results: List[Image]) -> List[Image]:
+ output_dir = self.output_images.getValue()
+
+ image_io = RGBImageFile()
+ for image in results:
+ image.saveImage(image_io, output_dir)
+
+ return results
+
+Adding Metadata to Images
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ from Granny.Models.Values.StringValue import StringValue
+ from Granny.Models.Values.FloatValue import FloatValue
+
+ # Add metadata in _processImage()
+ score = FloatValue("score", "score", "Analysis score")
+ score.setValue(95.5)
+ image.addValue(score)
+
+ # Retrieve metadata in _postRun()
+ for image in results:
+ metadata = image.getMetaData()
+ if "score" in metadata:
+ score_value = metadata["score"].getValue()
+
+Setting Analysis Parameters (in interfaces)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
@@ -787,13 +945,16 @@ For better type checking, use these type hints:
from Granny.Models.Images.Image import Image
from Granny.Models.Values.Value import Value
- def performAnalysis(self) -> List[Image]:
+ def _processImage(self, image: Image) -> Image:
+ ...
+
+ def _postRun(self, results: List[Image]) -> List[Image]:
...
def getInParams(self) -> Dict[str, Value]:
...
- def process_image(self, img: NDArray) -> NDArray:
+ def process_array(self, img: NDArray) -> NDArray:
...
See Also
diff --git a/docs/dev_guide/example_analysis.rst b/docs/dev_guide/example_analysis.rst
index 62def42..447a979 100644
--- a/docs/dev_guide/example_analysis.rst
+++ b/docs/dev_guide/example_analysis.rst
@@ -12,7 +12,7 @@ This example implements a **BruiseDetection** analysis that:
- Calculates bruise percentage
- Provides adjustable threshold and visualization parameters
- Outputs annotated images and CSV results
-- Follows all Granny best practices
+- Uses the multiprocessing architecture via ``_preRun()``, ``_processImage()``, ``_postRun()``
The algorithm uses LAB color space to detect darker regions that indicate bruising.
@@ -39,10 +39,10 @@ File: ``Granny/Analyses/BruiseDetection.py``
Date: 2024-01-15
"""
- import os
import csv
+ import os
from datetime import datetime
- from typing import List, Tuple
+ from typing import Dict, List, Tuple
import cv2
import numpy as np
@@ -50,11 +50,10 @@ File: ``Granny/Analyses/BruiseDetection.py``
from Granny.Analyses.Analysis import Analysis
from Granny.Models.Images.Image import Image
- from Granny.Models.Images.RGBImage import RGBImage
- from Granny.Models.IO.ImageIO import ImageIO
from Granny.Models.IO.RGBImageFile import RGBImageFile
from Granny.Models.Values.IntValue import IntValue
from Granny.Models.Values.FloatValue import FloatValue
+ from Granny.Models.Values.StringValue import StringValue
from Granny.Models.Values.ImageListValue import ImageListValue
from Granny.Models.Values.MetaDataValue import MetaDataValue
@@ -67,16 +66,24 @@ File: ``Granny/Analyses/BruiseDetection.py``
identifying darker regions in LAB color space. Results include both
visual annotations and quantitative measurements.
+ The base Analysis class handles:
+ - Loading images from input directory
+ - Parallel processing via multiprocessing.Pool
+ - CPU core management
+
+ You implement:
+ - _preRun(): Setup before processing
+ - _processImage(): Process single image (runs in parallel)
+ - _postRun(): Save results after all images processed
+
Attributes:
- images (List[Image]): Loaded input images
+ images (List[Image]): Loaded input images (set by base class)
input_images (ImageListValue): Input directory parameter
output_images (ImageListValue): Output directory for annotated images
- output_results (MetaDataValue): Output directory for CSV results
lightness_threshold (IntValue): L-channel threshold for bruise detection
min_bruise_area (IntValue): Minimum area (pixels) for valid bruise
morphological_kernel (IntValue): Kernel size for noise removal
mask_alpha (FloatValue): Transparency for bruise mask overlay
- bruise_color (tuple): Color for bruise mask visualization (BGR)
"""
# This name will be used in CLI: granny -i cli --analysis bruise
@@ -89,12 +96,9 @@ File: ``Granny/Analyses/BruiseDetection.py``
Sets up input/output directories, detection thresholds, and
visualization parameters with sensible defaults.
"""
- # STEP 1: Initialize base class
+ # STEP 1: Initialize base class (REQUIRED)
super().__init__()
- # Initialize image list
- self.images: List[Image] = []
-
# STEP 2: Set up input parameter
self.input_images = ImageListValue(
"input",
@@ -198,45 +202,35 @@ File: ``Granny/Analyses/BruiseDetection.py``
self.font_scale.setIsRequired(False)
self.addInParam(self.font_scale)
- self.text_thickness = IntValue(
- "text_thick",
- "text_thickness",
- "Thickness of text annotations in pixels. "
- "Range: 1-50, default: 2."
- )
- self.text_thickness.setMin(1)
- self.text_thickness.setMax(50)
- self.text_thickness.setValue(2)
- self.text_thickness.setIsRequired(False)
- self.addInParam(self.text_thickness)
-
# Bruise mask color (BGR format for OpenCV)
self.bruise_color = (0, 0, 255) # Red
- def performAnalysis(self) -> List[Image]:
+ # =========================================================================
+ # REQUIRED ABSTRACT METHODS
+ # =========================================================================
+
+ def _preRun(self):
"""
- Perform bruise detection on all input images.
+ Setup before image processing begins.
- Workflow:
- 1. Load images from input directory
- 2. Process each image to detect bruises
- 3. Create annotated output images
- 4. Save results to disk (images and CSV)
- 5. Return processed images
+ Called once by performAnalysis() before parallel processing starts.
+ self.images is already populated with loaded Image objects.
- Returns:
- List[Image]: List of processed Image objects with bruise annotations
+ Use this for:
+ - Getting parameter values
+ - Initializing output directory
+ - Printing analysis info
"""
- # STEP 1: Get parameter values
- input_dir = self.input_images.getValue()
- output_dir = self.output_images.getValue()
- results_dir = self.output_results.getValue()
+ # Get output directory path
+ self.output_dir = self.output_images.getValue()
+ self.results_dir = self.output_results.getValue()
+ # Print analysis info
print(f"\n{'='*60}")
print(f"BRUISE DETECTION ANALYSIS")
print(f"{'='*60}")
- print(f"Input directory: {input_dir}")
- print(f"Output directory: {output_dir}")
+ print(f"Input directory: {self.input_images.getValue()}")
+ print(f"Output directory: {self.output_dir}")
print(f"\nAnalysis Parameters:")
print(f" Lightness threshold: {self.lightness_threshold.getValue()}")
print(f" Min bruise area: {self.min_bruise_area.getValue()} pixels")
@@ -244,74 +238,123 @@ File: ``Granny/Analyses/BruiseDetection.py``
print(f"\nVisualization Parameters:")
print(f" Mask alpha: {self.mask_alpha.getValue()}")
print(f" Font scale: {self.font_scale.getValue()}")
+ print(f"\nProcessing {len(self.images)} images...")
print(f"{'='*60}\n")
- # STEP 2: Load images
- print(f"Loading images from: {input_dir}")
- imageIO = ImageIO()
- self.images = imageIO.load(input_dir, RGBImageFile)
-
- if not self.images:
- print("ERROR: No images found in input directory.")
- return []
-
- print(f"Found {len(self.images)} images to process.\n")
-
- # STEP 3: Process each image
- result_images = []
- results_data = []
-
- for idx, image in enumerate(self.images, 1):
- filename = image.getFileName()
- print(f"Processing {idx}/{len(self.images)}: {filename}")
-
- # Get the image as NumPy array (BGR format from OpenCV)
- img_array = image.getImageFile().getImage()
-
- # Perform bruise detection
- annotated_img, bruise_pct, bruise_count = self._detect_bruises(img_array)
-
- print(f" -> Bruise coverage: {bruise_pct:.2f}%")
- print(f" -> Bruise regions: {bruise_count}")
-
- # Create result image
- result_image = RGBImage()
- result_file = RGBImageFile()
- result_file.setImage(annotated_img)
- result_file.setFileName(filename)
- result_file.setFilePath(output_dir)
- result_image.setImageFile(result_file)
-
- # Add metadata
- result_image.addMetadata(self.metadata) # Standard metadata
- result_image.addMetadata([
- {"name": "bruise_percentage", "value": bruise_pct},
- {"name": "bruise_count", "value": bruise_count},
- {"name": "lightness_threshold", "value": self.lightness_threshold.getValue()}
- ])
-
- result_images.append(result_image)
- results_data.append({
- "filename": filename,
- "bruise_percentage": f"{bruise_pct:.2f}",
- "bruise_count": bruise_count,
- "lightness_threshold": self.lightness_threshold.getValue()
+ def _processImage(self, image: Image) -> Image:
+ """
+ Process a single image for bruise detection.
+
+ This method runs in PARALLEL across multiple CPU cores.
+ Each call receives one Image and must return the processed Image.
+
+ IMPORTANT: Don't modify shared state here - it won't work
+ with multiprocessing. Return all results via the Image object.
+
+ Args:
+ image: Input Image instance (filepath set, image not loaded)
+
+ Returns:
+ Image: The same Image with processed data and metadata
+ """
+ # Get parameter values (safe - these are read-only)
+ l_thresh = self.lightness_threshold.getValue()
+ min_area = self.min_bruise_area.getValue()
+ kernel_size = self.morphological_kernel.getValue()
+ alpha = self.mask_alpha.getValue()
+ font_scale = self.font_scale.getValue()
+
+ # Load the image data
+ image_io = RGBImageFile()
+ image_io.setFilePath(image.getFilePath())
+ image.loadImage(image_io)
+
+ # Get the numpy array (BGR format from OpenCV)
+ img_array = image.getImage()
+
+ # Perform bruise detection
+ result_array, bruise_pct, bruise_count = self._detect_bruises(
+ img_array, l_thresh, min_area, kernel_size, alpha, font_scale
+ )
+
+ # Update the image with processed result
+ image.setImage(result_array)
+
+ # Add metadata to the image (will be collected in _postRun)
+ bruise_pct_val = FloatValue("bruise_percentage", "bruise_pct", "Bruise percentage")
+ bruise_pct_val.setValue(bruise_pct)
+ image.addValue(bruise_pct_val)
+
+ bruise_count_val = IntValue("bruise_count", "bruise_count", "Number of bruise regions")
+ bruise_count_val.setValue(bruise_count)
+ image.addValue(bruise_count_val)
+
+ threshold_val = IntValue("threshold_used", "threshold", "L threshold used")
+ threshold_val.setValue(l_thresh)
+ image.addValue(threshold_val)
+
+ return image
+
+ def _postRun(self, results: List[Image]) -> List[Image]:
+ """
+ Post-processing after all images are processed.
+
+ Called once with all processed Image objects.
+ Use this to save images, generate CSV reports, print summaries.
+
+ Args:
+ results: List of processed Image objects from _processImage()
+
+ Returns:
+ List[Image]: The final list of result images
+ """
+ print(f"\nSaving {len(results)} images to: {self.output_dir}")
+
+ # Ensure output directory exists
+ os.makedirs(self.output_dir, exist_ok=True)
+
+ # Save each image and collect CSV data
+ image_io = RGBImageFile()
+ csv_data = []
+
+ for image in results:
+ # Save the image
+ image.saveImage(image_io, self.output_dir)
+
+ # Collect data for CSV
+ metadata = image.getMetaData()
+ csv_data.append({
+ "filename": image.getImageName(),
+ "bruise_percentage": f"{metadata['bruise_percentage'].getValue():.2f}",
+ "bruise_count": metadata['bruise_count'].getValue(),
+ "lightness_threshold": metadata['threshold_used'].getValue()
})
- # STEP 4: Save results
- print(f"\nSaving annotated images to: {output_dir}")
- imageIO.save(result_images)
+ print(f" Saved: {image.getImageName()} - "
+ f"Bruise: {metadata['bruise_percentage'].getValue():.2f}%")
- print(f"Saving CSV results to: {results_dir}")
- self._save_csv(results_data, results_dir)
+ # Save CSV results
+ self._save_csv(csv_data, self.results_dir)
print(f"\n{'='*60}")
- print(f"Analysis complete! Processed {len(result_images)} images.")
+ print(f"Analysis complete! Processed {len(results)} images.")
print(f"{'='*60}\n")
- return result_images
+ return results
- def _detect_bruises(self, img: NDArray) -> Tuple[NDArray, float, int]:
+ # =========================================================================
+ # HELPER METHODS
+ # =========================================================================
+
+ def _detect_bruises(
+ self,
+ img: NDArray,
+ l_thresh: int,
+ min_area: int,
+ kernel_size: int,
+ alpha: float,
+ font_scale: float
+ ) -> Tuple[NDArray, float, int]:
"""
Detect bruises on a single fruit image.
@@ -325,16 +368,15 @@ File: ``Granny/Analyses/BruiseDetection.py``
Args:
img: Input image as NumPy array (BGR format)
+ l_thresh: Lightness threshold
+ min_area: Minimum bruise area in pixels
+ kernel_size: Morphological kernel size
+ alpha: Mask transparency
+ font_scale: Text font scale
Returns:
Tuple of (annotated_image, bruise_percentage, bruise_count)
"""
- # Get parameter values
- l_thresh = self.lightness_threshold.getValue()
- min_area = self.min_bruise_area.getValue()
- kernel_size = self.morphological_kernel.getValue()
- alpha = self.mask_alpha.getValue()
-
# Convert to LAB color space
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l_channel, a_channel, b_channel = cv2.split(lab)
@@ -370,7 +412,7 @@ File: ``Granny/Analyses/BruiseDetection.py``
# Create visualization
annotated = self._create_visualization(
- img, bruise_mask, bruise_pct, bruise_count, alpha
+ img, bruise_mask, bruise_pct, bruise_count, alpha, font_scale
)
return annotated, bruise_pct, bruise_count
@@ -381,7 +423,8 @@ File: ``Granny/Analyses/BruiseDetection.py``
bruise_mask: NDArray,
bruise_pct: float,
bruise_count: int,
- alpha: float
+ alpha: float,
+ font_scale: float
) -> NDArray:
"""
Create annotated visualization of bruise detection results.
@@ -392,6 +435,7 @@ File: ``Granny/Analyses/BruiseDetection.py``
bruise_pct: Bruise percentage
bruise_count: Number of bruise regions
alpha: Transparency for mask overlay
+ font_scale: Font scale for text
Returns:
Annotated image
@@ -404,8 +448,7 @@ File: ``Granny/Analyses/BruiseDetection.py``
result = cv2.addWeighted(img, 1.0, mask_color, alpha, 0)
# Add text annotations
- font_scale = self.font_scale.getValue()
- thickness = self.text_thickness.getValue()
+ thickness = 2
font = cv2.FONT_HERSHEY_SIMPLEX
# Bruise percentage
@@ -444,22 +487,15 @@ File: ``Granny/Analyses/BruiseDetection.py``
writer.writeheader()
writer.writerows(data)
- print(f" -> CSV saved: {csv_path}")
+ print(f"Results saved to: {csv_path}")
+
Integration Steps
-----------------
After creating the file, follow these steps to integrate it into Granny:
-1. **Update Analysis Imports**
-
- Edit ``Granny/Analyses/__init__.py``:
-
- .. code-block:: python
-
- from .BruiseDetection import BruiseDetection
-
-2. **Update CLI Interface**
+1. **Update CLI Interface**
Edit ``Granny/Interfaces/UI/GrannyCLI.py``:
@@ -475,7 +511,7 @@ After creating the file, follow these steps to integrate it into Granny:
choices=["segmentation", "blush", "color", "scald", "starch", "bruise"]
-3. **Create Test File**
+2. **Create Test File**
Create ``tests/test_Analyses/test_BruiseDetection.py``:
@@ -547,11 +583,13 @@ Usage Examples
--mask_alpha 0.7 \
--font_scale 1.5
-**Check available parameters:**
+**Specify CPU cores:**
.. code-block:: bash
- granny -i cli --analysis bruise --help
+ granny -i cli --analysis bruise \
+ --input ./images/ \
+ --cpu 4
Running Tests
-------------
@@ -570,18 +608,40 @@ Key Takeaways
This example demonstrates:
1. **Proper class structure** with inheritance from ``Analysis``
-2. **Complete parameter setup** including both analysis and visualization parameters
-3. **Type-safe parameters** using IntValue, FloatValue, ImageListValue
-4. **Parameter constraints** with min/max ranges
-5. **Comprehensive docstrings** for class and methods
-6. **Image processing workflow** using OpenCV and NumPy
-7. **Result storage** for both images and CSV data
-8. **Metadata handling** for result images
-9. **User feedback** via print statements
-10. **Clean code organization** with helper methods
-
-Best Practices Demonstrated
-----------------------------
+2. **Three abstract methods**: ``_preRun()``, ``_processImage()``, ``_postRun()``
+3. **Parallel processing** via the base class (no manual Pool management)
+4. **Type-safe parameters** using IntValue, FloatValue, ImageListValue
+5. **Parameter constraints** with min/max ranges
+6. **Comprehensive docstrings** for class and methods
+7. **Image processing workflow** using OpenCV and NumPy
+8. **Result storage** for both images and CSV data
+9. **Metadata handling** via ``addValue()`` on Image objects
+10. **User feedback** via print statements
+
+Architecture Notes
+------------------
+
+**Why three methods instead of one performAnalysis()?**
+
+The base ``Analysis.performAnalysis()`` handles:
+
+- Loading images from the input directory
+- Managing the multiprocessing Pool
+- CPU core allocation (80% default, or user-specified)
+- Calling your methods in the right order
+
+This means you get parallel processing for free just by implementing the three methods correctly.
+
+**Multiprocessing constraints in _processImage():**
+
+Since ``_processImage()`` runs in separate processes:
+
+- Don't modify instance variables (changes won't propagate)
+- Return results via the Image object's metadata
+- Aggregation happens in ``_postRun()``
+- Parameter values can be read (they're pickled with the object)
+
+**Best Practices Demonstrated:**
- **Separation of concerns:** Detection logic separate from visualization
- **Configurable parameters:** All magic numbers are parameters
@@ -599,6 +659,5 @@ Next Steps
- Modify this example for your specific use case
- Add additional parameters as needed
- Implement more sophisticated algorithms
-- Add multiprocessing for better performance
- Create unit tests for your specific analysis
- Update documentation with your analysis details
diff --git a/docs/users_guide/installation.rst b/docs/users_guide/installation.rst
index e74178e..6c6d0db 100644
--- a/docs/users_guide/installation.rst
+++ b/docs/users_guide/installation.rst
@@ -1,6 +1,12 @@
Installation
============
-Granny was built using Python3 and can be run on any modern operating system including Windows, variants of Linux and OS X. Currently to install Granny, you must use the command-line interface. Follow the installation instructions that match the operating system you are using.
+Granny was built using Python3 and can be run on any modern operating system including variants of Linux, OS X, and potentially Windows. Currently to install Granny, you must use the command-line interface. Follow the installation instructions that match the operating system you are using.
+
+Requirements
+-----------
+Granny requires Python verison 3.9 or greater. It also requires several python packages such as ultralytics, numpy, and opencv-python but these packages will be installed automatically when Granny is installed.
+
+Please see the `Python instructions for downloading `_ and installing Python. Alternatively, you can install `Anaconda `_ which is a data analytics platform which includes Python.
Opening the terminal
@@ -18,13 +24,6 @@ Search for "terminal" in the search box that appears. Click the icon with the t
Within the terminal you can type the commands to install Granny.
-
-Windows Terminal
-````````````````
-.. note::
-
- Instructions will be added here.
-
Mac OS X Terminal
`````````````````
@@ -33,6 +32,14 @@ Mac OS X Terminal
Follow the same instructions as Linux: Ubuntu 22.04.
Install the Most Recent Release
+
+Windows Terminal
+````````````````
+.. warning::
+
+ We do not currently support Granny for Windows.
+
+
-------------------------------
Ubuntu 22.04 Installation
@@ -51,14 +58,6 @@ You will see output printed to the terminal as the installation progresses. If s
Successfully installed granny-1.0a1
-Windows Installation
-`````````````````````
-
-.. note::
-
- Instructions will be added here.
-
-
Mac OS X Installation
`````````````````````
@@ -76,6 +75,15 @@ You will see output printed to the terminal as the installation progresses. If s
Successfully installed granny-1.0a1
+Windows Installation
+`````````````````````
+
+.. warning::
+
+ There is currently a dependency missing for Granny to run in Windows and we encourage users of Granny to use a Linux or OS X computer to use Granny. You are welcome to try Granny on Windows but we will not currently offer support for this operating system.
+
+
+
Install the Development Version
-------------------------------
For most users, the development version of Granny should not be used. This is where leading-edge code is housed before it gets added to a release version. However, if you would like to explore using the development version, perhaps to test new features, you can install following these instructions.