Skip to content

Commit

Permalink
Add tool for evaluation of tissue geometry properties #21
Browse files Browse the repository at this point in the history
  • Loading branch information
Mr-Milk committed Jun 14, 2024
1 parent 8ac54d7 commit 6eed8c1
Showing 1 changed file with 163 additions and 0 deletions.
163 changes: 163 additions & 0 deletions lazyslide/tl/tissue_props.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import cv2
import numpy as np
from functools import cached_property

import pandas as pd

from lazyslide.wsi import WSI
from lazyslide.get import tissue_contours


def tissue_props(
wsi: WSI,
key: str = "tissue",
):
"""Extract tissue properties from the WSI."""
props = []
cnts = []
for tissue_contour in tissue_contours(wsi, key=key, return_type="numpy"):
cnt = tissue_contour.contour
holes = tissue_contour.holes
_props = TissueProps(cnt, holes)()
cnts.append(cnt)
_props["tissue_id"] = tissue_contour.id
props.append(_props)

wsi.add_contours(cnts, data=pd.DataFrame(props), name=f"{key}_contours")


class TissueProps:
def __init__(self, cnt, holes=None):
self.cnt = cnt
self.holes = holes

@cached_property
def area_filled(self):
return cv2.contourArea(self.cnt)

@cached_property
def area(self):
"""Area without holes."""
if self.holes is None:
return self.area_filled
else:
area = self.area_filled
for hole in self.holes:
area -= cv2.contourArea(hole)
return area

@cached_property
def bbox(self):
x, y, w, h = cv2.boundingRect(self.cnt)
return x, y, w, h

@cached_property
def centroid(self):
M = self.moments
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
return cX, cY

@cached_property
def convex_hull(self):
return cv2.convexHull(self.cnt)

@cached_property
def convex_area(self):
return cv2.contourArea(self.convex_hull)

@cached_property
def solidity(self):
"""Solidity is the ratio of the contour area to the convex area."""
if self.convex_area == 0:
return 0
return self.area / self.convex_area

@cached_property
def convexity(self):
"""Convexity is the ratio of the convex area to the contour area."""
if self.area == 0:
return 0
return self.convex_area / self.area

@cached_property
def ellipse(self):
return cv2.fitEllipse(self.cnt)

@cached_property
def axis_major_length(self):
x1, x2 = self.ellipse[1]
if x1 < x2:
return x2
return x1

@cached_property
def axis_minor_length(self):
x1, x2 = self.ellipse[1]
if x1 < x2:
return x1
return x2

@cached_property
def eccentricity(self):
if self.axis_major_length == 0:
return 0
return np.sqrt(1 - (self.axis_minor_length**2) / (self.axis_major_length**2))

@cached_property
def orientation(self):
return self.ellipse[2]

@cached_property
def extent(self):
if self.area == 0:
return 0
return self.area / (self.bbox[2] * self.bbox[3])

@cached_property
def perimeter(self):
return cv2.arcLength(self.cnt, True)

@cached_property
def circularity(self):
if self.perimeter == 0:
return 0
return 4 * np.pi * self.area / (self.perimeter**2)

@cached_property
def moments(self):
return cv2.moments(self.cnt)

@cached_property
def moments_hu(self):
return cv2.HuMoments(self.moments)

def __call__(self):
props = {
"area": self.area,
"area_filled": self.area_filled,
"convex_area": self.convex_area,
"solidity": self.solidity,
"convexity": self.convexity,
"axis_major_length": self.axis_major_length,
"axis_minor_length": self.axis_minor_length,
"eccentricity": self.eccentricity,
"orientation": self.orientation,
"extent": self.extent,
"perimeter": self.perimeter,
"circularity": self.circularity,
}

for ix, box in enumerate(self.bbox):
props[f"bbox-{ix}"] = box

for ix, c in enumerate(self.centroid):
props[f"centroid-{ix}"] = c

for i, hu in enumerate(self.moments_hu):
props[f"hu-{i}"] = hu[0]

for key, value in self.moments.items():
props[f"moment-{key}"] = value

return props

0 comments on commit 6eed8c1

Please sign in to comment.