4. Working with GridImage: Grid-Specific Features#
This tutorial focuses on GridImage - a specialized class for analyzing arrayed microbe colonies on solid media agar plates.
Prerequisites#
Before starting this tutorial, please complete the {doc} Image to understand the core Image class features (rgb, gray, enh_gray, objmap, objects, etc.).
What Makes GridImage Special?#
GridImage extends Image with grid-specific functionality:
Automatic grid alignment to detected colonies
Grid position tracking (row, column, section)
Grid-based visualization
Position information in measurements
When to Use GridImage#
✅ Use GridImage for: 96-well plates, 384-well plates, any arrayed format
❌ Use regular Image for: Single colonies, random arrangements
Let’s explore the grid-specific features!
Setup: Import Libraries#
First, let’s import the phenotypic library (commonly abbreviated as pht).
[ ]:
import phenotypic as pht
import numpy as np
import matplotlib.pyplot as plt
Part 2: Grid-Specific Components#
Now let’s explore the components that are unique to GridImage:
Grid Components#
``grid_finder``: The algorithm that determines where grid lines should be placed
Default:
AutoGridFinder- automatically aligns to detected coloniesAlternative:
ManualGridFinder- for manual grid specification
``grid``: An accessor object that provides grid-specific operations
Access grid information
Get individual grid sections
Visualize rows and columns
``nrows`` and ``ncols``: Grid dimensions (can be adjusted)
``show_overlay()``: Enhanced visualization with gridlines and labels
[ ]:
image = pht.data.load_imager_plate(mode="GridImage")
image.name = "MyGridImage"
image.show()
(<Figure size 800x600 with 1 Axes>, <Axes: >)
[ ]:
image = image[390:3675, 425:5620] # Cropping a GridImage returns a regular Image
print(f"Cropped type: {type(image)}")
image = pht.GridImage(image)
print(f"Converted back: {type(image)}")
image.show()
Cropped type: <class 'phenotypic.core._image.Image'>
Converted back: <class 'phenotypic.core._grid_image.GridImage'>
(<Figure size 800x600 with 1 Axes>, <Axes: >)
[ ]:
# Examine grid properties
print(f"Grid finder type: {type(image.grid_finder).__name__}")
print(f"Number of rows: {image.nrows}")
print(f"Number of columns: {image.ncols}")
print(f"Total sections: {image.nrows*image.ncols}")
Grid finder type: AutoGridFinder
Number of rows: 8
Number of columns: 12
Total sections: 96
[ ]:
pht.detect.RoundPeaksDetector().apply(image, inplace=True)
# The S&P imager uses plates that have a corner cutthing through the image, so we remove it with BorderObjectRemover
pht.refine.BorderObjectRemover(100).apply(image, inplace=True)
image.show_overlay()
Notice how:
Cyan dashed lines show the grid boundaries
Column numbers appear at the top (0-11)
Row numbers appear on the right (0-7)
Colored boxes show which colonies belong to which grid section
Part 4: Accessing Grid Information#
After detection, the grid is automatically aligned to the colonies. We can access detailed grid assignment information using grid.info().
[6]:
# Get grid information for all colonies
grid_info = image.grid.info(include_metadata=True)
# Display first 10 colonies
print("Grid Information for Detected Colonies:")
print()
grid_info.head(10)
Grid Information for Detected Colonies:
[6]:
| Metadata_BitDepth | Metadata_ImageType | Metadata_ImageName | ObjectLabel | Bbox_CenterRR | Bbox_CenterCC | Bbox_MinRR | Bbox_MinCC | Bbox_MaxRR | Bbox_MaxCC | Grid_RowNum | Grid_ColNum | Grid_SectionNum | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 1 | 203.223483 | 3599.114734 | 133 | 3534 | 277 | 3666 | 0 | 8 | 8 |
| 1 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 2 | 208.305812 | 2791.626799 | 134 | 2722 | 283 | 2865 | 0 | 6 | 6 |
| 2 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 3 | 201.547514 | 4404.335588 | 135 | 4345 | 266 | 4467 | 0 | 10 | 10 |
| 3 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 4 | 205.583136 | 4001.737503 | 138 | 3941 | 274 | 4064 | 0 | 9 | 9 |
| 4 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 5 | 209.897206 | 2384.582457 | 140 | 2318 | 280 | 2451 | 0 | 5 | 5 |
| 5 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 6 | 219.215315 | 767.650918 | 147 | 698 | 291 | 838 | 0 | 1 | 1 |
| 6 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 7 | 209.147074 | 4809.871436 | 148 | 4761 | 268 | 4863 | 0 | 11 | 11 |
| 7 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 8 | 214.015541 | 1172.998471 | 152 | 1117 | 279 | 1234 | 0 | 2 | 2 |
| 8 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 9 | 223.262739 | 367.155403 | 161 | 314 | 279 | 424 | 0 | 0 | 0 |
| 9 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 10 | 614.416882 | 2791.059955 | 539 | 2716 | 689 | 2866 | 1 | 6 | 18 |
Understanding Grid Columns#
The grid information includes several important columns:
Position Information:#
``RowNum``: Which row the colony is in (0 to nrows-1)
``ColNum``: Which column the colony is in (0 to ncols-1)
``SectionNum``: Unique section ID (numbered left-to-right, top-to-bottom)
Bounding Box Coordinates:#
``MinRowCoord``, ``MaxRowCoord``: Minimum and maximum row coordinates
``MinColCoord``, ``MaxColCoord``: Minimum and maximum column coordinates
``CenterRowCoord``, ``CenterColCoord``: Center coordinates of the colony
Metadata:#
``Metadata_ImageName``: Name of the image
``Metadata_ImageType``: Type designation (GridImage)
[7]:
# Example: Find all colonies in row 3
row_3_colonies = grid_info[grid_info['Grid_RowNum'] == 3]
print(f"Colonies in row 3: {len(row_3_colonies)}")
print()
row_3_colonies[['Grid_RowNum', 'Grid_ColNum', 'Grid_SectionNum']]
Colonies in row 3: 9
[7]:
| Grid_RowNum | Grid_ColNum | Grid_SectionNum | |
|---|---|---|---|
| 29 | 3 | 10 | 46 |
| 30 | 3 | 4 | 40 |
| 31 | 3 | 11 | 47 |
| 32 | 3 | 5 | 41 |
| 33 | 3 | 7 | 43 |
| 34 | 3 | 2 | 38 |
| 35 | 3 | 0 | 36 |
| 36 | 3 | 3 | 39 |
| 37 | 3 | 6 | 42 |
[8]:
# Example: Find colonies in a specific section (e.g., section 25)
section_25 = grid_info[grid_info['Grid_SectionNum'] == 25]
if len(section_25) > 0:
print(
f"Section 25 corresponds to row {section_25['Grid_RowNum'].iloc[0]}, column {section_25['Grid_ColNum'].iloc[0]}")
print(f"Number of colonies detected: {len(section_25)}")
else:
print("No colonies detected in section 25")
Section 25 corresponds to row 2, column 1
Number of colonies detected: 1
Part 5: Working with Grid Sections#
You can extract individual grid sections (wells) from the plate. Sections are numbered from 0 to (nrows × ncols - 1), going left-to-right, top-to-bottom.
[9]:
# Access the first grid section (top-left)
section_0 = image.grid[0]
print(f"Section 0 shape: {section_0.shape}")
print(f"Number of objects in section 0: {section_0.num_objects}")
Section 0 shape: (404, 404, 3)
Number of objects in section 0: 0
[10]:
# Visualize a single section
fig, ax = section_0.show_overlay()
plt.title("Grid Section 0 (Top-Left Well)")
plt.show()
[11]:
# Compare multiple sections
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.ravel()
sections_to_show = [0, 1, 2, 12, 13, 14] # First two rows, first 3 columns
for idx, section_num in enumerate(sections_to_show):
section = image.grid[section_num] # Access using flat index
row = section_num//image.ncols
col = section_num%image.ncols
section.show(ax=axes[idx])
axes[idx].set_title(f"Section {section_num}\n(Row {row}, Col {col})")
axes[idx].axis('off')
plt.tight_layout()
plt.show()
[ ]:
# Compare multiple sections
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.ravel()
# Access using (row, col) tuple
sections_to_show = [
(0, 0),
(0, 1),
(0, 2),
(1, 9),
(1, 10),
(1, 11),
]
for idx, section_num in enumerate(sections_to_show):
section = image.grid[section_num]
section.show(ax=axes[idx])
axes[idx].set_title(f"Section {section_num}\n(Row {section_num[0]}, Col {section_num[1]})")
axes[idx].axis('off')
plt.tight_layout()
plt.show()
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[22], line 15
6 sections_to_show = [
7 (0, 0),
8 (0, 1),
(...) 12 (1, 13),
13 ]
14 for idx, section_num in enumerate(sections_to_show):
---> 15 section = image.grid[section_num]
17 section.show(ax=axes[idx])
18 axes[idx].set_title(f"Section {section_num}\n(Row {section_num[0]}, Col {section_num[1]})")
File ~/Projects/PhenoTypic/src/phenotypic/core/_image_parts/accessors/_grid_accessor.py:236, in GridAccessor.__getitem__(self, idx)
234 row_idx, col_idx = idx
235 # This will naturally raise IndexError for out-of-range indices
--> 236 idx = int(self._idx_ref_matrix[row_idx, col_idx])
238 if self._root_image.objects.num_objects != 0:
239 min_coords, max_coords = self._adv_get_grid_section_slices(idx)
IndexError: index 12 is out of bounds for axis 1 with size 12
Part 6: Advanced Grid Visualization#
GridImage provides specialized visualization methods to highlight rows and columns.
Visualizing by Row#
Color colonies according to which row they belong to:
[12]:
# Show colonies colored by row
fig, ax = image.grid.show_row_overlay(show_gridlines=True, figsize=(12, 10))
plt.title("Colonies Colored by Row")
plt.show()
Visualizing by Column#
Color colonies according to which column they belong to:
[13]:
# Show colonies colored by column
fig, ax = image.grid.show_column_overlay(show_gridlines=True, figsize=(12, 10))
plt.title("Colonies Colored by Column")
plt.show()
Accessing Grid Edges#
You can get the exact pixel coordinates where the grid lines are placed:
[14]:
# Get row and column edges
row_edges = image.grid.get_row_edges()
col_edges = image.grid.get_col_edges()
print(f"Row edges (pixel positions): {row_edges}")
print()
print(f"Column edges (pixel positions): {col_edges}")
print()
print(f"Number of row edges: {len(row_edges)} (should be nrows + 1 = {image.nrows + 1})")
print(f"Number of column edges: {len(col_edges)} (should be ncols + 1 = {image.ncols + 1})")
Row edges (pixel positions): [ 10 414 819 1223 1628 2032 2436 2841 3245]
Column edges (pixel positions): [ 165 569 973 1377 1781 2185 2589 2993 3397 3801 4205 4609 5013]
Number of row edges: 9 (should be nrows + 1 = 9)
Number of column edges: 13 (should be ncols + 1 = 13)
Part 8: Making Measurements with Grid Data#
Now let’s measure colony properties. The grid information is automatically included in the measurements!
[15]:
from phenotypic.measure import MeasureSize
# Create a size measurement module
size_measurer = MeasureSize()
# Measure all colonies
measurements = size_measurer.measure(image, include_meta=True)
print(f"Measured {len(measurements)} colonies")
print()
measurements.head(10)
Measured 78 colonies
[15]:
| Metadata_BitDepth | Metadata_ImageType | Metadata_ImageName | ObjectLabel | Bbox_CenterRR | Bbox_CenterCC | Bbox_MinRR | Bbox_MinCC | Bbox_MaxRR | Bbox_MaxCC | Grid_RowNum | Grid_ColNum | Grid_SectionNum | Size_Area | Size_IntegratedIntensity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 1 | 203.223483 | 3599.114734 | 133 | 3534 | 277 | 3666 | 0 | 8 | 8 | 14538.0 | 5859.603221 |
| 1 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 2 | 208.305812 | 2791.626799 | 134 | 2722 | 283 | 2865 | 0 | 6 | 6 | 16605.0 | 7481.397601 |
| 2 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 3 | 201.547514 | 4404.335588 | 135 | 4345 | 266 | 4467 | 0 | 10 | 10 | 12712.0 | 5459.275299 |
| 3 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 4 | 205.583136 | 4001.737503 | 138 | 3941 | 274 | 4064 | 0 | 9 | 9 | 12263.0 | 4876.491319 |
| 4 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 5 | 209.897206 | 2384.582457 | 140 | 2318 | 280 | 2451 | 0 | 5 | 5 | 14923.0 | 6453.527189 |
| 5 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 6 | 219.215315 | 767.650918 | 147 | 698 | 291 | 838 | 0 | 1 | 1 | 15684.0 | 6666.968775 |
| 6 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 7 | 209.147074 | 4809.871436 | 148 | 4761 | 268 | 4863 | 0 | 11 | 11 | 9995.0 | 3966.058251 |
| 7 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 8 | 214.015541 | 1172.998471 | 152 | 1117 | 279 | 1234 | 0 | 2 | 2 | 11775.0 | 4963.391140 |
| 8 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 9 | 223.262739 | 367.155403 | 161 | 314 | 279 | 424 | 0 | 0 | 0 | 8301.0 | 3091.764911 |
| 9 | 16 | GridImage | 6d85b9af-e79b-4acc-ac94-88597c1bef16 | 10 | 614.416882 | 2791.059955 | 539 | 2716 | 689 | 2866 | 1 | 6 | 18 | 17830.0 | 9547.184071 |
Understanding Measurement Columns#
The measurements include:
Size Measurements:
``Shape_Area``: Colony area in pixels
``Shape_EquivalentDiameter``: Diameter of a circle with the same area
``Shape_Perimeter``: Colony perimeter length
Grid Information (automatically included!):
``Grid_RowNum``, ``Grid_ColNum``, ``Grid_SectionNum``: Position in the grid
Metadata:
``Metadata_ImageName``, ``Metadata_ImageType``: Image information
[ ]:
# Analyze measurements by row
row_summary = measurements.groupby('Grid_RowNum')['Size_Area'].agg(['mean', 'std', 'count'])
row_summary.columns = ['Mean Area', 'Std Area', 'Colony Count']
print("Colony Area Summary by Row:")
print()
row_summary
Colony Area Summary by Row:
/var/folders/78/rnctrlmn5kj996kjnq_mmqlh0000gn/T/ipykernel_98842/2810716340.py:2: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
row_summary = measurements.groupby('Grid_RowNum')['Size_Area'].agg(['mean', 'std', 'count'])
| Mean Area | Std Area | Colony Count | |
|---|---|---|---|
| Grid_RowNum | |||
| 0 | 12977.333333 | 2727.086220 | 9 |
| 1 | 14087.800000 | 1777.789251 | 10 |
| 2 | 15287.500000 | 1942.377652 | 10 |
| 3 | 14422.555556 | 3032.566574 | 9 |
| 4 | 15037.555556 | 2906.213822 | 9 |
| 5 | 14758.900000 | 1228.125894 | 10 |
| 6 | 14557.222222 | 3701.339311 | 9 |
| 7 | 13627.250000 | 4767.139911 | 12 |
[17]:
# Visualize area distribution by row
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
measurements.boxplot(column='Size_Area', by='Grid_RowNum', ax=plt.gca())
plt.xlabel('Row Number')
plt.ylabel('Colony Area (pixels)')
plt.title('Colony Size Distribution by Row')
plt.suptitle('') # Remove default title
plt.tight_layout()
plt.show()
Exporting Data for Further Analysis#
You can easily export measurements to CSV for analysis in other programs (Excel, R, etc.)
[ ]:
# Export to CSV
# measurements.to_csv('colony_measurements_with_grid.csv')
# For this tutorial, let's just show what the export would look like
print("Example exported data structure:")
measurements[['Grid_RowNum', 'Grid_ColNum', 'Grid_SectionNum', 'Size_Area']].head()
Example exported data structure:
| Grid_RowNum | Grid_ColNum | Grid_SectionNum | Size_Area | |
|---|---|---|---|---|
| 0 | 0 | 8 | 8 | 14538.0 |
| 1 | 0 | 6 | 6 | 16605.0 |
| 2 | 0 | 10 | 10 | 12712.0 |
| 3 | 0 | 9 | 9 | 12263.0 |
| 4 | 0 | 5 | 5 | 14923.0 |
Part 9: Manual Grid Control (Advanced)#
In some cases, automatic grid alignment might not work perfectly. You can manually specify grid edges using ManualGridFinder.
This is useful when:
Colony detection is poor
Grid alignment is consistently off
You want precise control over grid placement
[19]:
from phenotypic.grid import ManualGridFinder
# Example: Create manual grid edges
# You would determine these by looking at your image
# Here we'll use evenly spaced edges as an example
image_height, image_width = image.shape[0], image.shape[1]
# Create evenly spaced row edges (9 edges for 8 rows)
manual_row_edges = np.linspace(0, image_height, image.nrows + 1).astype(int)
# Create evenly spaced column edges (13 edges for 12 columns)
manual_col_edges = np.linspace(0, image_width, image.ncols + 1).astype(int)
print(f"Manual row edges: {manual_row_edges}")
print(f"Manual column edges: {manual_col_edges}")
# Create a manual grid finder
manual_finder = ManualGridFinder(row_edges=manual_row_edges, col_edges=manual_col_edges)
print(f"\Manual grid finder created with {manual_finder.nrows} rows and {manual_finder.ncols} columns")
Manual row edges: [ 0 410 821 1231 1642 2053 2463 2874 3285]
Manual column edges: [ 0 432 865 1298 1731 2164 2597 3030 3463 3896 4329 4762 5195]
\Manual grid finder created with 8 rows and 12 columns
[20]:
# To use the manual grid finder, create a new GridImage with it:
# manual_image = pht.GridImage(plate_array, grid_finder=manual_finder)
# For this tutorial, we'll stick with automatic grid finding
print("Note: Manual grid finding is an advanced feature.")
print("The automatic AutoGridFinder works well for most cases!")
Note: Manual grid finding is an advanced feature.
The automatic AutoGridFinder works well for most cases!
Part 10: Alternative Loading Method#
You can also load images directly from files using GridImage.imread():
# Example of loading from a file path:
image = pht.GridImage.imread('path/to/your/plate_image.jpg', nrows=8, ncols=12)
# You can also specify other parameters:
image = pht.GridImage.imread(
'path/to/plate.jpg',
name='my_experiment',
nrows=16, # For 384-well plate
ncols=24
)
GridImage.imread() supports common formats: JPEG, PNG, TIFF
Summary and Key Takeaways#
What We Learned#
GridImage = Image + Grid Support
Inherits all regular Image features (rgb, gray, objmap, etc.)
Adds grid-specific functionality for arrayed colonies
Automatic Grid Alignment
Grid automatically aligns to detected colonies using
AutoGridFinderNo manual adjustment needed in most cases
Grid Information in Measurements
RowNum, ColNum, and SectionNum automatically included
Easy to analyze by position (row, column, or well)
Flexible Grid Access
Access entire plate:
imageAccess grid sections:
image.grid[0]Access individual colonies:
image.objects[0]
Typical Workflow#
# 1. Load image
image = pht.GridImage.imread('plate.jpg', nrows=8, ncols=12)
# 2. Enhance and detect
image = GaussianBlur(sigma=5).apply(image)
image = OtsuDetector().apply(image)
# 3. Visualize
image.show_overlay(show_gridlines=True)
# 4. Measure
measurements = MeasureSize().measure(image)
# 5. Export
measurements.to_csv('results.csv')
When to Use GridImage#
✅ Use GridImage when:
Colonies are in a regular array (96-well, 384-well, etc.)
You need to track position information
Analyzing replicate experiments
Comparing specific rows/columns/conditions
❌ Use regular Image when:
Single colonies or random arrangements
Position tracking not needed
Non-gridded experiments
Next Steps#
Try with your own plate images
Explore other measurement modules (MeasureShape, MeasureIntensity, MeasureColor)
Use ImagePipeline for complex workflows
Check out the growth curve tutorial for time series analysis
Additional Resources#
Documentation: Full API reference in the documentation
Other Examples: Check the
examples/folder for more notebooksHelp: Open an issue on GitHub for questions or bug reports
Happy analyzing! 🔬