diff --git a/rpi/assets/models/best-unified.pt b/rpi/assets/models/best-unified.pt new file mode 100644 index 00000000..268d6f1a Binary files /dev/null and b/rpi/assets/models/best-unified.pt differ diff --git a/rpi/assets/models/epoch-115.pt b/rpi/assets/models/epoch-115.pt new file mode 100644 index 00000000..a5756ec3 Binary files /dev/null and b/rpi/assets/models/epoch-115.pt differ diff --git a/rpi/assets/models/epoch-130.pt b/rpi/assets/models/epoch-130.pt new file mode 100644 index 00000000..eb066530 Binary files /dev/null and b/rpi/assets/models/epoch-130.pt differ diff --git a/rpi/assets/models/epoch-150.pt b/rpi/assets/models/epoch-150.pt new file mode 100644 index 00000000..58da544e Binary files /dev/null and b/rpi/assets/models/epoch-150.pt differ diff --git a/rpi/assets/models/epoch-95.pt b/rpi/assets/models/epoch-95.pt new file mode 100644 index 00000000..2ee07b6a Binary files /dev/null and b/rpi/assets/models/epoch-95.pt differ diff --git a/rpi/assets/models/unified.pt b/rpi/assets/models/unified.pt new file mode 100644 index 00000000..d7a86778 Binary files /dev/null and b/rpi/assets/models/unified.pt differ diff --git a/rpi/board-detector/__init__.py b/rpi/board-detector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rpi/board-detector/debug.py b/rpi/board-detector/debug.py new file mode 100644 index 00000000..f01eeec8 --- /dev/null +++ b/rpi/board-detector/debug.py @@ -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() \ No newline at end of file diff --git a/rpi/board-detector/main.py b/rpi/board-detector/main.py new file mode 100644 index 00000000..cf8cc8e3 --- /dev/null +++ b/rpi/board-detector/main.py @@ -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() \ No newline at end of file diff --git a/rpi/board-detector/paths.py b/rpi/board-detector/paths.py new file mode 100644 index 00000000..5a6d5e27 --- /dev/null +++ b/rpi/board-detector/paths.py @@ -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" diff --git a/rpi/board-detector/test/1.png b/rpi/board-detector/test/1.png new file mode 100644 index 00000000..a089fc47 Binary files /dev/null and b/rpi/board-detector/test/1.png differ diff --git a/rpi/board-detector/test/2.webp b/rpi/board-detector/test/2.webp new file mode 100644 index 00000000..1df029bc Binary files /dev/null and b/rpi/board-detector/test/2.webp differ diff --git a/rpi/board-detector/test/3.jpg b/rpi/board-detector/test/3.jpg new file mode 100644 index 00000000..c6d7aaa5 Binary files /dev/null and b/rpi/board-detector/test/3.jpg differ diff --git a/rpi/board-detector/test/4.jpg b/rpi/board-detector/test/4.jpg new file mode 100644 index 00000000..341b6e44 Binary files /dev/null and b/rpi/board-detector/test/4.jpg differ diff --git a/rpi/board-detector/test/IMG_20251218_155300.jpg b/rpi/board-detector/test/IMG_20251218_155300.jpg new file mode 100644 index 00000000..f27f643a Binary files /dev/null and b/rpi/board-detector/test/IMG_20251218_155300.jpg differ diff --git a/rpi/board-detector/test/glass-board.jpg b/rpi/board-detector/test/glass-board.jpg new file mode 100644 index 00000000..d999e530 Binary files /dev/null and b/rpi/board-detector/test/glass-board.jpg differ diff --git a/rpi/board-detector/test/random_pieces.jpg b/rpi/board-detector/test/random_pieces.jpg new file mode 100644 index 00000000..0296cbfd Binary files /dev/null and b/rpi/board-detector/test/random_pieces.jpg differ diff --git a/rpi/board-detector/test/test.jpg b/rpi/board-detector/test/test.jpg new file mode 100644 index 00000000..7dbc7ee4 Binary files /dev/null and b/rpi/board-detector/test/test.jpg differ diff --git a/rpi/requirements.txt b/rpi/requirements.txt index fc9f9584..fc732441 100644 --- a/rpi/requirements.txt +++ b/rpi/requirements.txt @@ -9,4 +9,7 @@ typing-extensions requests python-dotenv pyyaml -pyserial \ No newline at end of file +pyserial +opencv-python +numpy +ultralytics diff --git a/rpi/scripts/timer/grove_rgb_lcd.py b/rpi/scripts/timer/grove_rgb_lcd.py index 4283f69d..41a6e8e3 100644 --- a/rpi/scripts/timer/grove_rgb_lcd.py +++ b/rpi/scripts/timer/grove_rgb_lcd.py @@ -111,7 +111,7 @@ def create_char(location, pattern): # example code if __name__=="__main__": - setText("Hello world\nThis is an LCD test") + setText("Hello world\nThis is an LCD chesscog-bck") setRGB(0,128,64) time.sleep(2) for c in range(0,255): diff --git a/rpi/training/__init__.py b/rpi/training/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rpi/training/chess.yaml b/rpi/training/chess.yaml new file mode 100644 index 00000000..588dc9a7 --- /dev/null +++ b/rpi/training/chess.yaml @@ -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'] \ No newline at end of file diff --git a/rpi/training/datasets/chesscog/data.yaml b/rpi/training/datasets/chesscog/data.yaml new file mode 100644 index 00000000..9e3904e8 --- /dev/null +++ b/rpi/training/datasets/chesscog/data.yaml @@ -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'] \ No newline at end of file diff --git a/rpi/training/datasets/unified/data.yaml b/rpi/training/datasets/unified/data.yaml new file mode 100644 index 00000000..9e3904e8 --- /dev/null +++ b/rpi/training/datasets/unified/data.yaml @@ -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'] \ No newline at end of file diff --git a/rpi/training/decrease_labels.py b/rpi/training/decrease_labels.py new file mode 100644 index 00000000..3276ddb8 --- /dev/null +++ b/rpi/training/decrease_labels.py @@ -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!") \ No newline at end of file diff --git a/rpi/training/labelizer.py b/rpi/training/labelizer.py new file mode 100644 index 00000000..5d632bb4 --- /dev/null +++ b/rpi/training/labelizer.py @@ -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!") diff --git a/rpi/training/move_image.py b/rpi/training/move_image.py new file mode 100644 index 00000000..a6af7d59 --- /dev/null +++ b/rpi/training/move_image.py @@ -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}'") diff --git a/rpi/training/sort_labels.py b/rpi/training/sort_labels.py new file mode 100644 index 00000000..e57433ba --- /dev/null +++ b/rpi/training/sort_labels.py @@ -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}'") diff --git a/rpi/training/training.py b/rpi/training/training.py new file mode 100644 index 00000000..3bbf2e62 --- /dev/null +++ b/rpi/training/training.py @@ -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() \ No newline at end of file