| import cv2 |
| import numpy as np |
| import math |
|
|
| def _leer_mask(mask): |
| """Lee una máscara que puede venir como ruta o como array.""" |
| if isinstance(mask, str): |
| mask = cv2.imread(mask, cv2.IMREAD_GRAYSCALE) |
| elif mask.ndim == 3: |
| mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY) |
|
|
| if mask is None: |
| raise ValueError("No se pudo leer la máscara.") |
| return mask |
|
|
|
|
| def calcular_area(mask): |
| mask = _leer_mask(mask) |
| _, mask_bin = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) |
| contours, _ = cv2.findContours(mask_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
| if not contours: |
| return 0.0 |
| cnt = max(contours, key=cv2.contourArea) |
| area = cv2.contourArea(cnt) |
| if not np.isfinite(area): |
| area = 0.0 |
| return round(float(area), 2) |
|
|
|
|
| def calcular_perimetro(mask): |
| mask = _leer_mask(mask) |
| _, mask_bin = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) |
| contours, _ = cv2.findContours(mask_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
| if not contours: |
| return 0.0 |
| cnt = max(contours, key=cv2.contourArea) |
| perimetro = cv2.arcLength(cnt, True) |
| if not np.isfinite(perimetro): |
| perimetro = 0.0 |
| return round(float(perimetro), 2) |
|
|
|
|
| def calcular_circularidad(mask): |
| mask = _leer_mask(mask) |
| _, mask_bin = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) |
| contours, _ = cv2.findContours(mask_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
|
|
| if not contours: |
| return 0.0 |
|
|
| cnt = max(contours, key=cv2.contourArea) |
| area = cv2.contourArea(cnt) |
| perimetro = cv2.arcLength(cnt, True) |
|
|
| if perimetro == 0 or area == 0: |
| return 0.0 |
|
|
| circ = (4 * math.pi * area) / (perimetro ** 2) |
|
|
| if not np.isfinite(circ): |
| circ = 0.0 |
|
|
| circ = np.clip(circ, 0, 1) |
| return round(float(circ), 4) |
|
|
|
|
| def calcular_simetria(mask): |
| mask = _leer_mask(mask) |
| _, mask_bin = cv2.threshold(mask, 127, 1, cv2.THRESH_BINARY) |
|
|
| if np.sum(mask_bin) == 0: |
| return 0.0, 0.0 |
|
|
| y, x = np.where(mask_bin > 0) |
| y_min, y_max = y.min(), y.max() |
| x_min, x_max = x.min(), x.max() |
| roi = mask_bin[y_min:y_max+1, x_min:x_max+1] |
|
|
| h, w = roi.shape |
| size = max(h, w) |
| canvas = np.zeros((size, size), dtype=np.uint8) |
| y_off, x_off = (size - h)//2, (size - w)//2 |
| canvas[y_off:y_off+h, x_off:x_off+w] = roi |
| mask_centered = canvas |
|
|
| cy, cx = np.mean(np.column_stack(np.where(mask_centered > 0)), axis=0).astype(int) |
| area_total = np.sum(mask_centered) |
|
|
| if area_total == 0: |
| return 0.0, 0.0 |
|
|
| |
| left = mask_centered[:, :cx] |
| right = mask_centered[:, cx:] |
| right_flipped = np.fliplr(right) |
| min_width = min(left.shape[1], right_flipped.shape[1]) |
| xor_v = np.logical_xor(left[:, :min_width], right_flipped[:, :min_width]) |
| sim_v = 1 - (np.sum(xor_v) / area_total) |
|
|
| |
| top = mask_centered[:cy, :] |
| bottom = mask_centered[cy:, :] |
| bottom_flipped = np.flipud(bottom) |
| min_height = min(top.shape[0], bottom_flipped.shape[0]) |
| xor_h = np.logical_xor(top[:min_height, :], bottom_flipped[:min_height, :]) |
| sim_h = 1 - (np.sum(xor_h) / area_total) |
|
|
| sim_v = max(0.0, min(1.0, sim_v)) |
| sim_h = max(0.0, min(1.0, sim_h)) |
|
|
| return round(sim_v, 3), round(sim_h, 3) |