Skip to content

Commit

Permalink
implement cliipping and add correponding tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dummyindex committed Nov 29, 2023
1 parent 67e9e9f commit dfc3167
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 4 deletions.
27 changes: 25 additions & 2 deletions livecellx/core/sc_seg_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,11 @@ def _get_contours_from_shape_layer(layer: Shapes):
res_contours.append(vertices)
return res_contours

def save_seg_callback(self):
def save_seg_callback(self, in_image=True):
"""Save the segmentation to the single cell object."""
import napari
from PyQt5.QtWidgets import QMessageBox
from livecellx.core.utils import clip_polygon

print("<save_seg_callback fired>")
# Get the contour coordinates from the shape layer
Expand All @@ -265,7 +266,29 @@ def save_seg_callback(self):
QMessageBox.warning(None, "Warning", message)
return
assert len(contours) > 0, "No contour is found in the shape layer."
contour = contours[0]
contour = contours[0] # n x 2

# limit the contour coordinates to the image height and width
if in_image:
main_info("Limiting the contour coordinates to the image height and width.", indent_level=2)
main_debug("contour before clipping:" + str(contour.shape), indent_level=2)
image = self.sc.get_img()
image_dim = image.shape

# Clipping algorithm
contour = clip_polygon(contour, image_dim[0], image_dim[1])

# Ensure the contour is within the image
contour[:, 0] = np.clip(contour[:, 0], 0, image_dim[0] - 1)
contour[:, 1] = np.clip(contour[:, 1], 0, image_dim[1] - 1)

# update the shape layer as well
main_info("Updating the shape layer of sc...", indent_level=2)
napari_vertices = [[self.sc.timeframe] + list(point) for point in contour]
napari_vertices = np.array(napari_vertices)
self.shape_layer.data = []
self.shape_layer.add([(napari_vertices, "polygon")], shape_type=["polygon"])

# Store the contour in the single cell object
self.sc.update_contour(contour)

Expand Down
4 changes: 2 additions & 2 deletions livecellx/core/sct_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def lookup_sc_shape_index(self, sc) -> Optional[int]:
return update_shape_index

def update_shape_layer_by_sc(self, sc: SingleCellStatic):
print("<update shape layer by sc>")
main_info("<update shape layer by sc>")

# clear selected data first because adding/deleting shapes will change the shape index
self.clear_selection()
Expand Down Expand Up @@ -262,7 +262,7 @@ def update_shape_layer_by_sc(self, sc: SingleCellStatic):
# print("<setting shapes...>")
# self.shape_layer.data = shape_data
self.store_shape_layer_info()
print("<update shape layer by sc complete>")
main_info("<update shape layer by sc complete>")

def connect_two_scts(self):
assert len(self.select_info) == 2, "Please select two shapes to connect."
Expand Down
46 changes: 46 additions & 0 deletions livecellx/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,49 @@ def label_mask_to_edt_mask(label_mask, bg_val=0):
tmp_mask[tmp_mask != bg_val] = normalized_mask[tmp_mask != bg_val]
edt_mask += tmp_mask
return edt_mask.astype(np.uint8)


def clip_polygon(polygon, h, w):
"""
The Sutherland-Hodgman algorithm. Adapted from ChatGpt's implementation.
Define the Polygon and Clipping Rectangle: The polygon is defined by its vertices, and the clipping rectangle is defined by the dimensions H and W.
Implement the Clipping Algorithm: The Sutherland-Hodgman algorithm is a common choice for polygon clipping. This algorithm iteratively clips the edges of the polygon against each edge of the clipping rectangle.
Handle Edge Cases: Special care must be taken to handle edge cases, such as when a polygon vertex lies exactly on a clipping boundary.
"""

def clip_edge(polygon, x1, y1, x2, y2):
new_polygon = []
for i in range(len(polygon)):
current_x, current_y = polygon[i]
previous_x, previous_y = polygon[i - 1]

# Check if current and previous points are inside the clipping edge
inside_current = (x2 - x1) * (current_y - y1) > (y2 - y1) * (current_x - x1)
inside_previous = (x2 - x1) * (previous_y - y1) > (y2 - y1) * (previous_x - x1)

if inside_current:
if not inside_previous:
# Compute intersection and add to new polygon
new_polygon.append(intersect(previous_x, previous_y, current_x, current_y, x1, y1, x2, y2))
new_polygon.append((current_x, current_y))
elif inside_previous:
# Compute intersection and add to new polygon
new_polygon.append(intersect(previous_x, previous_y, current_x, current_y, x1, y1, x2, y2))

return new_polygon

def intersect(px, py, qx, qy, ax, ay, bx, by):
# Compute the intersection point
det = (qx - px) * (by - ay) - (qy - py) * (bx - ax)
if det == 0:
return qx, qy # Lines are parallel
l = ((by - ay) * (bx - px) - (bx - ax) * (by - py)) / det
return px + l * (qx - px), py + l * (qy - py)

# Clipping against four edges of the rectangle
clipped_polygon = clip_edge(polygon, 0, 0, w, 0)
clipped_polygon = clip_edge(clipped_polygon, w, 0, w, h)
clipped_polygon = clip_edge(clipped_polygon, w, h, 0, h)
clipped_polygon = clip_edge(clipped_polygon, 0, h, 0, 0)

return np.array(clipped_polygon)
54 changes: 54 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
SingleCellTrajectoryCollection,
)
from livecellx.core.datasets import LiveCellImageDataset
from livecellx.core.utils import clip_polygon


class TestHelper(unittest.TestCase):
Expand Down Expand Up @@ -137,3 +138,56 @@ def compare_datasets(self, ds1: LiveCellImageDataset, ds2: LiveCellImageDataset)
f"max_cache_size mismatch: {ds1.max_cache_size} vs {ds2.max_cache_size}",
)
return True


class TestUtils(unittest.TestCase):
def test_clip_polygon(self):
# Define the input polygon and clipping rectangle
polygon = [(0, 0), (2, 0), (2, 2), (0, 2)]
h = 3
w = 3

# Expected clipped polygon
expected_clipped_polygon = np.array([(0, 0), (2, 0), (2, 2), (0, 2)])

# Call the clip_polygon function
clipped_polygon = clip_polygon(polygon, h, w)

# Check if the clipped polygon matches the expected result
np.testing.assert_array_equal(clipped_polygon, expected_clipped_polygon)

def test_clip_polygon_edge_case(self):
# Define the input polygon and clipping rectangle
polygon = [(0, 0), (2, 0), (2, 2), (0, 2)]
h = 2
w = 2

# Expected clipped polygon: starting coordinates can be different
expected_clipped_polygon = np.array([(0, 2), (0, 0), (2, 0), (2, 2)])

# Call the clip_polygon function
clipped_polygon = clip_polygon(polygon, h, w)

# Check if the clipped polygon matches the expected result
np.testing.assert_array_equal(clipped_polygon, expected_clipped_polygon)

def test_clip_polygon_parallel_lines(self):
# Define the input polygon and clipping rectangle
polygon = [(0, 0), (2, 0), (2, 2), (0, 2)]
h = 3
w = 3

# Expected clipped polygon (same as input polygon)
expected_clipped_polygon = np.array([(0, 0), (2, 0), (2, 2), (0, 2)])

# Call the clip_polygon function with parallel lines
clipped_polygon = clip_polygon(polygon, h, w)

# Check if the clipped polygon matches the expected result
np.testing.assert_array_equal(clipped_polygon, expected_clipped_polygon)


# Append the test cases to the existing test class
# TestHelper.test_clip_polygon = TestUtils.test_clip_polygon
# TestHelper.test_clip_polygon_edge_case = TestUtils.test_clip_polygon_edge_case
# TestHelper.test_clip_polygon_parallel_lines = TestUtils.test_clip_polygon_parallel_lines

0 comments on commit dfc3167

Please sign in to comment.