Skip to content

Conversation

@ilyushkin
Copy link
Contributor

  • 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.

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

📝 Walkthrough

Walkthrough

Three files are modified to integrate Cryo feature reference images into the FibsEM milling workflow. feature.py corrects a docstring typo. fibsemos.py undergoes significant refactoring: the FibsemOSMillingTaskManager class now accepts and stores a feature parameter, loads its reference image, computes a pixel-precise crop based on stage alignment, and passes the cropped reference image to the milling stages. millmng.py removes the alignment step invocation during milling and makes filename generation conditional—omitting it when the fibsemos backend is installed, avoiding redundant computation.

Sequence Diagram

sequenceDiagram
    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
Loading

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: passing the correct reference image to fibsemOS, which aligns with the PR's primary objective of ensuring proper reference image handling in the milling workflow.
Description check ✅ Passed The description is directly related to the changeset, detailing three key aspects of the changes: passing the reference image, cropping requirements, and skipping alignment in Odemis, all of which are reflected in the code modifications.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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: Missing feature parameter in __init__ signature.

The docstring at line 217 documents a feature parameter, and line 230 assigns self.feature = feature, but feature is not declared in the method signature. This will cause a NameError at 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: Missing feature parameter in run_milling_tasks_fibsemos signature.

The docstring at line 319 documents a feature parameter, but it's not in the function signature (lines 314-315), and it's not passed to FibsemOSMillingTaskManager at 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, and MicroscopeState are 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

📥 Commits

Reviewing files that changed from the base of the PR and between ed4659c and 2984f3c.

📒 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".

Comment on lines +264 to +287
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)
Copy link

@coderabbitai coderabbitai bot Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Copy link
Member

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.

Copy link

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)

Comment on lines 421 to 427
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +264 to +285
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]
Copy link
Contributor

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.

Comment on lines +264 to +287
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)
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants