Skip to content

Commit

Permalink
Merge pull request #8 from georgw777/develop
Browse files Browse the repository at this point in the history
v1.0.0
  • Loading branch information
georg-wolflein authored Jan 7, 2021
2 parents 76d827d + 7ae3cb7 commit 137ff94
Show file tree
Hide file tree
Showing 100 changed files with 8,416 additions and 5,488 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/sphinx_docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: publish-docs

env:
GITHUB_ACTOR: georgw777
GITHUB_REPOSITORY: georgw777/chesscog
GITHUB_TOKEN: ${{ secrets.CHESSCOG_PAT_TOKEN }}

on:
push:
branches: [master]

jobs:
build_sphinx_job:
runs-on: ubuntu-latest
container: debian:buster-slim

steps:
- name: Get prerequisites and clone repository
env:
GITHUB_TOKEN: ${{ secrets.CHESSCOG_PAT_TOKEN }}
run: |
set -x
apt-get update
apt-get install -y git curl
git clone -b master "https://token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" .
shell: bash
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install Poetry
uses: snok/install-poetry@v1.1.1
with:
virtualenvs-create: false
- name: Install dependencies
run: poetry install -E docs
- name: Run build script for Sphinx pages
env:
GITHUB_TOKEN: ${{ secrets.CHESSCOG_PAT_TOKEN }}
run: |
cd docs
./buildsite.sh
shell: bash
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

_chesscog_ combines traditional computer vision techniques with deep learning to recognise chess positions from photos.

I am developing this project as part of my master thesis at the University of St Andrews.
I am developed this project as part of my [master thesis](https://github.com/georgw777/chesscog-report) at the University of St Andrews. Documentation is available [here](https://georgw777.github.io/chesscog).

## Related repositories

Expand All @@ -14,8 +14,8 @@ I am developing this project as part of my master thesis at the University of St

## Demo

See it in action at [**chesscog.com**](https://www.chesscog.com)!
![Screenshot](docs/demo_screenshot.png)
See it in action at [chesscog.com](https://www.chesscog.com)!
![Screenshot](https://github.com/georgw777/chesscog/raw/master/docs/demo_screenshot.png)

## Background

Expand All @@ -28,7 +28,7 @@ The goal of this project is to develop a system that is able to map a photo of a
The chess recognition system is trained using a dataset of ~5,000 synthetically generated images of chess positions (3D renderings of different chess positions at various camera angles and lighting conditions).
At a high level, the recognition system itself consists of the following pipeline:

1. square and corner detection
2. occupancy detection
1. board localisation (square and corner detection)
2. occupancy classification
3. piece classification
4. probabilistic refinement of predictions based on legal and likely positions
4. post-processing to generate the FEN string
3 changes: 3 additions & 0 deletions chesscog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Chess position inference using computer vision.
"""

import sys
import logging

Expand Down
2 changes: 1 addition & 1 deletion chesscog/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.7"
__version__ = "1.0.0"
25 changes: 24 additions & 1 deletion chesscog/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,27 @@
import functools
from collections.abc import Iterable

#: Device to be used for computation (GPU if available, else CPU).
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

T = typing.Union[torch.Tensor, torch.nn.Module, typing.List[torch.Tensor],
tuple, dict, typing.Generator]


def device(x: T, dev: str = DEVICE) -> T:
"""Convenience method to move a tensor/module/other structure containing tensors to the device.
Args:
x (T): the tensor (or strucure containing tensors)
dev (str, optional): the device to move the tensor to. Defaults to DEVICE.
Raises:
TypeError: if the type was not a compatible tensor
Returns:
T: the input tensor moved to the device
"""

to = functools.partial(device, dev=dev)
if isinstance(x, (torch.Tensor, torch.nn.Module)):
return x.to(dev)
Expand Down Expand Up @@ -46,7 +60,16 @@ def sort_corner_points(points: np.ndarray) -> np.ndarray:
return points


def listify(func):
def listify(func: typing.Callable[..., typing.Iterable]) -> typing.Callable[..., typing.List]:
"""Decorator to convert the output of a generator function to a list.
Args:
func (typing.Callable[..., typing.Iterable]): the function to be decorated
Returns:
typing.Callable[..., typing.List]: the decorated function
"""

@functools.wraps(func)
def wrapper(*args, **kwargs):
return list(func(*args, **kwargs))
Expand Down
19 changes: 19 additions & 0 deletions chesscog/core/coordinates.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
"""Utility functions to convert between Cartesian and homogenous coordinates.
"""

import numpy as np


def to_homogenous_coordinates(coordinates: np.ndarray) -> np.ndarray:
"""Convert Cartesian to homogenous coordinates.
Args:
coordinates (np.ndarray): the Cartesian coordinates (shape: [..., 2])
Returns:
np.ndarray: the homogenous coordinates (shape: [..., 3])
"""
return np.concatenate([coordinates,
np.ones((*coordinates.shape[:-1], 1))], axis=-1)


def from_homogenous_coordinates(coordinates: np.ndarray) -> np.ndarray:
"""Convert homogenous to Cartesian coordinates.
Args:
coordinates (np.ndarray): the homogenous coordinates (shape: [..., 3])
Returns:
np.ndarray: the Cartesian coordinates (shape: [..., 2])
"""
return coordinates[..., :2] / coordinates[..., 2, np.newaxis]
3 changes: 3 additions & 0 deletions chesscog/core/dataset/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Module for handling datasets.
"""

from .dataset import color_name, piece_name, name_to_piece, build_transforms, build_dataset, build_data_loader
from .transforms import unnormalize, build_transforms
from .datasets import Datasets
48 changes: 47 additions & 1 deletion chesscog/core/dataset/dataset.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Methods specific to handling chess datasets.
"""

import torch
import torchvision
import typing
Expand All @@ -13,30 +16,73 @@
logger = logging.getLogger(__name__)


def color_name(color: chess.Color):
def color_name(color: chess.Color) -> str:
"""Convert a chess color to a string.
Args:
color (chess.Color): the color
Returns:
str: the string representation
"""
return {chess.WHITE: "white",
chess.BLACK: "black"}[color]


def piece_name(piece: chess.Piece) -> str:
"""Convert a chess piece to a string.
Args:
piece (chess.Piece): the piece
Returns:
str: the corresponding string
"""
return f"{color_name(piece.color)}_{chess.piece_name(piece.piece_type)}"


def name_to_piece(name: str) -> chess.Piece:
"""Convert the name of a piece to an instance of :class:`chess.Piece`.
Args:
name (str): the name of the piece
Returns:
chess.Piece: the instance of :class:`chess.Piece`
"""
color, piece_type = name.split("_")
color = color == "white"
piece_type = chess.PIECE_NAMES.index(piece_type)
return chess.Piece(piece_type, color)


def build_dataset(cfg: CN, mode: Datasets) -> torch.utils.data.Dataset:
"""Build a dataset from its configuration.
Args:
cfg (CN): the config object
mode (Datasets): the split (important to figure out which transforms to apply)
Returns:
torch.utils.data.Dataset: the dataset
"""
transform = build_transforms(cfg, mode)
dataset = torchvision.datasets.ImageFolder(root=URI(cfg.DATASET.PATH) / mode.value,
transform=transform)
return dataset


def build_data_loader(cfg: CN, dataset: torch.utils.data.Dataset, mode: Datasets) -> torch.utils.data.DataLoader:
"""Build a data loader for a dataset.
Args:
cfg (CN): the config object
dataset (torch.utils.data.Dataset): the dataset
mode (Datasets): the split
Returns:
torch.utils.data.DataLoader: the data loader
"""
shuffle = mode in {Datasets.TRAIN, Datasets.VAL}

return torch.utils.data.DataLoader(dataset, batch_size=cfg.DATASET.BATCH_SIZE,
Expand Down
2 changes: 2 additions & 0 deletions chesscog/core/dataset/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@


class Datasets(Enum):
"""Enumeration of the dataset split.
"""
TRAIN = "train"
VAL = "val"
TEST = "test"
20 changes: 19 additions & 1 deletion chesscog/core/dataset/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@


def unnormalize(x: typing.Union[torch.Tensor, np.ndarray]) -> typing.Union[torch.Tensor, np.ndarray]:
# x must be of the form ([..., W, H, 3])
"""Unnormalize an input image.
It must be of the form ([..., W, H, 3]).
Args:
x (typing.Union[torch.Tensor, np.ndarray]): the input tensor/array representing the image
Returns:
typing.Union[torch.Tensor, np.ndarray]: the unnormalized image
"""
return x * _STD + _MEAN


Expand Down Expand Up @@ -104,6 +113,15 @@ def __call__(self, img: Image) -> Image:


def build_transforms(cfg: CN, mode: Datasets) -> typing.Callable:
"""Build the transforms for a dataset.
Args:
cfg (CN): the config object
mode (Datasets): the dataset split
Returns:
typing.Callable: the transform function
"""
transforms = cfg.DATASET.TRANSFORMS
t = []
if transforms.CENTER_CROP:
Expand Down
23 changes: 23 additions & 0 deletions chesscog/core/evaluation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Common functions for evaluation CNNs.
"""

import argparse
import torch
import torchvision
Expand Down Expand Up @@ -44,6 +47,21 @@ def class_headings(metric: str) -> typing.List[str]:


def evaluate(model_path: Path, datasets: typing.List[Datasets], output_folder: Path, find_mistakes: bool = False, include_heading: bool = False) -> str:
"""Evaluate a model, returning the results as CSV.
Args:
model_path (Path): path to the model folder containing the YAML file and the saved weights
datasets (typing.List[Datasets]): the datasets to evaluate on
output_folder (Path): output folder for the mistake images (if applicable)
find_mistakes (bool, optional): whether to output all mistakes as images to the output folder. Defaults to False.
include_heading (bool, optional): whether to include a heading in the CSV output. Defaults to False.
Raises:
ValueError: if the YAML config file is missing
Returns:
str: the CSV string
"""
model_name = model_path.stem
config_file = model_path.parent / f"{model_name}.yaml"
if not config_file.exists():
Expand Down Expand Up @@ -90,6 +108,11 @@ def evaluate(model_path: Path, datasets: typing.List[Datasets], output_folder: P


def perform_evaluation(classifier: str):
"""Function to set up the CLI for the evaluation script.
Args:
classifier (str): the classifier
"""
parser = argparse.ArgumentParser(description="Evaluate trained models.")
parser.add_argument("--model", help=f"the model to evaluate (if unspecified, all models in 'runs://{classifier}' will be evaluated)",
type=str, default=None)
Expand Down
10 changes: 10 additions & 0 deletions chesscog/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
"""Core exceptions.
"""


class RecognitionException(Exception):
"""Exception representing an error in the chess recognition pipeline.
"""

def __init__(self, message: str = "unknown error"):
super().__init__("chess recognition error: " + message)


class ChessboardNotLocatedException(RecognitionException):
"""Exception if the chessboard could not be located.
"""

def __init__(self, reason: str = None):
message = "chessboard could not be located"
if reason:
Expand Down
5 changes: 5 additions & 0 deletions chesscog/core/io/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""Core file system operations.
This module is included by default to set up the paths for use with :class:`recap.URI`.
"""

from pathlib import Path
import os
from recap.path_manager import register_translator
Expand Down
Loading

0 comments on commit 137ff94

Please sign in to comment.