rt-utils icon indicating copy to clipboard operation
rt-utils copied to clipboard

Unnecessary system dependency

Open cbarkr opened this issue 1 week ago • 0 comments

Hello!

I noticed that the libGL system dependency is not strictly necessary as OpenCV's GUI functionalities (e.g. cv2.imshow) are not utilized. opencv-python-headless is a drop-in replacement for opencv-python which excludes the GUI library dependencies, and therefore may be used to remove rt-utils' dependency on libGL.

To ensure that switching to opencv-python-headless doesn't break existing behaviour, I drafted basic sanity tests for some relevant functions in image_helper.py, which I ran in a Docker container explicitly built without libGL. Please see the end of this message for reference. While these tests pass, I welcome any suggestions for further testing of this module (specifically get_slice_mask_from_slice_contour_data).

Will make a PR with these changes soon in the hopes that they may help someone else.

Thanks!

📁 requirements.txt

# requirements.txt
- opencv-python>=4.0.0
+ opencv-python-headless>=4.0.0

📁 Dockerfile

# Dockerfile
FROM ubuntu

WORKDIR /app

# Explicitly *not* installing LibGL (libgl1 && libglib2.0-0)
RUN apt-get update && \
    apt-get install -y python3 python3-pip python3-venv && \
    rm -rf /var/lib/apt/lists/*

COPY . .

ENV PATH="/opt/venv/bin:$PATH"
RUN python3 -m venv /opt/venv && \
    pip3 install pytest && \
    pip3 install --no-cache-dir -r requirements.txt

CMD ["pytest"]

📁 tests/test_image_helper.py

# tests/test_image_helper.py
import pytest
import numpy as np

from rt_utils.image_helper import (
    load_sorted_image_series,
    create_empty_series_mask,
    find_mask_contours,
    draw_line_upwards_from_point,
)


def create_full_series_mask(series_data):
    ref_dicom_image = series_data[0]
    mask_dims = (
        int(ref_dicom_image.Columns),
        int(ref_dicom_image.Rows),
        len(series_data),
    )
    mask = np.ones(mask_dims).astype(bool)
    return mask


def test_find_mask_contours_with_empty_mask(series_path):
    series_data = load_sorted_image_series(series_path)
    empty_mask = create_empty_series_mask(series_data)[:, :, 0]

    # `hierarchy` is `None` in this case, thus `hierarchy = hierarchy[0]` fails
    with pytest.raises(TypeError):
        actual_contours, actual_hierarchy = find_mask_contours(empty_mask, True)


def test_find_mask_contours_with_full_mask(series_path):
    series_data = load_sorted_image_series(series_path)
    full_mask = create_full_series_mask(series_data)[:, :, 0]

    # Entire mask is a single contour
    expected_contours = [
        [
            [np.int32(0), np.int32(0)],
            [np.int32(0), np.int32(511)],
            [np.int32(511), np.int32(511)],
            [np.int32(511), np.int32(0)],
        ]
    ]
    expected_hierarchy = [[np.int32(-1), np.int32(-1), np.int32(-1), np.int32(-1)]]

    actual_contours, actual_hierarchy = find_mask_contours(full_mask, True)

    assert np.array_equal(expected_contours, actual_contours) == True
    assert np.array_equal(expected_hierarchy, actual_hierarchy) == True


def test_draw_line_upwards_from_point_with_empty_mask(series_path):
    series_data = load_sorted_image_series(series_path)
    empty_mask = create_empty_series_mask(series_data)[:, :, 0]
    start = (0, 0)

    # Should fill (0,0) and one point in each direction
    expected_binary_mask = empty_mask.copy()
    expected_binary_mask[0][0] = True
    expected_binary_mask[0][1] = True
    expected_binary_mask[1][0] = True

    actual_binary_mask = draw_line_upwards_from_point(empty_mask, start, 1)

    assert np.array_equal(expected_binary_mask, actual_binary_mask) == True


def test_draw_line_upwards_from_point_with_full_mask(series_path):
    series_data = load_sorted_image_series(series_path)
    full_mask = create_full_series_mask(series_data)[:, :, 0]
    start = (0, 0)

    # Should remain unchanged
    expected_binary_mask = full_mask.copy()

    actual_binary_mask = draw_line_upwards_from_point(full_mask, start, 1)

    assert np.array_equal(expected_binary_mask, actual_binary_mask) == True

cbarkr avatar Dec 18 '25 21:12 cbarkr