Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Czaki
Copy link
Collaborator

@Czaki Czaki commented Oct 20, 2025

Summary by Sourcery

Add full support for reading and writing time scale (time_increment) metadata in ImageJ and OME TIFF images, enrich Image model with a validated time_increment property, and extend unit conversion to cover time units.

New Features:

  • Persist time spacing metadata when saving and reading TIFF images in both ImageJ and OME formats
  • Expose time_increment property on Image with validation

Enhancements:

  • Extend unit conversion dictionary with a full range of time units for metadata parsing

Tests:

  • Add parameterized tests to verify time_increment roundtrip for ImageJ and OME TIFF writers

Summary by CodeRabbit

  • New Features

    • Per-image time increment added with validation; carried through image creation, substitution, reading, and saving flows.
    • Automatic extraction of time metadata from ImageJ and OME sources; recognition of many time units.
    • Time metadata (interval and unit) preserved and written into TIFF/IMAGEJ outputs.
  • Tests

    • New tests validate time increment setter behavior and round-trip preservation of spacing and time increment when saving and reading.

@Czaki Czaki added this to the 0.16.4 milestone Oct 20, 2025
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 20, 2025

Reviewer's Guide

This PR introduces support for reading and writing time scale metadata (time_increment) across ImageJ and OME TIFF formats by extending the image reader, writer, and Image model, adding unit conversions, and corresponding tests.

ER diagram for time_increment and unit conversion in metadata

erDiagram
IMAGE {
  float time_increment
}
METADATA {
  string TimeIncrement
  string TimeIncrementUnit
  float finterval
  float fps
}
IMAGE ||--|| METADATA : "has"
Loading

Class diagram for updated Image class with time_increment property

classDiagram
class Image {
  - _time_increment: float
  + time_increment: float
  + time_increment: float (getter)
  + time_increment(value: float) (setter)
}
Loading

Class diagram for updated ImageReader with time_increment support

classDiagram
class ImageReader {
  - time_increment: float
  + read_imagej_metadata(image_file)
  + read_ome_metadata(image_file)
}
Loading

Class diagram for updated ImageWriter with time_increment in metadata

classDiagram
class ImageWriter {
  + prepare_metadata(image: Image, channels: int): dict
  + save(image: Image, save_path, compression, **kwargs)
}
Loading

Flow diagram for reading and writing time_increment metadata

flowchart TD
A["Read TIFF file"] --> B["Extract time_increment from metadata"]
B --> C["Set Image.time_increment"]
C --> D["Prepare metadata for saving"]
D --> E["Write time_increment to TIFF metadata"]
Loading

File-Level Changes

Change Details Files
Add time_increment attribute and parse time metadata in the reader
  • Initialize self.time_increment in reader
  • Include time_increment in report_func outputs
  • Parse 'finterval' from ImageJ metadata
  • Parse 'TimeIncrement' with unit conversion from OME metadata
package/PartSegImage/image_reader.py
Extend time unit conversions
  • Add second-based units (Ys, Zs…ys, min, h, d) to name_to_scalar dict
package/PartSegImage/image_reader.py
Introduce time_increment property in Image model
  • Accept time_increment parameter in Image.init
  • Store time_increment as _time_increment
  • Add getter and setter with positive-value validation
package/PartSegImage/image.py
Include time metadata when writing TIFFs
  • Add TimeIncrement and TimeIncrementUnit to OME metadata
  • Add finterval and fps fields to ImageJ metadata
package/PartSegImage/image_writer.py
Add tests for time metadata persistence
  • Parametrize test_save_time_meta to verify saving and reading time_increment
package/tests/test_PartSegImage/test_image_writer.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 20, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Added time_increment support: Image gained a validated time_increment attribute and setter; readers extract and propagate time_increment from ImageJ/OME/TIFF metadata; writers persist TimeIncrement and unit (and IMAGEJ finterval/fps); tests added for setter validation and TIFF round-trip.

Changes

Cohort / File(s) Summary
Image model enhancement
package/PartSegImage/image.py
Add time_increment: float parameter to Image.__init__, store as _time_increment; add public time_increment property with getter and setter (validate >0); update substitute(...) to accept and forward time_increment.
Reader metadata extraction
package/PartSegImage/image_reader.py
Add time_increment attribute (default 1.0); extend name_to_scalar with time units/prefixes; parse ImageJ finterval and OME TimeIncrement into self.time_increment; include time_increment in report_func payload and pass to image_class constructor.
Writer metadata persistence
package/PartSegImage/image_writer.py
Add return annotation to prepare_metadata; include TimeIncrement and TimeIncrementUnit in TIFF/IMAGEJ metadata; set IMAGEJ finterval and compute fps when time data present (fps only if time_increment != 0).
Tests — writer metadata
package/tests/test_PartSegImage/test_image_writer.py
Add test_save_time_meta (parameterized) that writes a TZYX image with time_increment, saves via IMAGEJ/ImageWriter variants, reads back TIFF, and asserts spacing and time_increment round-trip.
Tests — image setter
package/tests/test_PartSegImage/test_image.py
Add test_set_time_increment verifying default value, successful setter, ValueError on non-positive assignment, and that valid value persists.
Project metadata
.github/project_dict.pws
Append "ome" and "ImageJ" entries to the project dictionary list.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Reader as Image Reader
    participant Meta as Metadata Parser
    participant Img as Image Model
    participant Writer as Image Writer

    Reader->>Meta: open file (TIFF / ImageJ / OME)
    Meta-->>Meta: extract spacing, channels, TimeIncrement / finterval
    Meta->>Reader: set reader.time_increment
    Reader->>Img: Instantiate image_class(..., spacing=..., time_increment=val, ...)
    Img->>Img: store _time_increment (validate > 0)
    Note right of Img #D5F5E3: exposes time_increment property

    Writer->>Img: query image.time_increment
    Writer->>Writer: prepare_metadata(...): include TimeIncrement, TimeIncrementUnit, finterval, fps
    Writer->>Meta: write TIFF/IMAGEJ metadata with TimeIncrement fields
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through metadata, quick and spry,

I felt each finterval as moments fly,
From ImageJ read to TIFF's stored frame,
Time_increment now travels with the name,
A tiny tick preserved in every file.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: Add saving and reading of time scale in ImageJ and OME TIFF files" directly and accurately captures the primary objective of the changeset. The raw summary and PR objectives confirm that the core changes involve adding support for reading and writing time scale (time_increment) metadata in ImageJ and OME TIFF image formats. The title is concise and specific, clearly communicating the main change without vague terminology or unnecessary noise. While the changeset also includes supporting changes like exposing the time_increment property on the Image model and expanding unit mappings, these are secondary to the primary objective of metadata persistence and do not need to be explicitly mentioned in the title. A teammate reviewing the git history would immediately understand the pull request's intent from this title.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add_time_saving

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92e0399 and 0dc6788.

📒 Files selected for processing (1)
  • package/tests/test_PartSegImage/test_image.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package/tests/test_PartSegImage/test_image.py
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test windows)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test macos)
  • GitHub Check: 4DNucleome.PartSeg (Tests_linux test_linux)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller windows)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos_arm)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos)
  • GitHub Check: 4DNucleome.PartSeg (Builds sdist)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller_linux)
  • GitHub Check: Base py3.11 / macos-15-intel py 3.11 latest PyQt5
  • GitHub Check: test_coverage / ubuntu-24.04 py 3.12 latest PyQt5
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check help)
  • GitHub Check: 4DNucleome.PartSeg (formatting_check check_formating)
  • GitHub Check: 4DNucleome.PartSeg (manifest_check manifest_check)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check Notebook_check)
  • GitHub Check: 4DNucleome.PartSeg (GetTestData linux)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Sourcery review

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
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • Add a fallback to parse an ‘fps’ field from ImageJ metadata when ‘finterval’ is missing so that time_increment can still be recovered for older or alternative exports.
  • Instead of mixing all temporal units into the general name_to_scalar dict, consider extracting a dedicated time_unit_to_scalar mapping to keep spatial and temporal unit conversions clearly separated.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Add a fallback to parse an ‘fps’ field from ImageJ metadata when ‘finterval’ is missing so that time_increment can still be recovered for older or alternative exports.
- Instead of mixing all temporal units into the general name_to_scalar dict, consider extracting a dedicated time_unit_to_scalar mapping to keep spatial and temporal unit conversions clearly separated.

## Individual Comments

### Comment 1
<location> `package/PartSegImage/image_reader.py:634-635` </location>
<code_context>
         if "Ranges" in image_file.imagej_metadata:
             ranges = image_file.imagej_metadata["Ranges"]
             self.ranges = list(zip(ranges[::2], ranges[1::2]))
+        if "finterval" in image_file.imagej_metadata:
+            self.time_increment = image_file.imagej_metadata["finterval"]
         self.metadata = image_file.imagej_metadata

</code_context>

<issue_to_address>
**suggestion:** Type conversion for 'finterval' may be needed for robustness.

If 'finterval' can be a string or another type, add explicit type conversion and error handling to ensure self.time_increment is always a float.

```suggestion
        if "finterval" in image_file.imagej_metadata:
            try:
                self.time_increment = float(image_file.imagej_metadata["finterval"])
            except (ValueError, TypeError):
                # Optionally, log a warning here if you have a logger
                self.time_increment = None
```
</issue_to_address>

### Comment 2
<location> `package/tests/test_PartSegImage/test_image_writer.py:163-170` </location>
<code_context>
     assert np.all(np.isclose(read_mask.spacing, image.spacing))
+
+
[email protected]("save_method", [IMAGEJImageWriter.save, ImageWriter.save], ids=["ImageJ TIFF", "OME TIFF"])
+def test_save_time_meta(tmp_path, save_method):
+    data = np.zeros((5, 10, 20, 20), dtype=np.uint8)
+    image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=2.0)
+    save_method(image, tmp_path / "time_image.tif")
+    read_image = TiffImageReader.read_image(tmp_path / "time_image.tif")
+    assert np.all(np.isclose(image.spacing, read_image.spacing))
+    assert read_image.time_increment == 2.0
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for edge cases of time_increment, such as zero, negative, and very large/small values.

Please add tests for zero, negative, and extreme time_increment values, and confirm that ValueError is raised for invalid inputs.

Suggested implementation:

```python
@pytest.mark.parametrize("save_method", [IMAGEJImageWriter.save, ImageWriter.save], ids=["ImageJ TIFF", "OME TIFF"])
def test_save_time_meta(tmp_path, save_method):
    data = np.zeros((5, 10, 20, 20), dtype=np.uint8)
    image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=2.0)
    save_method(image, tmp_path / "time_image.tif")
    read_image = TiffImageReader.read_image(tmp_path / "time_image.tif")
    assert np.all(np.isclose(image.spacing, read_image.spacing))
    assert read_image.time_increment == 2.0

@pytest.mark.parametrize(
    "time_increment, should_raise",
    [
        (0.0, True),           # zero value, should raise
        (-1.0, True),          # negative value, should raise
        (1e-12, False),        # very small positive value, should not raise
        (1e12, False),         # very large positive value, should not raise
    ],
    ids=["zero", "negative", "very_small", "very_large"]
)
@pytest.mark.parametrize("save_method", [IMAGEJImageWriter.save, ImageWriter.save], ids=["ImageJ TIFF", "OME TIFF"])
def test_save_time_meta_edge_cases(tmp_path, save_method, time_increment, should_raise):
    data = np.zeros((5, 10, 20, 20), dtype=np.uint8)
    if should_raise:
        import pytest
        with pytest.raises(ValueError):
            image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=time_increment)
            save_method(image, tmp_path / f"time_image_{time_increment}.tif")
    else:
        image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=time_increment)
        save_method(image, tmp_path / f"time_image_{time_increment}.tif")
        read_image = TiffImageReader.read_image(tmp_path / f"time_image_{time_increment}.tif")
        assert np.all(np.isclose(image.spacing, read_image.spacing))
        assert read_image.time_increment == time_increment

```

- Ensure that the `Image` class and/or the `save_method` implementation actually raises a `ValueError` for zero or negative `time_increment`. If not, you will need to add validation logic in those classes.
- If the `Image` constructor does not currently validate `time_increment`, add a check like:
  ```
  if time_increment is not None and time_increment <= 0:
      raise ValueError("time_increment must be positive")
  ```
  in the `Image` class.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 163 to 170
@pytest.mark.parametrize("save_method", [IMAGEJImageWriter.save, ImageWriter.save], ids=["ImageJ TIFF", "OME TIFF"])
def test_save_time_meta(tmp_path, save_method):
data = np.zeros((5, 10, 20, 20), dtype=np.uint8)
image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=2.0)
save_method(image, tmp_path / "time_image.tif")
read_image = TiffImageReader.read_image(tmp_path / "time_image.tif")
assert np.all(np.isclose(image.spacing, read_image.spacing))
assert read_image.time_increment == 2.0
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding tests for edge cases of time_increment, such as zero, negative, and very large/small values.

Please add tests for zero, negative, and extreme time_increment values, and confirm that ValueError is raised for invalid inputs.

Suggested implementation:

@pytest.mark.parametrize("save_method", [IMAGEJImageWriter.save, ImageWriter.save], ids=["ImageJ TIFF", "OME TIFF"])
def test_save_time_meta(tmp_path, save_method):
    data = np.zeros((5, 10, 20, 20), dtype=np.uint8)
    image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=2.0)
    save_method(image, tmp_path / "time_image.tif")
    read_image = TiffImageReader.read_image(tmp_path / "time_image.tif")
    assert np.all(np.isclose(image.spacing, read_image.spacing))
    assert read_image.time_increment == 2.0

@pytest.mark.parametrize(
    "time_increment, should_raise",
    [
        (0.0, True),           # zero value, should raise
        (-1.0, True),          # negative value, should raise
        (1e-12, False),        # very small positive value, should not raise
        (1e12, False),         # very large positive value, should not raise
    ],
    ids=["zero", "negative", "very_small", "very_large"]
)
@pytest.mark.parametrize("save_method", [IMAGEJImageWriter.save, ImageWriter.save], ids=["ImageJ TIFF", "OME TIFF"])
def test_save_time_meta_edge_cases(tmp_path, save_method, time_increment, should_raise):
    data = np.zeros((5, 10, 20, 20), dtype=np.uint8)
    if should_raise:
        import pytest
        with pytest.raises(ValueError):
            image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=time_increment)
            save_method(image, tmp_path / f"time_image_{time_increment}.tif")
    else:
        image = Image(data, spacing=(0.4, 0.1, 0.1), axes_order="TZYX", time_increment=time_increment)
        save_method(image, tmp_path / f"time_image_{time_increment}.tif")
        read_image = TiffImageReader.read_image(tmp_path / f"time_image_{time_increment}.tif")
        assert np.all(np.isclose(image.spacing, read_image.spacing))
        assert read_image.time_increment == time_increment
  • Ensure that the Image class and/or the save_method implementation actually raises a ValueError for zero or negative time_increment. If not, you will need to add validation logic in those classes.
  • If the Image constructor does not currently validate time_increment, add a check like:
    if time_increment is not None and time_increment <= 0:
        raise ValueError("time_increment must be positive")
    
    in the Image class.

Copy link
Contributor

@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: 1

🧹 Nitpick comments (1)
package/PartSegImage/image.py (1)

788-788: Consider extracting the error message to a constant.

While inline messages are common, extracting to a module-level constant improves consistency and testability. However, this is a stylistic choice and the current implementation is acceptable.

Example refactor:

+_TIME_INCREMENT_ERROR = "Time spacing need to be positive"
+
 @time_increment.setter
 def time_increment(self, value: float):
     """set time spacing in seconds"""
     if value <= 0:
-        raise ValueError("Time spacing need to be positive")
+        raise ValueError(_TIME_INCREMENT_ERROR)
     self._time_increment = value

Based on learnings

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa9269a and edf62c8.

📒 Files selected for processing (4)
  • package/PartSegImage/image.py (3 hunks)
  • package/PartSegImage/image_reader.py (5 hunks)
  • package/PartSegImage/image_writer.py (3 hunks)
  • package/tests/test_PartSegImage/test_image_writer.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
package/tests/test_PartSegImage/test_image_writer.py (2)
package/PartSegImage/image_writer.py (5)
  • IMAGEJImageWriter (118-181)
  • save (16-17)
  • save (67-83)
  • save (122-146)
  • ImageWriter (25-115)
package/PartSegImage/image_reader.py (2)
  • read_image (168-189)
  • read_image (268-289)
package/PartSegImage/image_writer.py (2)
package/tests/conftest.py (1)
  • image (49-56)
package/PartSegImage/image.py (5)
  • Image (198-1031)
  • channels (636-638)
  • time_increment (780-782)
  • time_increment (785-789)
  • is_time (626-628)
package/PartSegImage/image_reader.py (1)
package/PartSegImage/image.py (2)
  • time_increment (780-782)
  • time_increment (785-789)
🪛 Ruff (0.14.1)
package/PartSegImage/image.py

788-788: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test windows)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test macos)
  • GitHub Check: 4DNucleome.PartSeg (Tests_linux test_linux)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller windows)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos_arm)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos)
  • GitHub Check: 4DNucleome.PartSeg (Builds sdist)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller_linux)
  • GitHub Check: Base py3.11 / macos-15-intel py 3.11 latest PyQt5
  • GitHub Check: test_coverage / ubuntu-24.04 py 3.12 latest PyQt5
  • GitHub Check: 4DNucleome.PartSeg (manifest_check manifest_check)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check Notebook_check)
  • GitHub Check: 4DNucleome.PartSeg (formatting_check check_formating)
  • GitHub Check: 4DNucleome.PartSeg (GetTestData linux)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check help)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Sourcery review
🔇 Additional comments (10)
package/PartSegImage/image.py (1)

237-237: LGTM! Time increment parameter added correctly.

The parameter has a sensible default value of 1.0 second and is properly stored as an internal attribute.

Also applies to: 260-260

package/tests/test_PartSegImage/test_image_writer.py (1)

163-170: LGTM! Comprehensive test for time metadata preservation.

The test correctly verifies that time_increment is preserved through both ImageJ and OME TIFF write/read cycles. The parametrization ensures both writers handle time metadata consistently.

package/PartSegImage/image_writer.py (3)

29-29: LGTM! Good addition of return type annotation.


52-52: LGTM! Time metadata correctly added to OME TIFF.

The TimeIncrement and TimeIncrementUnit fields follow OME-TIFF specifications and align with the spatial metadata already present.

Also applies to: 56-56


133-136: LGTM! ImageJ time metadata added correctly.

The implementation adds finterval and fps to ImageJ metadata when time data is present. The zero check on line 135 is defensive programming—while the setter enforces positive values, this guards against any edge cases.

package/PartSegImage/image_reader.py (5)

129-129: LGTM! Default time increment initialized consistently.

The default value of 1.0 second matches the default in the Image class constructor.


539-539: LGTM! Time increment passed to image constructor.

The time_increment is correctly propagated from the reader to the Image class constructor, completing the metadata flow.


634-635: LGTM! ImageJ time metadata extraction implemented correctly.

The finterval field is the standard ImageJ TIFF metadata key for frame intervals, and it's correctly extracted and assigned.


659-662: LGTM! OME time metadata extraction with unit conversion.

The implementation correctly extracts TimeIncrement and TimeIncrementUnit from OME metadata and applies the appropriate unit conversion, consistent with the spatial metadata handling pattern.


697-721: LGTM! Comprehensive time unit mappings added.

The additions cover SI prefixes from yotta- to yocto-seconds, plus common time units (minutes, hours, days). The conversions are mathematically correct and follow the same pattern as existing spatial unit mappings.

@codecov
Copy link

codecov bot commented Oct 20, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.21%. Comparing base (3a46f96) to head (0dc6788).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1323      +/-   ##
===========================================
+ Coverage    93.18%   93.21%   +0.02%     
===========================================
  Files          210      210              
  Lines        33365    33406      +41     
===========================================
+ Hits         31092    31138      +46     
+ Misses        2273     2268       -5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
package/PartSegImage/image.py (1)

782-793: Consider extracting the error message to satisfy the linter.

The property implementation with validation is correct. However, Ruff flags the inline error message at line 791 (TRY003). While functional, you could optionally define a custom exception class or extract the message to a constant for better maintainability.

If you'd like to address the linter warning, apply this diff:

+TIME_INCREMENT_ERROR_MSG = "Time spacing need to be positive"
+
 @property
 def time_increment(self) -> float:
     """time spacing in seconds"""
     return self._time_increment

 @time_increment.setter
 def time_increment(self, value: float):
     """set time spacing in seconds"""
     if value <= 0:
-        raise ValueError("Time spacing need to be positive")
+        raise ValueError(TIME_INCREMENT_ERROR_MSG)
     self._time_increment = value
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edf62c8 and a7cac56.

📒 Files selected for processing (4)
  • .github/project_dict.pws (1 hunks)
  • package/PartSegImage/image.py (6 hunks)
  • package/PartSegImage/image_reader.py (5 hunks)
  • package/tests/test_PartSegImage/test_image_writer.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .github/project_dict.pws
🚧 Files skipped from review as they are similar to previous changes (1)
  • package/tests/test_PartSegImage/test_image_writer.py
🧰 Additional context used
🧬 Code graph analysis (1)
package/PartSegImage/image_reader.py (1)
package/PartSegImage/image.py (2)
  • time_increment (783-785)
  • time_increment (788-792)
🪛 Ruff (0.14.1)
package/PartSegImage/image.py

791-791: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test windows)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test macos)
  • GitHub Check: 4DNucleome.PartSeg (Tests_linux test_linux)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller windows)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos_arm)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos)
  • GitHub Check: 4DNucleome.PartSeg (Builds sdist)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller_linux)
  • GitHub Check: Base py3.10 / ubuntu-24.04 py 3.10 latest PySide6
  • GitHub Check: Base py3.11 / macos-15-intel py 3.11 latest PyQt5
  • GitHub Check: Base py3.9 / ubuntu-24.04 py 3.9 latest PyQt5
  • GitHub Check: test_coverage / ubuntu-24.04 py 3.12 latest PyQt5
  • GitHub Check: 4DNucleome.PartSeg (manifest_check manifest_check)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check help)
  • GitHub Check: 4DNucleome.PartSeg (formatting_check check_formating)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check Notebook_check)
  • GitHub Check: 4DNucleome.PartSeg (GetTestData linux)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Sourcery review
🔇 Additional comments (6)
package/PartSegImage/image.py (1)

237-237: LGTM! The time_increment parameter is now properly integrated and propagated.

The addition of the time_increment parameter to __init__ (line 237), its storage (line 260), and its propagation through the substitute method (lines 505, 515, 530) fully addresses the past review comment about missing time_increment preservation. The default value of 1.0 is reasonable.

Based on past review comments.

Also applies to: 260-260, 505-505, 515-515, 530-530

package/PartSegImage/image_reader.py (5)

129-129: LGTM! Consistent initialization of time_increment.

The default value of 1.0 matches the default in the Image class and provides a sensible fallback when time metadata is unavailable.


634-636: Good error handling for ImageJ metadata parsing.

The use of float() conversion with suppress(ValueError) properly handles cases where finterval might be an invalid or non-numeric value. If conversion fails, time_increment retains its default value of 1.0.

Based on past review comments.


660-663: OME metadata parsing with unit conversion looks correct.

The code properly extracts TimeIncrement and converts it using the unit scalar from TimeIncrementUnit. The suppress(KeyError) ensures graceful fallback if either field is missing or if the unit is not recognized in name_to_scalar.


698-721: Comprehensive time unit support added.

The extended name_to_scalar dictionary includes a complete set of SI time prefixes and common time units. Spot-checked conversions are correct:

  • SI prefixes follow powers of 10 (ms=10⁻³, µs=10⁻⁶, ks=10³, etc.)
  • Common units are accurate (min=60, h=3600, d=86400)

539-539: LGTM! Time increment properly passed to Image constructor.

The time_increment is correctly forwarded to the image_class constructor, completing the metadata propagation from TIFF reading to the final Image instance.

Copy link
Contributor

@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 (1)
package/PartSegImage/image.py (1)

922-929: The cut_image method doesn't preserve time_increment.

When creating a new image from a cut region, the method passes spacing but not time_increment, causing the cut image to lose time metadata and revert to the default value of 1.0.

Apply this diff to preserve time_increment:

         return self.__class__(
             data=self._image_data_normalize(new_image),
             spacing=self._image_spacing,
+            time_increment=self._time_increment,
             file_path=None,
             mask=new_mask,
             channel_info=self._channel_info,
             axes_order=self.axis_order,
         )
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7cac56 and ace36b9.

📒 Files selected for processing (1)
  • package/PartSegImage/image.py (6 hunks)
🧰 Additional context used
🪛 Ruff (0.14.1)
package/PartSegImage/image.py

791-791: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (21)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller windows)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test windows)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos_arm)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test macos)
  • GitHub Check: 4DNucleome.PartSeg (Builds sdist)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller_linux)
  • GitHub Check: 4DNucleome.PartSeg (Tests_linux test_linux)
  • GitHub Check: Base py3.11 / macos-15-intel py 3.11 latest PyQt5
  • GitHub Check: Base py3.11 / windows-latest py 3.11 latest PyQt5
  • GitHub Check: Base py3.12 / ubuntu-22.04 py 3.12 latest PyQt6
  • GitHub Check: Base py3.10 / ubuntu-22.04 py 3.10 latest PySide2
  • GitHub Check: Test PartSeg minimal / ubuntu-24.04 py 3.9 latest PyQt5
  • GitHub Check: test_coverage / ubuntu-24.04 py 3.12 latest PyQt5
  • GitHub Check: 4DNucleome.PartSeg (manifest_check manifest_check)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check Notebook_check)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check help)
  • GitHub Check: 4DNucleome.PartSeg (formatting_check check_formating)
  • GitHub Check: 4DNucleome.PartSeg (GetTestData linux)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Sourcery review
🔇 Additional comments (1)
package/PartSegImage/image.py (1)

505-505: LGTM! Time increment is now preserved in substitution.

The substitute method correctly propagates time_increment through the copy operation, addressing the concern raised in previous reviews.

Also applies to: 515-515, 530-530

Copy link
Contributor

@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: 0

🧹 Nitpick comments (2)
package/PartSegImage/image.py (1)

260-261: Consider removing redundant initialization.

Line 260 initializes _time_increment to 1.0, but line 261 immediately calls the setter which will overwrite it. The intermediate assignment adds no value since the setter will either succeed with the parameter value or raise a ValueError.

Apply this diff to simplify:

-        self._time_increment = 1.0
         self.time_increment = time_increment
package/PartSegImage/image_reader.py (1)

634-636: Consider logging when finterval parsing fails.

The current implementation silently falls back to the default when finterval cannot be parsed as a float. While defensive, this could mask malformed metadata that users might want to know about.

Apply this diff to add optional logging:

         if "finterval" in image_file.imagej_metadata:
-            with suppress(ValueError):
-                self.time_increment = float(image_file.imagej_metadata["finterval"])
+            try:
+                self.time_increment = float(image_file.imagej_metadata["finterval"])
+            except (ValueError, TypeError) as e:
+                logging.debug(f"Could not parse finterval from ImageJ metadata: {e}")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ace36b9 and c84276e.

📒 Files selected for processing (3)
  • package/PartSegImage/image.py (6 hunks)
  • package/PartSegImage/image_reader.py (5 hunks)
  • package/tests/test_PartSegImage/test_image.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
package/tests/test_PartSegImage/test_image.py (1)
package/PartSegImage/image.py (3)
  • spacing (779-781)
  • time_increment (784-786)
  • time_increment (789-793)
package/PartSegImage/image_reader.py (1)
package/PartSegImage/image.py (2)
  • time_increment (784-786)
  • time_increment (789-793)
🪛 Ruff (0.14.1)
package/PartSegImage/image.py

792-792: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test windows)
  • GitHub Check: 4DNucleome.PartSeg (Tests_other test macos)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller windows)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos_arm)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller_linux)
  • GitHub Check: 4DNucleome.PartSeg (Builds sdist)
  • GitHub Check: 4DNucleome.PartSeg (Tests_linux test_linux)
  • GitHub Check: 4DNucleome.PartSeg (Builds pyinstaller macos)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check Notebook_check)
  • GitHub Check: 4DNucleome.PartSeg (formatting_check check_formating)
  • GitHub Check: 4DNucleome.PartSeg (manifest_check manifest_check)
  • GitHub Check: 4DNucleome.PartSeg (Documentation_check help)
  • GitHub Check: 4DNucleome.PartSeg (GetTestData linux)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Sourcery review
🔇 Additional comments (8)
package/tests/test_PartSegImage/test_image.py (1)

468-479: LGTM!

The test comprehensively covers the time_increment property: default initialization, valid updates, and validation behavior. The guard for missing time axis and the verification that state is preserved after validation failure are well-designed.

package/PartSegImage/image.py (3)

237-237: LGTM!

The default value of 1.0 for time_increment is appropriate for representing one-second intervals.


502-537: LGTM!

The time_increment parameter is correctly integrated into the substitute method, following the same pattern as other optional parameters (defaulting to current value if None, then passing to constructor).


783-794: LGTM!

The property implementation correctly enforces positive time_increment values with clear validation and documentation. The Ruff TRY003 hint about extracting the error message to a constant is a minor style preference that can be safely deferred.

package/PartSegImage/image_reader.py (4)

129-129: LGTM!

Initializing time_increment to 1.0 provides a sensible default that matches the Image class default.


539-539: LGTM!

The time_increment is correctly passed through to the image constructor, completing the data flow from metadata parsing to the final Image instance.


660-665: LGTM!

The OME metadata parsing correctly applies unit conversion and validates that time_increment is positive before assignment. The validation is consistent with the Image class property setter.


700-724: LGTM!

The time unit conversions are comprehensive and accurate, covering SI prefixes from yottasecond (10²⁴) to yoctosecond (10⁻²⁴), plus common units (minute, hour, day). All scalar values are correct.

@sonarqubecloud
Copy link

@Czaki Czaki merged commit 42c542b into develop Oct 21, 2025
58 checks passed
@Czaki Czaki deleted the add_time_saving branch October 21, 2025 10:43
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.

1 participant