import attrs import numpy as np from typing import Tuple from dnafiber.postprocess.skan import trace_skeleton @attrs.define class Bbox: x: int y: int width: int height: int @property def bbox(self) -> Tuple[int, int, int, int]: return (self.x, self.y, self.width, self.height) @bbox.setter def bbox(self, value: Tuple[int, int, int, int]): self.x, self.y, self.width, self.height = value @attrs.define class Fiber: bbox: Bbox data: np.ndarray @attrs.define class FiberProps: fiber: Fiber fiber_id: int red_pixels: int = None green_pixels: int = None category: str = None @property def bbox(self): return self.fiber.bbox.bbox @bbox.setter def bbox(self, value): self.fiber.bbox = value @property def data(self): return self.fiber.data @data.setter def data(self, value): self.fiber.data = value @property def red(self): if self.red_pixels is None: self.red_pixels, self.green_pixels = self.counts return self.red_pixels @property def green(self): if self.green_pixels is None: self.red_pixels, self.green_pixels = self.counts return self.green_pixels @property def length(self): return sum(self.counts) @property def counts(self): if self.red_pixels is None or self.green_pixels is None: self.red_pixels = np.sum(self.data == 1) self.green_pixels = np.sum(self.data == 2) return self.red_pixels, self.green_pixels @property def fiber_type(self): if self.category is not None: return self.category red_pixels, green_pixels = self.counts if red_pixels == 0 or green_pixels == 0: self.category = "single" else: self.category = estimate_fiber_category(self.data) return self.category @property def ratio(self): return self.green / self.red @property def is_valid(self): return ( self.fiber_type == "double" or self.fiber_type == "one-two-one" or self.fiber_type == "two-one-two" ) def scaled_coordinates(self, scale: float) -> Tuple[int, int]: """ Scale down the coordinates of the fiber's bounding box. """ x, y, width, height = self.bbox return ( int(x * scale), int(y * scale), int(width * scale), int(height * scale), ) def estimate_fiber_category(fiber: np.ndarray) -> str: """ Estimate the fiber category based on the number of red and green pixels. """ coordinates = trace_skeleton(fiber > 0) coordinates = np.asarray(coordinates) values = fiber[coordinates[:, 0], coordinates[:, 1]] diff = np.diff(values) jump = np.sum(diff != 0) n_ccs = jump + 1 if n_ccs == 2: return "double" elif n_ccs == 3: if values[0] == 1: return "one-two-one" else: return "two-one-two" else: return "multiple"