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))