Idk
BIN
rpi/assets/models/best-unified.pt
Normal file
BIN
rpi/assets/models/epoch-115.pt
Normal file
BIN
rpi/assets/models/epoch-130.pt
Normal file
BIN
rpi/assets/models/epoch-150.pt
Normal file
BIN
rpi/assets/models/epoch-95.pt
Normal file
BIN
rpi/assets/models/unified.pt
Normal file
0
rpi/board-detector/__init__.py
Normal file
31
rpi/board-detector/debug.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from ultralytics import YOLO
|
||||||
|
from paths import *
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
model = YOLO(model_path)
|
||||||
|
|
||||||
|
# Load image
|
||||||
|
image = cv2.imread(img_path)
|
||||||
|
if image is None:
|
||||||
|
print(f"Failed to read {img_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
height, width = image.shape[:2]
|
||||||
|
|
||||||
|
warped = image # For now assume top-down view
|
||||||
|
|
||||||
|
# Run YOLO detection
|
||||||
|
results = model(warped)
|
||||||
|
res = results[0]
|
||||||
|
|
||||||
|
debug_img = res.plot() # draws boxes around detected objects
|
||||||
|
cv2.imshow("Detections", debug_img)
|
||||||
|
cv2.waitKey(0)
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
79
rpi/board-detector/main.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from paths import *
|
||||||
|
|
||||||
|
from ultralytics import YOLO
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
# Map class names to FEN characters
|
||||||
|
class_to_fen = {
|
||||||
|
'w_pawn': 'P',
|
||||||
|
'w_knight': 'N',
|
||||||
|
'w_bishop': 'B',
|
||||||
|
'w_rook': 'R',
|
||||||
|
'w_queen': 'Q',
|
||||||
|
'w_king': 'K',
|
||||||
|
'b_pawn': 'p',
|
||||||
|
'b_knight': 'n',
|
||||||
|
'b_bishop': 'b',
|
||||||
|
'b_rook': 'r',
|
||||||
|
'b_queen': 'q',
|
||||||
|
'b_king': 'k',
|
||||||
|
}
|
||||||
|
|
||||||
|
def prediction_to_fen(results, width, height):
|
||||||
|
|
||||||
|
# Initialize empty board
|
||||||
|
board = [['' for _ in range(8)] for _ in range(8)]
|
||||||
|
|
||||||
|
# Iterate through predictions
|
||||||
|
for result in results:
|
||||||
|
for box, cls in zip(result.boxes.xyxy, result.boxes.cls):
|
||||||
|
x1, y1, x2, y2 = box.tolist()
|
||||||
|
class_name = model.names[int(cls)]
|
||||||
|
fen_char = class_to_fen.get(class_name)
|
||||||
|
|
||||||
|
if fen_char:
|
||||||
|
# Compute board square
|
||||||
|
col = int((x1 + x2) / 2 / (width / 8))
|
||||||
|
row = 7 - int((y1 + y2) / 2 / (height / 8))
|
||||||
|
board[row][col] = fen_char
|
||||||
|
print(f"[{class_name}] {fen_char} {row} {col}")
|
||||||
|
|
||||||
|
# Convert board to FEN
|
||||||
|
fen_rows = []
|
||||||
|
for row in board:
|
||||||
|
fen_row = ''
|
||||||
|
empty_count = 0
|
||||||
|
for square in row:
|
||||||
|
if square == '':
|
||||||
|
empty_count += 1
|
||||||
|
else:
|
||||||
|
if empty_count > 0:
|
||||||
|
fen_row += str(empty_count)
|
||||||
|
empty_count = 0
|
||||||
|
fen_row += square
|
||||||
|
if empty_count > 0:
|
||||||
|
fen_row += str(empty_count)
|
||||||
|
fen_rows.append(fen_row)
|
||||||
|
|
||||||
|
# Join rows into a FEN string (default: white to move, all castling rights, no en passant)
|
||||||
|
fen_string = '/'.join(fen_rows) + ' w KQkq - 0 1'
|
||||||
|
return fen_string
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
img = cv2.imread(img_path)
|
||||||
|
height, width = img.shape[:2]
|
||||||
|
|
||||||
|
model = YOLO(model_path)
|
||||||
|
results = model.predict(source=img_path, conf=0.5)
|
||||||
|
|
||||||
|
#fen = prediction_to_fen(results, height, width)
|
||||||
|
#print("Predicted FEN:", fen)
|
||||||
|
|
||||||
|
annotated_image = results[0].plot() # Annotated image as NumPy array
|
||||||
|
cv2.namedWindow("YOLO Predictions", cv2.WINDOW_NORMAL) # make window resizable
|
||||||
|
cv2.imshow("YOLO Predictions", annotated_image)
|
||||||
|
cv2.waitKey(0)
|
||||||
|
cv2.destroyAllWindows()
|
||||||
3
rpi/board-detector/paths.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
model_path = "C:/Users/Laurent/Desktop/board-mate/rpi/assets/models/epoch-130.pt"
|
||||||
|
#img_path = "./test/4.jpg"
|
||||||
|
img_path = "../training/datasets/unified/train/images/WIN_20221220_11_27_27_Pro_jpg.rf.4f01cb68c8944ef1c4c7dc57847b4cd3.jpg"
|
||||||
BIN
rpi/board-detector/test/1.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
rpi/board-detector/test/2.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
rpi/board-detector/test/3.jpg
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
rpi/board-detector/test/4.jpg
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
rpi/board-detector/test/IMG_20251218_155300.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
rpi/board-detector/test/glass-board.jpg
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
rpi/board-detector/test/random_pieces.jpg
Normal file
|
After Width: | Height: | Size: 336 KiB |
BIN
rpi/board-detector/test/test.jpg
Normal file
|
After Width: | Height: | Size: 316 KiB |
@@ -10,3 +10,6 @@ requests
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
pyyaml
|
pyyaml
|
||||||
pyserial
|
pyserial
|
||||||
|
opencv-python
|
||||||
|
numpy
|
||||||
|
ultralytics
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ def create_char(location, pattern):
|
|||||||
|
|
||||||
# example code
|
# example code
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
setText("Hello world\nThis is an LCD test")
|
setText("Hello world\nThis is an LCD chesscog-bck")
|
||||||
setRGB(0,128,64)
|
setRGB(0,128,64)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
for c in range(0,255):
|
for c in range(0,255):
|
||||||
|
|||||||
0
rpi/training/__init__.py
Normal file
6
rpi/training/chess.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
train: C:/Users/Laurent/Desktop/board-mate/rpi/training/datasets/roboflow/labels-bck-bck-bck
|
||||||
|
val: C:/Users/Laurent/Desktop/board-mate/rpi/training/datasets/roboflow/labels-bck-bck-bck
|
||||||
|
|
||||||
|
nc: 12
|
||||||
|
names: ['w_pawn','w_knight','w_bishop','w_rook','w_queen','w_king',
|
||||||
|
'b_pawn','b_knight','b_bishop','b_rook','b_queen','b_king']
|
||||||
7
rpi/training/datasets/chesscog/data.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
train: ../train/
|
||||||
|
val: ../valid/
|
||||||
|
test: ../test/
|
||||||
|
|
||||||
|
nc: 12
|
||||||
|
names: ['w_pawn','w_knight','w_bishop','w_rook','w_queen','w_king',
|
||||||
|
'b_pawn','b_knight','b_bishop','b_rook','b_queen','b_king']
|
||||||
7
rpi/training/datasets/unified/data.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
train: ../train/
|
||||||
|
val: ../valid/
|
||||||
|
test: ../test/
|
||||||
|
|
||||||
|
nc: 12
|
||||||
|
names: ['w_pawn','w_knight','w_bishop','w_rook','w_queen','w_king',
|
||||||
|
'b_pawn','b_knight','b_bishop','b_rook','b_queen','b_king']
|
||||||
36
rpi/training/decrease_labels.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Configuration
|
||||||
|
# --------------------------
|
||||||
|
labels_dir = "datasets/visiope/test/labels"
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Process each label file
|
||||||
|
# --------------------------
|
||||||
|
for filename in os.listdir(labels_dir):
|
||||||
|
if not filename.endswith(".txt"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
txt_path = os.path.join(labels_dir, filename)
|
||||||
|
new_lines = []
|
||||||
|
|
||||||
|
with open(txt_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
parts = line.strip().split()
|
||||||
|
if len(parts) < 5:
|
||||||
|
continue # skip invalid lines
|
||||||
|
cls = int(parts[0])
|
||||||
|
cls -= 1 # subtract 1 from class index
|
||||||
|
cls = max(cls, 0) # ensure no negative indices
|
||||||
|
new_lines.append(" ".join([str(cls)] + parts[1:]))
|
||||||
|
|
||||||
|
# Overwrite file with updated indices
|
||||||
|
with open(txt_path, "w") as f:
|
||||||
|
f.write("\n".join(new_lines))
|
||||||
|
|
||||||
|
print(f"Updated {filename}")
|
||||||
|
|
||||||
|
print("All label files have been adjusted!")
|
||||||
70
rpi/training/labelizer.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
from ultralytics import YOLO
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Configuration
|
||||||
|
# --------------------------
|
||||||
|
model_path = "models/bck/best-3.pt" # your trained YOLO model
|
||||||
|
images_dir = "C:/Users/Laurent/Desktop/board-mate/rpi/training/datasets/universe/train/images"
|
||||||
|
labels_dir = "C:/Users/Laurent/Desktop/board-mate/rpi/training/datasets/universe/train/labels"
|
||||||
|
img_width = 640
|
||||||
|
img_height = 640
|
||||||
|
|
||||||
|
os.makedirs(labels_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Load model
|
||||||
|
# --------------------------
|
||||||
|
model = YOLO(model_path)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Mapping YOLO class index -> piece name (optional)
|
||||||
|
# --------------------------
|
||||||
|
names = ['w_pawn','w_knight','w_bishop','w_rook','w_queen','w_king',
|
||||||
|
'b_pawn','b_knight','b_bishop','b_rook','b_queen','b_king']
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Process images
|
||||||
|
# --------------------------
|
||||||
|
for img_file in os.listdir(images_dir):
|
||||||
|
if not img_file.lower().endswith((".png", ".jpg", ".jpeg")):
|
||||||
|
continue
|
||||||
|
|
||||||
|
img_path = os.path.join(images_dir, img_file)
|
||||||
|
img = cv2.imread(img_path)
|
||||||
|
if img is None:
|
||||||
|
print(f"Failed to read {img_file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
height, width = img.shape[:2]
|
||||||
|
|
||||||
|
# Run YOLO detection
|
||||||
|
results = model(img)
|
||||||
|
res = results[0]
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
boxes = res.boxes.xyxy.cpu().numpy() # [x1, y1, x2, y2]
|
||||||
|
classes = res.boxes.cls.cpu().numpy()
|
||||||
|
confs = res.boxes.conf.cpu().numpy()
|
||||||
|
|
||||||
|
for box, cls, conf in zip(boxes, classes, confs):
|
||||||
|
if conf < 0.5: # skip low-confidence predictions
|
||||||
|
continue
|
||||||
|
|
||||||
|
x1, y1, x2, y2 = box
|
||||||
|
x_center = (x1 + x2) / 2 / width
|
||||||
|
y_center = (y1 + y2) / 2 / height
|
||||||
|
w_norm = (x2 - x1) / width
|
||||||
|
h_norm = (y2 - y1) / height
|
||||||
|
|
||||||
|
lines.append(f"{int(cls)} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}")
|
||||||
|
|
||||||
|
# Save YOLO .txt file with same basename as image
|
||||||
|
txt_path = os.path.join(labels_dir, os.path.splitext(img_file)[0] + ".txt")
|
||||||
|
with open(txt_path, "w") as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
|
print(f"Pre-labeled {img_file} -> {txt_path}")
|
||||||
|
|
||||||
|
print("All images have been pre-labeled!")
|
||||||
25
rpi/training/move_image.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Configuration
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
source_folder = "datasets/twhpv/valid/images"
|
||||||
|
destination_folder = "datasets/_unified/valid/images"
|
||||||
|
|
||||||
|
os.makedirs(destination_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# Supported image extensions
|
||||||
|
image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif"]
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Copy images
|
||||||
|
# ----------------------------
|
||||||
|
for filename in os.listdir(source_folder):
|
||||||
|
if any(filename.lower().endswith(ext) for ext in image_extensions):
|
||||||
|
src_path = os.path.join(source_folder, filename)
|
||||||
|
dst_path = os.path.join(destination_folder, filename)
|
||||||
|
shutil.copy2(src_path, dst_path) # copy2 preserves metadata
|
||||||
|
|
||||||
|
print(f"All images copied to '{destination_folder}'")
|
||||||
59
rpi/training/sort_labels.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Configuration
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
src_dir = "datasets/visiope/test/labels"
|
||||||
|
dest_dir = "datasets/_unified/test/labels"
|
||||||
|
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Reference class order you want to follow
|
||||||
|
"""[
|
||||||
|
'w_pawn','w_knight','w_bishop','w_rook','w_queen','w_king',
|
||||||
|
'b_pawn','b_knight','b_bishop','b_rook','b_queen','b_king'
|
||||||
|
]"""
|
||||||
|
|
||||||
|
reference_classes = [
|
||||||
|
'w_pawn','w_knight','w_bishop','w_rook','w_queen','w_king',
|
||||||
|
'b_pawn','b_knight','b_bishop','b_rook','b_queen','b_king'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Current class order in your dataset (change this to match your dataset!)
|
||||||
|
current_classes = ['bishop', 'black-bishop', 'black-king', 'black-knight', 'black-pawn', 'black-queen', 'black-rook',
|
||||||
|
'white-bishop', 'white-king', 'white-knight', 'white-pawn', 'white-queen', 'white-rook']
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Build index mapping
|
||||||
|
# ----------------------------
|
||||||
|
index_map = {current_classes.index(cls): reference_classes.index(cls) for cls in current_classes}
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Process each label file
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for filename in os.listdir(src_dir):
|
||||||
|
if filename.endswith(".txt"):
|
||||||
|
input_path = os.path.join(src_dir, filename)
|
||||||
|
output_path = os.path.join(dest_dir, filename)
|
||||||
|
|
||||||
|
with open(input_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
new_lines = []
|
||||||
|
for line in lines:
|
||||||
|
parts = line.strip().split()
|
||||||
|
old_idx = int(parts[0])
|
||||||
|
new_idx = index_map[old_idx]
|
||||||
|
new_lines.append(" ".join([str(new_idx)] + parts[1:]))
|
||||||
|
|
||||||
|
with open(output_path, "w") as f:
|
||||||
|
f.write("\n".join(new_lines))
|
||||||
|
|
||||||
|
if count%100 == 0:
|
||||||
|
print(count)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print(f"All labels remapped and saved to '{dest_dir}'")
|
||||||
18
rpi/training/training.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from ultralytics import YOLO
|
||||||
|
|
||||||
|
def main():
|
||||||
|
model = YOLO("models/yolo11n.pt")
|
||||||
|
model.train(
|
||||||
|
data="./datasets/unified/data.yaml",
|
||||||
|
epochs=200,
|
||||||
|
patience=30,
|
||||||
|
imgsz=640,
|
||||||
|
batch=12,
|
||||||
|
device=0,
|
||||||
|
project="result",
|
||||||
|
name="unified-training",
|
||||||
|
exist_ok=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||