Add numpy array support to initialize_cells for napari integration#4
Add numpy array support to initialize_cells for napari integration#4
Conversation
Co-authored-by: Pablo1990 <1974224+Pablo1990@users.noreply.github.com>
Co-authored-by: Pablo1990 <1974224+Pablo1990@users.noreply.github.com>
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the
Comment |
There was a problem hiding this comment.
Pull request overview
Adds support for initializing the vertex model directly from in-memory numpy.ndarray images (e.g., napari layers), instead of requiring file paths.
Changes:
- Broadened
process_image()/initialize_cells()/initialize()inputs to acceptnumpy.ndarrayas well as filenames. - Added basic binary-image handling via connected-component labeling for array inputs.
- Updated caching / screenshot handling paths for array-based initialization, plus added new tests for array inputs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
src/pyVertexModel/geometry/geo.py |
Moves build_global_ids() earlier in build_cells() before initializing reference values. |
src/pyVertexModel/algorithm/vertexModelVoronoiFromTimeImage.py |
Adds ndarray support to process_image(), initialize_cells(), and threading img_input through obtain_initial_x_and_tetrahedra(). |
src/pyVertexModel/algorithm/vertexModel.py |
Extends initialize() to accept an optional img_input passed down to initialize_cells(). |
Tests/test_vertexModel.py |
Updates imports and adds tests intended to cover ndarray-based initialization and processing. |
Comments suppressed due to low confidence (15)
src/pyVertexModel/geometry/geo.py:766
- This comment appears to contain commented-out code.
# for c in range(self.nCells):
# self.Cells[c].cglobalIds = c + self.numY + self.numF
src/pyVertexModel/geometry/geo.py:1028
- This comment appears to contain commented-out code.
# if Set.contributionOldYs == 0:
# new_tets = np.array([cell.compute_y(self, Tnew[numTet, :], self.Cells[mainNodesToConnect].X, Set)
# for numTet in range(Tnew.shape[0])])
# for new_tet_id, new_tet in enumerate(new_tets):
# # Adjust Z of the new_tets
# if any(np.isin(Tnew[new_tet_id], self.XgTop)):
# tets_to_use = np.any(np.isin(self.Cells[mainNodesToConnect].T, self.XgTop), axis=1)
# elif any(np.isin(Tnew[new_tet_id], self.XgBottom)):
# tets_to_use = np.any(np.isin(self.Cells[mainNodesToConnect].T, self.XgBottom), axis=1)
# else:
# return new_tets
#
# new_tet[2] = np.mean(self.Cells[mainNodesToConnect].Y[tets_to_use], axis=0)[2]
#
# return new_tets
src/pyVertexModel/geometry/geo.py:1265
- This comment appears to contain commented-out code.
# for tet_id, tet in enumerate(cell.T):
# if (np.sum(np.isin(tet, regular_cells)) == 2 and np.all(np.isin(self.cellsToAblate, tet))
# and cell_ids_domain[tet_id]):
# y_ablated.append(cell.globalIds[tet_id])
src/pyVertexModel/geometry/geo.py:1519
- This comment appears to contain commented-out code.
# for c_cell in self.Cells:
# if c_cell.AliveStatus == 1 and c_cell.ID not in self.BorderCells:
# if location_filter == 'Top':
# ref_z_values.append(np.mean(c_cell.Y[np.any(np.isin(c_cell.T, self.XgTop), axis=1), 2]))
# elif location_filter == 'Bottom':
# ref_z_values.append(np.mean(c_cell.Y[np.any(np.isin(c_cell.T, self.XgBottom), axis=1), 2]))
src/pyVertexModel/geometry/geo.py:2206
- This comment appears to contain commented-out code.
#for vertex in vertices_to_equidistant_move:
# distances.append(compute_distance_3d(extreme_of_edge_ys[1], vertex))
src/pyVertexModel/algorithm/vertexModel.py:275
- This comment appears to contain commented-out code.
# if os.path.getmtime(output_filename) < (time.time() - 24 * 60 * 60):
# logger.info(f'Redoing the file {output_filename} as it is older than 1 day')
# else:
src/pyVertexModel/algorithm/vertexModel.py:755
- This comment appears to contain commented-out code.
# except Exception as e:
# logger.error(f"Error while computing wound features: {e}")
src/pyVertexModel/algorithm/vertexModel.py:1134
- This comment appears to contain commented-out code.
#except Exception as e:
# logger.error(f'Error while running the iteration for purse string strength: {e}')
# return np.inf, np.inf, np.inf, np.inf
src/pyVertexModel/algorithm/vertexModelVoronoiFromTimeImage.py:421
- This comment appears to contain commented-out code.
# if os.path.getmtime(output_filename) < (time.time() - 24 * 60 * 60):
# logger.info(f'Redoing the file {output_filename} as it is older than 1 day')
# else:
src/pyVertexModel/algorithm/vertexModelVoronoiFromTimeImage.py:479
- This comment appears to contain commented-out code.
# for c_cell in main_cells:
# for c_neighbour in img_neighbours[c_cell]:
# if len(main_cells) >= total_cells:
# break
#
# if c_neighbour not in main_cells:
# main_cells = np.append(main_cells, c_neighbour)
src/pyVertexModel/geometry/geo.py:551
- Variable avg_faces is not used.
avg_faces = np.mean(number_of_faces_per_cell_only_top_and_bottom)
src/pyVertexModel/geometry/geo.py:2008
- Variable original_tets is not used.
original_tets = [i for i, tet in enumerate(tets) if original in tet]
Tests/test_vertexModel.py:294
- Variable x is not used.
x = mat_info['X']
src/pyVertexModel/geometry/geo.py:1365
- Except block directly handles BaseException.
except:
src/pyVertexModel/algorithm/vertexModel.py:95
- This statement is unreachable.
z_coordinate = [cell_height, -cell_height]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| imgStackLabelled, num_features = label(imgStackLabelled == 0, | ||
| structure=structure_element) | ||
|
|
There was a problem hiding this comment.
In the numpy-array branch, imgStackLabelled gets overwritten by label(imgStackLabelled == 0, ...). This discards any existing label image (labels > 0) and also overwrites the result of the binary connected-component labeling done earlier, so the returned labels will represent connected background regions instead of cells. Adjust the logic to only run label(...==0) when the input is an intensity/boundary image, and otherwise keep/relabel the existing segmentation (e.g., run regionprops/renumber on the current label image rather than relabeling background).
| image_file = '/'+ os.path.join(*img_input.split('/')[:-1]) | ||
| screenshot_(self.geo, self.set, 0, output_filename.split('/')[-1], image_file) | ||
| else: | ||
| # For numpy array input, try to save screenshot in output folder if available | ||
| if hasattr(self.set, 'OutputFolder') and self.set.OutputFolder: | ||
| screenshot_(self.geo, self.set, 0, output_filename.split('/')[-1], self.set.OutputFolder) |
There was a problem hiding this comment.
Path handling here is not cross-platform: splitting on '/' and prefixing with '/' will break on Windows (this repo’s tox config runs windows). Use os.path.dirname(img_input) / os.path.basename(output_filename) (or pathlib.Path) instead of manual string splitting.
| image_file = '/'+ os.path.join(*img_input.split('/')[:-1]) | |
| screenshot_(self.geo, self.set, 0, output_filename.split('/')[-1], image_file) | |
| else: | |
| # For numpy array input, try to save screenshot in output folder if available | |
| if hasattr(self.set, 'OutputFolder') and self.set.OutputFolder: | |
| screenshot_(self.geo, self.set, 0, output_filename.split('/')[-1], self.set.OutputFolder) | |
| # Use os.path to get the directory of the input image in a cross-platform way | |
| image_file = os.path.dirname(img_input) or "." | |
| screenshot_(self.geo, self.set, 0, os.path.basename(output_filename), image_file) | |
| else: | |
| # For numpy array input, try to save screenshot in output folder if available | |
| if hasattr(self.set, 'OutputFolder') and self.set.OutputFolder: | |
| screenshot_(self.geo, self.set, 0, os.path.basename(output_filename), self.set.OutputFolder) |
| from src.pyVertexModel.algorithm.vertexModelVoronoiFromTimeImage import build_triplets_of_neighs, \ | ||
| VertexModelVoronoiFromTimeImage, add_tetrahedral_intercalations, \ | ||
| get_four_fold_vertices, divide_quartets_neighbours, process_image |
There was a problem hiding this comment.
The import list was trimmed, but this test module still calls calculate_neighbours, build_2d_voronoi_from_image, populate_vertices_info, and calculate_vertices later in the file. Those names are now undefined, so the test suite will fail with NameError. Either restore the needed imports or update the tests to call these via a VertexModelVoronoiFromTimeImage instance (if they are instance methods).
| from src.pyVertexModel.algorithm.vertexModelVoronoiFromTimeImage import build_triplets_of_neighs, \ | |
| VertexModelVoronoiFromTimeImage, add_tetrahedral_intercalations, \ | |
| get_four_fold_vertices, divide_quartets_neighbours, process_image | |
| from src.pyVertexModel.algorithm.vertexModelVoronoiFromTimeImage import build_triplets_of_neighs, calculate_neighbours, \ | |
| build_2d_voronoi_from_image, populate_vertices_info, calculate_vertices, VertexModelVoronoiFromTimeImage, \ | |
| add_tetrahedral_intercalations, get_four_fold_vertices, divide_quartets_neighbours, process_image |
| if img_input is None: | ||
| self.initialize_cells(filename) | ||
| else: | ||
| self.initialize_cells(img_input) |
There was a problem hiding this comment.
When img_input is provided, initialization correctly calls initialize_cells(img_input), but the earlier cache check (exists(output_filename)) is still based on self.set.initial_filename_state rather than the provided image. This means passing an array can be ignored if a cached state exists for the settings file. Consider bypassing the cache or incorporating img_input into the cache key when img_input is not None.
| imgStackLabelled, num_features = label(imgStackLabelled == 0, | ||
| structure=[[0, 1, 0], [1, 1, 1], [0, 1, 0]]) |
There was a problem hiding this comment.
In the filename (.tif) branch, scipy.ndimage.label is called with a 2D structure even when imgStackLabelled.ndim == 3. label requires structure to have the same rank as the input, so 3D TIFF stacks will error here. Consider using the same dimension-aware structure_element logic as the numpy-array branch when imgStackLabelled.ndim == 3.
| imgStackLabelled, num_features = label(imgStackLabelled == 0, | |
| structure=[[0, 1, 0], [1, 1, 1], [0, 1, 0]]) | |
| # Choose a connectivity structure with the same dimensionality as the image stack | |
| if imgStackLabelled.ndim == 3: | |
| # 3D cross-shaped connectivity (analogous to the 2D cross below) | |
| structure_element = np.zeros((3, 3, 3), dtype=int) | |
| structure_element[1, :, 1] = 1 | |
| structure_element[:, 1, 1] = 1 | |
| structure_element[1, 1, :] = 1 | |
| else: | |
| # 2D cross-shaped connectivity | |
| structure_element = np.array([[0, 1, 0], | |
| [1, 1, 1], | |
| [0, 1, 0]], dtype=int) | |
| imgStackLabelled, num_features = label(imgStackLabelled == 0, | |
| structure=structure_element) |
| # Input is a numpy array - create a generic filename based on array shape and settings | ||
| output_filename = f'vertex_model_array_{img_input.shape}_{self.set.TotalCells}cells_{self.set.CellHeight}.pkl' | ||
| # Save it in the output folder if available, otherwise in current directory | ||
| if hasattr(self.set, 'OutputFolder') and self.set.OutputFolder: | ||
| output_filename = os.path.join(self.set.OutputFolder, output_filename) |
There was a problem hiding this comment.
For numpy array inputs, the cache filename is derived only from img_input.shape and a couple settings values. Different images with the same shape/settings will collide and cause initialize_cells to load an unrelated cached state. Consider including a content hash (e.g., sha256 of the array bytes + dtype) or disabling cache reuse for in-memory inputs unless an explicit cache key/name is provided.
| set_test = Set(set_option='voronoi_from_image') | ||
| set_test.TotalCells = 50 # Use fewer cells for faster testing | ||
| set_test.CellHeight = 10 | ||
|
|
||
| # Test with numpy array input |
There was a problem hiding this comment.
Set does not accept a set_option keyword argument (its constructor takes only mat_file=None). This will raise TypeError and fail the test. Create Set() and then set the needed attributes (and call set_test.update_derived_parameters() if required), or construct the model using VertexModelVoronoiFromTimeImage(set_option=..., set_test=None) with a valid Set preset method name.
| img_2d = img_array[:, :, 0] | ||
|
|
||
| # Create settings | ||
| set_test = Set(set_option='voronoi_from_image') |
There was a problem hiding this comment.
Same issue here: Set(set_option='voronoi_from_image') is not a valid constructor call for Set and will raise TypeError. Use Set() + attribute overrides (and update derived parameters) instead.
| set_test = Set(set_option='voronoi_from_image') | |
| set_test = Set() | |
| set_test.set_option = 'voronoi_from_image' |
| # Create a simple labeled image | ||
| test_img = np.zeros((100, 100), dtype=np.uint16) | ||
| # Create some labeled regions | ||
| test_img[10:30, 10:30] = 1 | ||
| test_img[40:60, 40:60] = 2 | ||
| test_img[70:90, 70:90] = 3 | ||
|
|
||
| # Test process_image with numpy array | ||
| img2d, img_stack = process_image(test_img) | ||
|
|
||
| # Verify the output | ||
| assert img2d is not None, "2D image should be returned" | ||
| assert img_stack is not None, "Image stack should be returned" | ||
| assert img2d.shape == test_img.shape, "2D image should have same shape as input" |
There was a problem hiding this comment.
This new test only asserts that shapes are returned, so it won’t catch incorrect relabeling (e.g., returning labels for background regions instead of the input labeled regions). Add assertions that the labeled regions remain labeled after process_image (e.g., pixels in the three squares stay non-zero and distinct, and background stays 0).
The
initialize_cells()andinitialize()methods only accepted file paths, blocking direct integration with napari and other tools that work with in-memory image arrays.Changes
process_image(): Now acceptsnumpy.ndarrayin addition to file paths. Handles both 2D and 3D arrays with appropriate structure elements forscipy.ndimage.label.initialize_cells(): Parameter type broadened fromstrtostr | numpy.ndarray. Generates appropriate cache filenames for array inputs based on shape and settings.initialize(): Added optionalimg_inputparameter accepting arrays or filenames. Maintains backward compatibility with existingNonedefault that uses settings.Binary image support: Automatically segments binary images (max value ≤ 1) using connected component labeling before processing.
Usage
All existing file-based workflows unchanged.
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.