Unnecessary system dependency
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