add initial chess diagram scanner - randomcrap - random crap programs of varying quality
 (HTM) git clone git://git.codemadness.org/randomcrap
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 97d003ba8b60c71862053019f76a2b1ea2c863c7
 (DIR) parent e14b3d0828974aeefde224dac8f92b3749d39e8c
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Sat,  2 Aug 2025 13:14:51 +0200
       
       add initial chess diagram scanner
       
       Diffstat:
         A opencv/chess-diagram/LICENSE        |      15 +++++++++++++++
         A opencv/chess-diagram/README         |      44 +++++++++++++++++++++++++++++++
         A opencv/chess-diagram/board_styles/… |       0 
         A opencv/chess-diagram/board_styles/… |       0 
         A opencv/chess-diagram/board_styles/… |       0 
         A opencv/chess-diagram/board_styles/… |       0 
         A opencv/chess-diagram/board_styles/… |       3 +++
         A opencv/chess-diagram/board_styles/… |       0 
         A opencv/chess-diagram/board_styles/… |       0 
         A opencv/chess-diagram/board_styles/… |       0 
         A opencv/chess-diagram/examples/wood… |       0 
         A opencv/chess-diagram/main.py        |     633 +++++++++++++++++++++++++++++++
       
       12 files changed, 695 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/opencv/chess-diagram/LICENSE b/opencv/chess-diagram/LICENSE
       @@ -0,0 +1,15 @@
       +ISC License
       +
       +Copyright (c) 2025 Hiltjo Posthuma <hiltjo@codemadness.org>
       +
       +Permission to use, copy, modify, and/or distribute this software for any
       +purpose with or without fee is hereby granted, provided that the above
       +copyright notice and this permission notice appear in all copies.
       +
       +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 (DIR) diff --git a/opencv/chess-diagram/README b/opencv/chess-diagram/README
       @@ -0,0 +1,44 @@
       +chess diagram scanner
       +
       +
       +# Usage:
       +
       +python3 main.py < someimage
       +
       +Output is in the FEN format, one line per diagram.
       +
       +
       +# Dependencies
       +
       +- python3
       +- opencv2
       +
       +
       +# Features
       +
       +- Automatically detect the board style.
       +- Simple FEN output and easy to use this way with other chess programs.
       +- Check basic castle rights.
       +
       +
       +# How to add a board style:
       +
       +- Setup the pieces on the board using FEN boardstyles/fen.txt.
       +- Save the board has a high-resolution PNG image in boardstyles/. 
       +
       +
       +# Limitations and room for improvement
       +
       +There are many limitations and there is much room for improvement.
       +This diagram scanner was written to convert diagrams from the book "The
       +Woodpecker Method" to a digital format.
       +
       +- Does not detect flipped boards yet (only from white its side).
       +- There should probably be some image filter/threshold for pieces, so
       +  highlighted squares or slightly different piece colors are better detected.
       +- Scan speed could still be faster (0.5 seconds per diagram on my machine).
       +  For example it would be cool to be able to detect boards very fast from a video
       +  stream (although using the Lichess API makes more sense here).
       +- No support for misalignment/skewed images yet.
       +  This could be useful for photographed or physically scanned books.
       +  But maybe this is out of the scope of this program and could be a pre-pass.
 (DIR) diff --git a/opencv/chess-diagram/board_styles/chess_com_neo_brown.png b/opencv/chess-diagram/board_styles/chess_com_neo_brown.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/board_styles/chess_com_neo_darkblue.png b/opencv/chess-diagram/board_styles/chess_com_neo_darkblue.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/board_styles/chess_com_neo_green.png b/opencv/chess-diagram/board_styles/chess_com_neo_green.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/board_styles/chess_com_neo_wood.png b/opencv/chess-diagram/board_styles/chess_com_neo_wood.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/board_styles/fen.txt b/opencv/chess-diagram/board_styles/fen.txt
       @@ -0,0 +1,2 @@
       +FEN:
       +        rnbqkbnr/pppppppp/8/3qk3/3QK3/8/PPPPPPPP/RNBQKBNR w HAha - 0 1
       +\ No newline at end of file
 (DIR) diff --git a/opencv/chess-diagram/board_styles/lichess.png b/opencv/chess-diagram/board_styles/lichess.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/board_styles/woodpecker.png b/opencv/chess-diagram/board_styles/woodpecker.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/board_styles/xboard_wood.png b/opencv/chess-diagram/board_styles/xboard_wood.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/examples/woodpecker_p1_easy.png b/opencv/chess-diagram/examples/woodpecker_p1_easy.png
       Binary files differ.
 (DIR) diff --git a/opencv/chess-diagram/main.py b/opencv/chess-diagram/main.py
       @@ -0,0 +1,633 @@
       +from skimage import data, img_as_float
       +
       +from operator import itemgetter
       +
       +import cv2
       +
       +import glob
       +import math
       +import numpy as np
       +import os
       +import sys
       +import time
       +
       +config = {
       +        "debug": True,
       +        "debugimage": True,
       +        "checkmoveindicator": True,
       +        "boardstyle": "lichess", # "lichess", "woodpecker"
       +        "guessstyle": True,
       +        "flip": False, # scanned board is flipped?
       +        "flipdisplay": False # output should be flipped?
       +}
       +
       +CHESS_BOARD_STYLE_DIR = glob.glob('board_styles/*.png')
       +
       +boardstyles = {}
       +
       +# Initiate SIFT detector
       +sift = cv2.SIFT_create()
       +
       +# [ x, y, square color, piece ],
       +# table to read each piece on a different squared color.
       +# and also an empty square for each color.
       +readtab = [
       +        # black
       +        [ 0, 0, "w", "r" ],
       +        [ 1, 0, "b", "n" ],
       +        [ 2, 0, "w", "b" ],
       +        [ 3, 0, "b", "q" ],
       +        [ 4, 0, "w", "k" ],
       +        [ 5, 0, "b", "b" ],
       +        [ 6, 0, "w", "n" ],
       +        [ 7, 0, "b", "r" ],
       +        [ 0, 1, "b", "p" ],
       +        [ 1, 1, "w", "p" ],
       +        [ 0, 2, "w", "" ], # empty
       +        [ 1, 2, "b", "" ],
       +        [ 3, 3, "w", "q" ],
       +        [ 4, 3, "b", "k" ],
       +        # white
       +        [ 0, 7, "b", "R" ],
       +        [ 1, 7, "w", "N" ],
       +        [ 2, 7, "b", "B" ],
       +        [ 3, 7, "w", "Q" ],
       +        [ 4, 7, "b", "K" ],
       +        [ 5, 7, "w", "B" ],
       +        [ 6, 7, "b", "N" ],
       +        [ 7, 7, "w", "R" ],
       +        [ 0, 6, "w", "P" ],
       +        [ 1, 6, "b", "P" ],
       +        [ 0, 5, "b", "" ], # empty
       +        [ 1, 5, "w", "" ],
       +        [ 3, 4, "b", "Q" ],
       +        [ 4, 4, "w", "K" ],
       +]
       +
       +# read input board setup data to compare to.
       +for path in CHESS_BOARD_STYLE_DIR:
       +        basename = os.path.basename(path)
       +        name = basename[:-4] # remove ".png"
       +
       +        # do not load board styles that are not used anyway.
       +        if "guessstyle" in config and config["guessstyle"] == False and \
       +        "boardstyle" in config and config["boardstyle"] != "" and name != config["boardstyle"]:
       +                continue
       +
       +        # read board style image
       +        img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
       +
       +        # split fields as images in board image
       +        height = len(img)
       +        width = len(img[0])
       +        # square width and height, fixed-size, 8x8 squares.
       +        sw = width / 8
       +        sh = height / 8
       +
       +        style = {
       +                "w": {},
       +                "b": {}
       +        }
       +
       +        # get each field and expected configuration, store them as template:
       +        # pieces (per square), empty squares.
       +        for t in readtab:
       +                # x, y position is floored/truncated
       +                x1 = int(t[0] * sw)
       +                y1 = int(t[1] * sh)
       +                x2 = int(x1 + sw)
       +                y2 = int(y1 + sh)
       +
       +                region = img[y1:y2, x1:x2]
       +
       +                # style[squarecolor][piece] = image data
       +                style[t[2]][t[3]] = region
       +
       +        boardstyles[name] = style
       +
       +def debug(s):
       +        if not config["debug"]:
       +                return
       +        print("DEBUG: " + s, file=sys.stderr, flush=True)
       +
       +def iswhitesquare(x, y):
       +        if (y & 1) == 0:
       +                return ((x + 1) & 1)
       +        else:
       +                return (x & 1)
       +
       +def img_resize(img, sw, sh, dw, dh):
       +        # resize piece relative to board size (approximately).
       +        #interp = cv2.INTER_CUBIC # enlarge: use cv2.INTER_LINEAR (fast) or cv2.INTER_CUBIC (slow)
       +
       +        # NOTE: not scaled for quality, because we use MSE. Sampling would change pixel value and
       +        # reduce matching results.
       +        interp = cv2.INTER_AREA
       +        #interp = cv2.INTER_LINEAR
       +
       +        return cv2.resize(img, (dw, dh), interpolation=interp)
       +
       +# https://docs.opencv.org/3.4/d1/de0/tutorial_py_feature_homography.html
       +def find_pieces_flann(desimg, despiece):
       +        FLANN_INDEX_KDTREE = 1
       +        index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
       +        search_params = dict(checks = 50)
       +        flann = cv2.FlannBasedMatcher(index_params, search_params)
       +
       +        matches = flann.knnMatch(desimg, despiece, k=2)
       +        # store all the good matches as per Lowe's ratio test.
       +        # https://docs.opencv.org/3.4/d5/d6f/tutorial_feature_flann_matcher.html
       +        good = []
       +        for m, n in matches:
       +                # NOTE: was: 0.7 (reference value from above webpage)
       +                if m.distance < 0.3 * n.distance:
       +                        good.append(m)
       +
       +        return good
       +
       +def board2txt(boardstate, flip):
       +        output = ""
       +        empty = 0
       +        matches = boardstate["matches"]
       +        nrows = len(matches)
       +        for iy in range(nrows):
       +                if flip:
       +                        y = 7 - iy
       +                else:
       +                        y = iy
       +                if iy > 0:
       +                        output = output + ("\n---+---+---+---+---+---+----+---\n")
       +                row = matches[y]
       +                nitems = len(row)
       +                for ix in range(nitems):
       +                        if flip:
       +                                x = 7 - ix
       +                        else:
       +                                x = ix
       +                        match = matches[y][x]
       +                        c = match["piece"]
       +                        if c == "":
       +                                c = " "
       +
       +                        output = output + (" %s |" % (c))
       +
       +        output = output + ("\n---+---+---+---+---+---+----+---\n")
       +
       +        return output
       +
       +def board2fen(boardstate, flip):
       +        output = ""
       +        empty = 0
       +        matches = boardstate["matches"]
       +        nrows = len(matches)
       +        for iy in range(nrows):
       +                if flip:
       +                        y = 7 - iy
       +                else:
       +                        y = iy
       +                if empty > 0:
       +                        output = output + ("%d" % empty)
       +                        empty = 0
       +
       +                if iy > 0:
       +                        output = output + ("/")
       +
       +                row = matches[y]
       +                nitems = len(row)
       +                for ix in range(nitems):
       +                        if flip:
       +                                x = 7 - ix
       +                        else:
       +                                x = ix
       +                        match = matches[y][x]
       +                        if not "piece" in match or match["piece"] == "":
       +                                empty = empty + 1
       +                                continue
       +
       +                        if empty > 0:
       +                                output = output + ("%d" % empty)
       +                                empty = 0
       +                        output = output + match["piece"]
       +
       +        if empty > 0:
       +                output = output + ("%d" % empty)
       +
       +        castle = "KQkq"
       +        if "castle" in boardstate:
       +                castle = boardstate["castle"]
       +        if castle == "":
       +                castle = "-"
       +
       +        # workaround: we do not know the turn or move number (for now).
       +        output = output + " " + boardstate["side"] + " " + castle + " - 0 1"
       +
       +        return output
       +
       +def board2lichess(boardstate, flip):
       +        output = board2fen(boardstate, flip)
       +        output = output.replace(" ", "_")
       +        output = "https://lichess.org/editor/" + output + "?color=white"
       +#        output = "https://lichess.org/analysis/" + output + "?color=white"
       +
       +        return output
       +
       +# sift cache for pieces, if dimensions are the same.
       +siftcache = {}
       +def detectpieces(boardimage, boardstyle, onlykings, flip):
       +        status = True
       +
       +        # pieces from template
       +        chesspieceimages = boardstyles[boardstyle]
       +        boardheight = len(boardimage)
       +        boardwidth = len(boardimage[0])
       +
       +        # pieces (scaled) to board size.
       +        piecerelative = {
       +                "w": {},
       +                "b": {}
       +        }
       +
       +        pieceimage = chesspieceimages["b"]["K"]
       +        piecewidth = len(pieceimage[0])
       +        usepiecewidth = int(boardwidth / 8)
       +
       +        # resize all template piece images.
       +        # scale pieces relative to the board
       +        for color in chesspieceimages:
       +                for piece in chesspieceimages[color]:
       +                        if onlykings and piece != "K" and piece != "k":
       +                                continue
       +                        pieceimage = chesspieceimages[color][piece]
       +                        pieceheight = len(pieceimage)
       +                        piecewidth = len(pieceimage[0])
       +
       +                        ratio = float(piecewidth) / pieceheight
       +                        dw = usepiecewidth
       +                        dh = int(dw * ratio)
       +                        img = img_resize(pieceimage, piecewidth, pieceheight, dw, dh)
       +
       +                        piecerelative[color][piece] = img # use new size
       +
       +        sw = int(boardwidth / 8)
       +        sh = int(boardheight / 8)
       +
       +        piecessift = {}
       +        for color in ["b", "w"]:
       +                piecessift[color] = {}
       +                for piece in piecerelative[color]:
       +                        if onlykings and piece != "K" and piece != "k":
       +                                continue
       +
       +                        key = ("%s|%s|%s|%s|%s" % (boardstyle, sw, sh, color, piece))
       +                        if key in siftcache:
       +                                s = siftcache[key]
       +                                piecessift[color][piece] = s
       +                                continue
       +
       +                        h = piecerelative[color][piece]
       +                        pimg = cv2.cvtColor(h, cv2.COLOR_BGR2GRAY)
       +                        kp, des = sift.detectAndCompute(pimg, None)
       +                        if des is None or len(des) < 2:
       +                                continue
       +                        s = {"kp": kp, "des": des}
       +                        piecessift[color][piece] = s
       +
       +                        # store in global cache.
       +                        siftcache[key] = s
       +
       +
       +        boardstate = {"matches":[], "side": "w"}
       +        matches = []
       +        for y in range(8):
       +                row = []
       +                for x in range(8):
       +                        # default: no match
       +                        match = { "color": "", "piece": "", "score": 0.0 }
       +                        row.append(match)
       +                matches.append(row)
       +
       +        for iy in range(8):
       +                if flip:
       +                        y = 7 - iy
       +                else:
       +                        y = iy
       +                for ix in range(8):
       +                        if flip:
       +                                x = 7 - ix
       +                        else:
       +                                x = ix
       +                        # x, y position is floored/truncated
       +                        x1 = int(x * sw)
       +                        y1 = int(y * sh)
       +                        x2 = int(x1 + sw)
       +                        y2 = int(y1 + sh)
       +
       +                        region = boardimage[y1:y2, x1:x2]
       +
       +                        regiongray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY)
       +                        kpregion, desregion = sift.detectAndCompute(regiongray, None)
       +                        if desregion is None:
       +                                continue
       +
       +                        bestmatch = -1
       +                        match = {}
       +                        color = "w"
       +                        if not iswhitesquare(x, y):
       +                                color = "b"
       +
       +                        for piece in piecerelative[color]:
       +                                if onlykings and piece != "K" and piece != "k":
       +                                        continue
       +
       +                                h = piecerelative[color][piece]
       +                                if not piece in piecessift[color]:
       +                                        continue
       +                                s = piecessift[color][piece]
       +                                if not "des" in s or s["des"] is None or len(s["des"]) < 2:
       +                                        continue
       +                                despiece = s["des"]
       +
       +                                # FLANN
       +                                fmatches = find_pieces_flann(desregion, despiece)
       +                                score = len(fmatches)
       +                                if score > 0: # threshold
       +                                        # higher matches is better
       +                                        if bestmatch == -1 or score > bestmatch:
       +                                                bestmatch = score
       +                                                match = { "color": color, "piece": piece, "score": score }
       +
       +                        if "score" in match:
       +                                matches[y][x] = match
       +
       +
       +        boardstate["matches"] = matches
       +        boardstate["status"] = status
       +        return boardstate
       +
       +def boarddetect(img):
       +        gimg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
       +
       +        # canny edge (without gaussian blur)
       +        canny = cv2.Canny(img, 100, 100)
       +        contours, hierarchy = cv2.findContours(canny,
       +            cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # simple only stores points
       +
       +        areas = []
       +        for c in contours:
       +                x, y, w, h = cv2.boundingRect(c)
       +                # skip too small areas (for example 8 * piece size)
       +                if w < 200 or h < 200:
       +                        continue
       +
       +                # quick check: must be somewhat square at least
       +                ratio = w / h
       +                if ratio < 0.95 or ratio > 1.05:
       +                        continue
       +
       +                areas.append({"x": x, "y": y, "w": w, "h": h })
       +
       +        return areas
       +
       +def isvalidboard(boardstate):
       +        wk = False
       +        bk = False
       +        matches = boardstate["matches"]
       +        # check if a white and black king is found.
       +        for y in range(len(matches)):
       +                row = matches[y]
       +                for x in range(len(row)):
       +                        c = row[x]["piece"]
       +                        if c == "K":
       +                                wk = True
       +                        if c == "k":
       +                                bk = True
       +
       +        return wk and bk
       +
       +# check castle rights based on the image, but it does not check if in check
       +# or if castling is legal.
       +def fixcastlerights(boardstate):
       +        m = boardstate["matches"] # 8x8 matches
       +
       +        wk = "K"
       +        wq = "Q"
       +        bk = "k"
       +        bq = "q"
       +
       +        # check if king moved.
       +        if m[7][4]["piece"] != "K": # white king
       +                wk = ""
       +                wq = ""
       +        else:
       +                # check if rooks moved.
       +                if m[7][0]["piece"] != "R":
       +                        wq = ""
       +                if m[7][7]["piece"] != "R":
       +                        wk = ""
       +
       +        if m[0][4]["piece"] != "k": # black king
       +                bk = ""
       +                bq = ""
       +        else:
       +                # check if rooks moved.
       +                if m[0][0]["piece"] != "r":
       +                        bq = ""
       +                if m[0][7]["piece"] != "r":
       +                        bk = ""
       +
       +        boardstate["castle"] = wk + wq + bk + bq
       +
       +# main
       +filename = "stdin"
       +title = "stdin"
       +fullimg = cv2.imread("/dev/stdin")
       +fullimgh, fullimgw, _ = fullimg.shape
       +
       +areas = boarddetect(fullimg)
       +
       +# try to somewhat sort by visible (columns: left bottom, to right down).
       +areas = sorted(areas, key=itemgetter("x", "y"))
       +debug("areas: %d" % len(areas))
       +
       +# filter away the most obvious duplicate areas.
       +uareas = [] # unique areas
       +maxdiff = 10
       +arealen = len(areas)
       +for i in range(arealen):
       +        dup = False
       +        for j in range(i, arealen):
       +                if i == j:
       +                        continue # self
       +                if areas[j]["x"] >= (areas[i]["x"] - maxdiff) and areas[j]["x"] <= (areas[i]["x"] + maxdiff) and \
       +                        areas[j]["y"] >= (areas[i]["y"] - maxdiff) and areas[j]["y"] <= (areas[i]["y"] + maxdiff):
       +                        dup = True
       +        if not dup:
       +                uareas.append(areas[i])
       +areas = uareas
       +# if no areas found, use whole image.
       +if len(areas) == 0:
       +        areas.append({"x": 0, "y": 0, "w": fullimgw, "h": fullimgh })
       +
       +debug("unique areas: %d" % len(areas))
       +
       +flip = "flip" in config and config["flip"]
       +dareas = []
       +boardstates = []
       +for area in areas:
       +        x1 = area["x"] + 1
       +        y1 = area["y"] + 1
       +        x2 = x1 + area["w"] - 1
       +        y2 = y1 + area["h"] - 1
       +        region = fullimg[y1:y2, x1:x2]
       +
       +        boardstyle = config["boardstyle"]
       +        if config["guessstyle"]:
       +                # detect board style, use the boardstyle with the highest score.
       +                # only scans for king pieces.
       +                best = 0
       +                beststyle = ""
       +                for boardstyle in boardstyles:
       +                        boardstate = detectpieces(region, boardstyle, True, flip)
       +                        if boardstate["status"] == False:
       +                                continue
       +                        l = len(boardstate["matches"])
       +                        wk = 0.0
       +                        bk = 0.0
       +                        for y in range(l):
       +                                row = boardstate["matches"][y]
       +                                for x in range(len(row)):
       +                                        match = row[x]
       +                                        c = match["piece"]
       +                                        score = match["score"]
       +                                        if c == "K":
       +                                                wk = score
       +                                        if c == "k":
       +                                                bk = score
       +                                if wk + bk > best:
       +                                        best = wk + bk
       +                                        beststyle = boardstyle
       +
       +                debug("best guess for board style: \"%s\"" % (beststyle))
       +                boardstyle = beststyle
       +
       +        if boardstyle != "":
       +                starttime = time.perf_counter_ns()
       +                boardstate = detectpieces(region, boardstyle, False, flip)
       +                endtime = time.perf_counter_ns()
       +                ms = (endtime - starttime) / 1000000
       +                debug("detectpieces() took: %d ms" % ms)
       +
       +                boardstate["area"] = area
       +                if boardstate["status"] == False:
       +                        boardstates.append(boardstate)
       +                        continue
       +
       +        # check move indicator: check for any figure next to the region (about one square).
       +        if boardstyle == "woodpecker" and config["checkmoveindicator"]:
       +                # top right: black to move
       +                mx1 = x2 + 4
       +                mx2 = int(x2 + (area["w"] / 8))
       +                my1 = y1
       +                my2 = int(y1 + (area["h"] / 8))
       +                if mx2 < fullimgw and my2 < fullimgh:
       +                        region = fullimg[my1:my2, mx1:mx2]
       +                        gimg = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY)
       +                        canny = cv2.Canny(gimg, 100, 100) # canny edge (without gaussian blur)
       +                        contours, hierarchy = cv2.findContours(canny,
       +                            cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # simple only stores points
       +                        blen = len(contours)
       +
       +                # bottom right: white to move
       +                mx1 = x2 + 4
       +                mx2 = int(mx1 + (area["w"] / 8))
       +                my1 = int(y2 - (area["h"] / 8))
       +                my2 = y2
       +
       +                if mx2 < fullimgw and my2 < fullimgh:
       +                        region = fullimg[my1:my2, mx1:mx2]
       +                        gimg = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY)
       +                        canny = cv2.Canny(gimg, 100, 100) # canny edge (without gaussian blur)
       +                        contours, hierarchy = cv2.findContours(canny,
       +                            cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # simple only stores points
       +                        wlen = len(contours)
       +
       +                        if blen > 0 and blen > wlen:
       +                                boardstate["side"] = "b" # black to move
       +                        elif wlen > 0 and wlen > blen:
       +                                boardstate["side"] = "w" # white to move"
       +
       +        if boardstyle == "":
       +                continue
       +
       +        if not isvalidboard(boardstate):
       +                continue
       +
       +        fixcastlerights(boardstate)
       +        boardstates.append(boardstate)
       +
       +        flipdisplay = "flipdisplay" in config and config["flipdisplay"]
       +#        print(board2txt(boardstate, flipdisplay), flush=True)
       +        print(board2fen(boardstate, flipdisplay), flush=True)
       +
       +# draw a debug image with detected board regions and pieces.
       +if config["debug"] or config["debugimage"]:
       +        debugimg = fullimg
       +        for b in boardstates:
       +                if "area" in b:
       +                        area = b["area"]
       +                        sw = area["w"] / 8 # square width
       +                        sh = area["h"] / 8 # square height
       +
       +                        # board area
       +                        x1 = area["x"] + 1
       +                        y1 = area["y"] + 1
       +                        x2 = x1 + area["w"] - 1
       +                        y2 = y1 + area["h"] - 1
       +                        color = (0, 255, 255)
       +                        if b["status"] == False:
       +                                color = (0, 0, 255)
       +                        cv2.rectangle(debugimg, (x1, y1), (x2, y2), color, 2)
       +
       +                # draw pieces as text characters.
       +                if "matches" in b:
       +                        matches = b["matches"]
       +                        l = len(matches)
       +                        for y in range(len(matches)):
       +                                row = matches[y]
       +                                for x in range(len(row)):
       +                                        m = row[x]
       +                                        c = m["piece"]
       +                                        if c == "":
       +                                                continue
       +
       +                                        px = int(x1 + (x * sw))
       +                                        py = int(y1 + (y * sh))
       +
       +                                        # text color for white or black.
       +                                        if c.isupper():
       +                                                color = (255, 0, 0) # BGR: blue
       +                                        else:
       +                                                color = (0, 0, 255) # BGR: red
       +
       +#                                        fontscale = 1.0 / min(area["w"], area["h"]) * (sh * 6)
       +                                        fontscale = 1.0
       +                                        cv2.putText(debugimg, c, (px, py + int(sh)), cv2.FONT_HERSHEY_SIMPLEX, fontscale, color, 2, cv2.LINE_AA)
       +
       +                if boardstyle == "woodpecker" and config["checkmoveindicator"]:
       +                        # side to move indicator.
       +                        if "side" in b and b["side"] == "w":
       +                                mx1 = x2 + 4
       +                                mx2 = int(mx1 + sw)
       +                                my1 = int(y2 - sh)
       +                                my2 = y2
       +                                cv2.rectangle(debugimg, (mx1, my1), (mx2, my2), (255, 0, 0), 2)
       +                        elif "side" in b and b["side"] == "b":
       +                                mx1 = x2 + 4
       +                                mx2 = int(x2 + sw)
       +                                my1 = y1
       +                                my2 = int(y1 + sh)
       +                                cv2.rectangle(debugimg, (mx1, my1), (mx2, my2), (0, 0, 255), 2)
       +
       +        filename = "/tmp/debug.png"
       +        cv2.imwrite(filename, debugimg)
       +        debug("debug image written: %s" % (filename))