Files
gobot/board-vision/src/board_detction.py
2025-01-05 18:36:42 +01:00

278 lines
9.8 KiB (Stored with Git LFS)
Python

import cv2
import argparse
import enum
import abc
from typing import Optional
import numpy as np
class Board_Sizes_ABC(abc.ABC):
@abc.abstractmethod
def get_cell_rel_pos(self, n: int):
pass
@abc.abstractmethod
def get_num_cells(self) -> tuple[int, int]:
pass
def get_cell_position_on_line(self, n: int, p0: np.ndarray, p1: np.ndarray):
vec = p1 - p0
return np.round(p0 + vec*self.get_cell_rel_pos(n)).astype(np.int32)
def iterat_pos(self) -> tuple[tuple[float, float], tuple[int, int]]:
for x in range(self.N + 1):
for y in range(self.N + 1):
yield (x, y), (self.get_cell_rel_pos(x), self.get_cell_rel_pos(y))
class Board_19x19(Board_Sizes_ABC):
LINE_WIDTH = 1
CELL_SIZE = 21
PADDING = 11.5
N = 18
TOTAL_WIDTH = N*CELL_SIZE + (N+1)*LINE_WIDTH + 2*PADDING
def __init__(self):
self.rel_lut = [(self.PADDING + self.LINE_WIDTH*0.5 + (self.LINE_WIDTH + self.CELL_SIZE)*i)/self.TOTAL_WIDTH for i in range(19)]
def get_num_cells(self):
return (self.N+1, self.N+1)
def get_cell_rel_pos(self, n: int):
return self.rel_lut[n]
class VISION_PREPROCESSING_MODE(enum.Enum):
CANNY = "CANNY"
THRES_HOLD = "THRES_HOLD"
THRES = "THRES"
TEST_CANNY_HOLD = "TEST_CANNY_HOLD"
TEST_CANNY = "TEST_CANNY"
class Board_Detection_SM:
def __init__(self):
self.state = VISION_PREPROCESSING_MODE.CANNY
self.hold_time = 0
self.dection_history = []
self.jitter = 0
self.num_decetions = 0
self.num_double_decetions = 0
self.last_decetion_time = 0
self.last_double_detection_time = 0
def advance_state(self, detections: list[np.array]) -> Optional[np.array]:
detection = self.process_nun_decetions(detections)
match self.state:
case VISION_PREPROCESSING_MODE.CANNY:
if self.last_decetion_time > 50:
self.state = VISION_PREPROCESSING_MODE.THRES_HOLD
self.hold_time = 100
case VISION_PREPROCESSING_MODE.THRES_HOLD:
if self.hold_time == 0:
self.state = VISION_PREPROCESSING_MODE.THRES
else:
self.hold_time -= 1
case VISION_PREPROCESSING_MODE.THRES:
if self.last_decetion_time > 50 or self.jitter > 3 or self.num_double_decetions > 15:
self.state = VISION_PREPROCESSING_MODE.TEST_CANNY_HOLD
self.hold_time = 200
case VISION_PREPROCESSING_MODE.TEST_CANNY_HOLD:
if self.hold_time == 0:
self.state = VISION_PREPROCESSING_MODE.TEST_CANNY
else:
self.hold_time -= 1
case VISION_PREPROCESSING_MODE.TEST_CANNY:
if self.last_decetion_time < 10 and self.jitter < 3:
self.state = VISION_PREPROCESSING_MODE.CANNY
else:
self.state = VISION_PREPROCESSING_MODE.THRES_HOLD
self.hold_time = 100
case _:
raise ValueError("Invalid preprocessing mode")
return detection
def process_nun_decetions(self, detections: list[np.array]) -> Optional[np.array]:
num_decetions = len(detections)
dection = None
if num_decetions > 2:
self.num_double_decetions += 1
self.num_last_double_detection_time = 0
else:
if self.last_double_detection_time > 300:
self.num_double_decetions = 0
else:
self.last_double_detection_time = max(1000, self.last_double_detection_time)
if num_decetions == 1:
self.last_decetion_time = 0
self.dection_history.append(np.squeeze(detections[0], axis=1))
self.dection_history = self.dection_history[-10:]
else:
self.last_decetion_time += 1
if len(self.dection_history) > 0:
dection_history_np = np.array(self.dection_history)
avrage_detection = np.mean(dection_history_np, axis=0)
dection = avrage_detection.copy()
if len(self.dection_history) > 6:
self.jitter = 0
for i in dection_history_np:
self.jitter = np.max(np.abs(i - avrage_detection))
return dection
def create_info_text(self) -> list[str]:
return [
f"Preprocessing: {self.state.value}",
f"Double Dection: {self.num_double_decetions}",
f"Jitter: {self.jitter:.2f}",
f"Last Dection Time: {self.last_decetion_time}",
f"Last Double Dection Time: {self.last_double_detection_time}",
f"Hold Time: {self.hold_time}"
]
def detect_board(state: VISION_PREPROCESSING_MODE, frame: np.ndarray) -> tuple[list[np.array], np.ndarray]:
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if state == VISION_PREPROCESSING_MODE.CANNY or \
state == VISION_PREPROCESSING_MODE.TEST_CANNY or \
state == VISION_PREPROCESSING_MODE.TEST_CANNY_HOLD:
frame_proc = cv2.GaussianBlur(frame_gray, (3, 3), 0)
frame_proc = cv2.Canny(frame_proc, 130, 200)
elif state == VISION_PREPROCESSING_MODE.THRES_HOLD or state == VISION_PREPROCESSING_MODE.THRES:
frame_proc = cv2.GaussianBlur(frame_gray, (3, 3), 0)
frame_proc = cv2.adaptiveThreshold(frame_proc, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 19, 3)
else:
raise ValueError("Invalid preprocessing mode")
countours, _ = cv2.findContours(frame_proc, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
return list(
filter(lambda x: type(x) == np.ndarray,
map(filter_countours, countours)
)
), frame_proc
def filter_countours(c: np.array) -> Optional[np.array]:
esp = cv2.arcLength(c, True)*0.05
approx = cv2.approxPolyDP(c, esp, True)
# Stage 1: Has 4 corners
if len(approx) != 4:
return None
# Stage 2: Area is big enough
area = cv2.contourArea(approx)
if area < 1000:
return None
# Stage 3: Are all side findContourslengths similar
lengths = np.array([
np.linalg.norm(approx[0] - approx[1]),
np.linalg.norm(approx[1] - approx[2]),
np.linalg.norm(approx[2] - approx[3]),
np.linalg.norm(approx[3] - approx[0])
])
avrg_length = np.mean(lengths)
lengths_diff = np.abs(lengths)/avrg_length
if not (np.all(lengths_diff < 1.1) and np.all(lengths_diff > 0.90)):
return None
return approx
def draw_board_markers(f: np.array, conture: np.array, board_messurements: Board_Sizes_ABC):
N_line_positions = []
S_line_positions = []
W_line_positions = []
E_line_positions = []
for i in range(19):
N_line_positions.append(board_messurements.get_cell_position_on_line(i, conture[0], conture[1]))
E_line_positions.append(board_messurements.get_cell_position_on_line(i, conture[1], conture[2]))
S_line_positions.append(board_messurements.get_cell_position_on_line(i, conture[2], conture[3]))
W_line_positions.append(board_messurements.get_cell_position_on_line(i, conture[3], conture[0]))
N_line_positions = np.array(N_line_positions, dtype=np.int32)
E_line_positions = np.array(E_line_positions, dtype=np.int32)
S_line_positions = np.array(list(reversed(S_line_positions)), dtype=np.int32)
W_line_positions = np.array(list(reversed(W_line_positions)), dtype=np.int32)
cv2.circle(f, conture[0].astype(np.int32), 3, (0, 255, 255), -1)
cv2.circle(f, conture[1].astype(np.int32), 3, (0, 255, 255), -1)
cv2.circle(f, conture[2].astype(np.int32), 3, (0, 255, 255), -1)
cv2.circle(f, conture[3].astype(np.int32), 3, (0, 255, 255), -1)
for p in N_line_positions:
cv2.circle(f, p, 2, (0, 0, 255), -1)
for p in E_line_positions:
cv2.circle(f, p, 2, (0, 0, 255), -1)
for p in S_line_positions:
cv2.circle(f, p, 2, (0, 0, 255), -1)
for p in W_line_positions:
cv2.circle(f, p, 2, (0, 0, 255), -1)
for p0, p1 in zip(N_line_positions, S_line_positions):
cv2.line(f, tuple(p0), tuple(p1), (0, 0, 255), 1)
for i in range(19):
p2 = board_messurements.get_cell_position_on_line(i, p0, p1)
cv2.circle(f, p2, 2, (0, 0, 255), -1)
for p0, p1 in zip(E_line_positions, W_line_positions):
cv2.line(f, tuple(p0), tuple(p1), (0, 255, 0), 1)
for i in range(19):
p2 = board_messurements.get_cell_position_on_line(i, p0, p1)
cv2.circle(f, p2, 2, (0, 255, 0), -1)
cv2.drawContours(f, [conture], -1, (0, 255, 255), 1)
def corret_board_perspective(frame: np.array, conture: np.array, s=250):
p1 = conture.astype(np.float32)
p2 = np.array([
[s,0], # Top right
[0, 0], # Top Left
[0,s], # Bottom Right
[s,s], # Bottom Left
], dtype=np.float32)
T = cv2.getPerspectiveTransform(p1, p2)
res = cv2.warpPerspective(frame, T, (s, s))
res = res[0:s,0:s]
return res
def pad_center(frame: np.array, target_size: np.array):
f = frame[0:target_size[1],0:target_size[0]]
horiztontal_padding = (target_size[1]-f.shape[1]) // 2
vertical_padding = (target_size[0]-f.shape[0]) // 2
return cv2.copyMakeBorder(
f,
vertical_padding, vertical_padding,
horiztontal_padding, horiztontal_padding,
cv2.BORDER_CONSTANT, value=(0, 0, 0)
)