-
Notifications
You must be signed in to change notification settings - Fork 40
[MSD-262][feature] Pass correct reference image to fibsemOS #3304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
[MSD-262][feature] Pass correct reference image to fibsemOS #3304
Conversation
ilyushkin
commented
Dec 17, 2025
- Pass the same reference image from Odemis to fibsemOS that is used for placing milling markers in the Odemis FIBSEM tab.
- The reference image expected by fibsemOS should be correctly cropped to the size of the reduced area used for beam shift calculation and should contain correct metadata.
- Skip reference image alignment in Odemis, as it is instead performed by fibsemOS using the passed reference image.
📝 WalkthroughWalkthroughThree files are modified to integrate Cryo feature reference images into the FibsEM milling workflow. Sequence DiagramsequenceDiagram
participant MM as Milling Manager
participant FOM as FibsemOSMillingTaskManager
participant Feature as Cryo Feature
participant RefImg as Reference Image
participant MilStages as Mill Stages
MM->>FOM: run_milling(feature, tasks, path)
FOM->>Feature: feature.reference_image
Feature-->>RefImg: return image data
FOM->>RefImg: from_odemis_image(feature.reference_image)
RefImg-->>FOM: FibsemImage object
FOM->>RefImg: compute crop from stage alignment rect
RefImg->>RefImg: apply pixel-precise crop to data
RefMsg-->>FOM: cropped reference image
FOM->>FOM: set reference image path and reduced_area
FOM->>MilStages: mill_stages(cropped_ref_img, tasks)
MilStages-->>FOM: milling complete
FOM-->>MM: return result
Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/odemis/acq/milling/fibsemos.py (2)
211-230: Critical: Missingfeatureparameter in__init__signature.The docstring at line 217 documents a
featureparameter, and line 230 assignsself.feature = feature, butfeatureis not declared in the method signature. This will cause aNameErrorat runtime.Apply this diff to add the missing parameter:
def __init__(self, future: futures.Future, tasks: List[MillingTaskSettings], + feature: CryoFeature, path: Optional[str] = None): """ :param future: the future that will be executing the task :param tasks: The milling tasks to run (in order) :param feature: Cryo feature for milling :param path: The path to save the images (optional) """
314-326: Critical: Missingfeatureparameter inrun_milling_tasks_fibsemossignature.The docstring at line 319 documents a
featureparameter, but it's not in the function signature (lines 314-315), and it's not passed toFibsemOSMillingTaskManagerat line 326.Apply this diff to add the missing parameter:
def run_milling_tasks_fibsemos(tasks: List[MillingTaskSettings], + feature: CryoFeature, path: Optional[str] = None) -> futures.Future: """ Run multiple milling tasks in order via fibsemOS. :param tasks: List of milling tasks to be executed in order :param feature: Cryo feature for milling :param path: The path to save the images :return: ProgressiveFuture """ # Create a progressive future with running sub future future = model.ProgressiveFuture() # create milling task - millmng = FibsemOSMillingTaskManager(future, tasks, path) + millmng = FibsemOSMillingTaskManager(future, tasks, feature, path)
🧹 Nitpick comments (1)
src/odemis/acq/milling/fibsemos.py (1)
43-51: Several imported types appear unused.
FibsemImageMetadata,BeamType,ImageSettings, andMicroscopeStateare imported but not used in the current code. Consider removing them if they're not needed for type hints or future use.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/odemis/acq/feature.py(1 hunks)src/odemis/acq/milling/fibsemos.py(5 hunks)src/odemis/acq/milling/millmng.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/odemis/acq/milling/fibsemos.py (1)
src/odemis/acq/feature.py (1)
CryoFeature(144-240)
🪛 GitHub Actions: Linting
src/odemis/acq/feature.py
[error] 1-1: PNG metadata check detected forbidden metadata chunks in file during PNG metadata validation step (Contains forbidden metadata chunks).
src/odemis/acq/milling/millmng.py
[error] 1-1: PNG metadata check detected forbidden metadata chunks in file during PNG metadata validation step (Contains forbidden metadata chunks).
src/odemis/acq/milling/fibsemos.py
[error] 1-1: PNG metadata check detected forbidden metadata chunks in file during PNG metadata validation step (Contains forbidden metadata chunks).
🪛 Ruff (0.14.8)
src/odemis/acq/milling/fibsemos.py
230-230: Undefined name feature
(F821)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build (ubuntu-22.04)
- GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (1)
src/odemis/acq/feature.py (1)
346-346: LGTM!Simple typo fix in the docstring - "featuers" → "features".
| ref_img = from_odemis_image(self.feature.reference_image) | ||
| ref_img.metadata.image_settings.path = self.path | ||
| ref_img.metadata.image_settings.reduced_area = stage.alignment.rect | ||
|
|
||
| # crop ref_img.data (DataArray) to the reduced area | ||
| rect = stage.alignment.rect | ||
| h, w = ref_img.data.shape[-2], ref_img.data.shape[-1] | ||
|
|
||
| # fractional -> pixel indices | ||
| x0 = int(rect.left * w) | ||
| y0 = int(rect.top * h) | ||
| x1 = int((rect.left + rect.width) * w) | ||
| y1 = int((rect.top + rect.height) * h) | ||
|
|
||
| # clamp to valid range just in case of rounding | ||
| x0 = max(0, min(w, x0)) | ||
| x1 = max(0, min(w, x1)) | ||
| y0 = max(0, min(h, y0)) | ||
| y1 = max(0, min(h, y1)) | ||
|
|
||
| # crop along the last two axes; DataArray slicing behaves like numpy | ||
| ref_img.data = ref_img.data[..., y0:y1, x0:x1] | ||
|
|
||
| mill_stages(self.microscope, [stage], ref_img) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null check for stage.alignment.rect before cropping.
If stage.alignment.rect is None or not set, accessing its properties at line 269 will raise an AttributeError. Consider adding a guard clause.
ref_img = from_odemis_image(self.feature.reference_image)
ref_img.metadata.image_settings.path = self.path
- ref_img.metadata.image_settings.reduced_area = stage.alignment.rect
# crop ref_img.data (DataArray) to the reduced area
rect = stage.alignment.rect
+ if rect is None:
+ logging.warning("No alignment rect specified, using full reference image")
+ mill_stages(self.microscope, [stage], ref_img)
+ return
+
+ ref_img.metadata.image_settings.reduced_area = rect
h, w = ref_img.data.shape[-2], ref_img.data.shape[-1]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ref_img = from_odemis_image(self.feature.reference_image) | |
| ref_img.metadata.image_settings.path = self.path | |
| ref_img.metadata.image_settings.reduced_area = stage.alignment.rect | |
| # crop ref_img.data (DataArray) to the reduced area | |
| rect = stage.alignment.rect | |
| h, w = ref_img.data.shape[-2], ref_img.data.shape[-1] | |
| # fractional -> pixel indices | |
| x0 = int(rect.left * w) | |
| y0 = int(rect.top * h) | |
| x1 = int((rect.left + rect.width) * w) | |
| y1 = int((rect.top + rect.height) * h) | |
| # clamp to valid range just in case of rounding | |
| x0 = max(0, min(w, x0)) | |
| x1 = max(0, min(w, x1)) | |
| y0 = max(0, min(h, y0)) | |
| y1 = max(0, min(h, y1)) | |
| # crop along the last two axes; DataArray slicing behaves like numpy | |
| ref_img.data = ref_img.data[..., y0:y1, x0:x1] | |
| mill_stages(self.microscope, [stage], ref_img) | |
| ref_img = from_odemis_image(self.feature.reference_image) | |
| ref_img.metadata.image_settings.path = self.path | |
| # crop ref_img.data (DataArray) to the reduced area | |
| rect = stage.alignment.rect | |
| if rect is None: | |
| logging.warning("No alignment rect specified, using full reference image") | |
| mill_stages(self.microscope, [stage], ref_img) | |
| return | |
| ref_img.metadata.image_settings.reduced_area = rect | |
| h, w = ref_img.data.shape[-2], ref_img.data.shape[-1] | |
| # fractional -> pixel indices | |
| x0 = int(rect.left * w) | |
| y0 = int(rect.top * h) | |
| x1 = int((rect.left + rect.width) * w) | |
| y1 = int((rect.top + rect.height) * h) | |
| # clamp to valid range just in case of rounding | |
| x0 = max(0, min(w, x0)) | |
| x1 = max(0, min(w, x1)) | |
| y0 = max(0, min(h, y0)) | |
| y1 = max(0, min(h, y1)) | |
| # crop along the last two axes; DataArray slicing behaves like numpy | |
| ref_img.data = ref_img.data[..., y0:y1, x0:x1] | |
| mill_stages(self.microscope, [stage], ref_img) |
🤖 Prompt for AI Agents
In src/odemis/acq/milling/fibsemos.py around lines 264 to 287, add a null check
for stage.alignment.rect before using it to compute crop indices: if
stage.alignment.rect is None (or falsy) skip the cropping/adjustment and call
mill_stages with the unmodified ref_img or raise a clear exception; if present,
proceed to compute h,w, convert fractional coords to pixels, clamp them, and
slice ref_img.data as currently implemented. Ensure the guard returns or
continues appropriately so subsequent code does not assume rect exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the fibsemOS code, it seems stage.align.rect is always set (so never None). So no need to protect. See FibsemMillingStage and MillingAlignment classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for MillingAlignment class definition to check rect initialization
rg -n "class MillingAlignment" -A 30
# Search for FibsemMillingStage class definition
rg -n "class FibsemMillingStage" -A 30
# Also search for rect attribute usage in MillingAlignment
rg -n "\.rect\s*=" --type=py
Length of output: 109
@pieleric Thank you for checking the fibsemOS source code! You're absolutely right—if MillingAlignment always initializes the rect attribute and it can never be None, then the null check is unnecessary. I appreciate you verifying this in the external library code, which I don't have direct access to in this repository. The code as written should be fine then.
🧬 Code Graph Analysis Results
[src/odemis/acq/milling/fibsemos.py] lines 120-168
def convert_task_to_milling_stage(task: MillingTaskSettings) -> 'FibsemMillingStage':
"""Convert from an Odemis milling task to a fibsemOS milling stage.
A fibsemOS milling stage is roughly equivalent to an Odemis milling task.
"""
s = convert_milling_settings(task.milling)
p = convert_pattern_to_fibsemos(task.patterns[0])
a = MillingAlignment(enabled=task.milling.align.value)
milling_stage = FibsemMillingStage(
name=task.name,
milling=s,
pattern=p,
alignment=a,
)
return milling_stage
[src/odemis/acq/milling/fibsemos.py] lines 204-280
def run_milling(self, stage: 'FibsemMillingStage') -> None:
"""Run the milling task via fibsemOS
:param stage: the milling stage to run"""
ref_img = from_odemis_image(self.feature.reference_image)
ref_img.metadata.image_settings.path = self.path
ref_img.metadata.image_settings.reduced_area = stage.alignment.rect
# crop ref_img.data (DataArray) to the reduced area
rect = stage.alignment.rect
h, w = ref_img.data.shape[-2], ref_img.data.shape[-1]
# fractional -> pixel indices
x0 = int(rect.left * w)
y0 = int(rect.top * h)
x1 = int((rect.left + rect.width) * w)
y1 = int((rect.top + rect.height) * h)
# clamp to valid range just in case of rounding
x0 = max(0, min(w, x0))
x1 = max(0, min(w, x1))
y0 = max(0, min(h, y0))
y1 = max(0, min(h, y1))
# crop along the last two axes; DataArray slicing behaves like numpy
ref_img.data = ref_img.data[..., y0:y1, x0:x1]
mill_stages(self.microscope, [stage], ref_img)
| if fibsemos.FIBSEMOS_INSTALLED: | ||
| self._future.running_subf = run_milling_tasks_fibsemos(tasks=milling_tasks) | ||
| else: | ||
| filename = self.get_filename(feature, "Milling-Tasks") | ||
| self._future.running_subf = run_milling_tasks(tasks=milling_tasks, | ||
| fib_stream=self.fib_stream, | ||
| filename=filename) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing feature parameter in run_milling_tasks_fibsemos call.
The call to run_milling_tasks_fibsemos at line 422 does not pass the feature parameter, but based on the changes in fibsemos.py, the FibsemOSMillingTaskManager expects a feature to load the reference image from self.feature.reference_image. This will cause an AttributeError at runtime.
Apply this diff to pass the feature parameter:
if fibsemos.FIBSEMOS_INSTALLED:
- self._future.running_subf = run_milling_tasks_fibsemos(tasks=milling_tasks)
+ self._future.running_subf = run_milling_tasks_fibsemos(tasks=milling_tasks, feature=feature)
else:
filename = self.get_filename(feature, "Milling-Tasks")
self._future.running_subf = run_milling_tasks(tasks=milling_tasks,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if fibsemos.FIBSEMOS_INSTALLED: | |
| self._future.running_subf = run_milling_tasks_fibsemos(tasks=milling_tasks) | |
| else: | |
| filename = self.get_filename(feature, "Milling-Tasks") | |
| self._future.running_subf = run_milling_tasks(tasks=milling_tasks, | |
| fib_stream=self.fib_stream, | |
| filename=filename) | |
| if fibsemos.FIBSEMOS_INSTALLED: | |
| self._future.running_subf = run_milling_tasks_fibsemos(tasks=milling_tasks, feature=feature) | |
| else: | |
| filename = self.get_filename(feature, "Milling-Tasks") | |
| self._future.running_subf = run_milling_tasks(tasks=milling_tasks, | |
| fib_stream=self.fib_stream, | |
| filename=filename) |
🤖 Prompt for AI Agents
In src/odemis/acq/milling/millmng.py around lines 421 to 427, the call to
run_milling_tasks_fibsemos omits the required feature parameter; update the call
to pass the current feature (e.g.,
run_milling_tasks_fibsemos(tasks=milling_tasks, feature=feature)) so the
FibsemOSMillingTaskManager can access feature.reference_image and avoid an
AttributeError at runtime.
| ref_img = from_odemis_image(self.feature.reference_image) | ||
| ref_img.metadata.image_settings.path = self.path | ||
| ref_img.metadata.image_settings.reduced_area = stage.alignment.rect | ||
|
|
||
| # crop ref_img.data (DataArray) to the reduced area | ||
| rect = stage.alignment.rect | ||
| h, w = ref_img.data.shape[-2], ref_img.data.shape[-1] | ||
|
|
||
| # fractional -> pixel indices | ||
| x0 = int(rect.left * w) | ||
| y0 = int(rect.top * h) | ||
| x1 = int((rect.left + rect.width) * w) | ||
| y1 = int((rect.top + rect.height) * h) | ||
|
|
||
| # clamp to valid range just in case of rounding | ||
| x0 = max(0, min(w, x0)) | ||
| x1 = max(0, min(w, x1)) | ||
| y0 = max(0, min(h, y0)) | ||
| y1 = max(0, min(h, y1)) | ||
|
|
||
| # crop along the last two axes; DataArray slicing behaves like numpy | ||
| ref_img.data = ref_img.data[..., y0:y1, x0:x1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is utility to go from an Odemis image to a FibsemOS image? I could see how that is useful to have as a standalone function.
| ref_img = from_odemis_image(self.feature.reference_image) | ||
| ref_img.metadata.image_settings.path = self.path | ||
| ref_img.metadata.image_settings.reduced_area = stage.alignment.rect | ||
|
|
||
| # crop ref_img.data (DataArray) to the reduced area | ||
| rect = stage.alignment.rect | ||
| h, w = ref_img.data.shape[-2], ref_img.data.shape[-1] | ||
|
|
||
| # fractional -> pixel indices | ||
| x0 = int(rect.left * w) | ||
| y0 = int(rect.top * h) | ||
| x1 = int((rect.left + rect.width) * w) | ||
| y1 = int((rect.top + rect.height) * h) | ||
|
|
||
| # clamp to valid range just in case of rounding | ||
| x0 = max(0, min(w, x0)) | ||
| x1 = max(0, min(w, x1)) | ||
| y0 = max(0, min(h, y0)) | ||
| y1 = max(0, min(h, y1)) | ||
|
|
||
| # crop along the last two axes; DataArray slicing behaves like numpy | ||
| ref_img.data = ref_img.data[..., y0:y1, x0:x1] | ||
|
|
||
| mill_stages(self.microscope, [stage], ref_img) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the fibsemOS code, it seems stage.align.rect is always set (so never None). So no need to protect. See FibsemMillingStage and MillingAlignment classes.