diff --git a/.vscode/launch.json b/.vscode/launch.json index c40731e..c361c4e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "program": "${file}", "console": "integratedTerminal", - "args": "sample/DSC_9657s.jpg" + "args": "sample/TIAS26_MCC.jpg" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 379fe5d..6b71615 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ (`numpy.ndarray`)です。配列は `uint8` 型、形状 `(高さ, 幅, 3)` の BGR画像(OpenCVの標準形式)を指定してください。入力配列は変更しません。 +横向き(6列×4行)と縦向き(4列×6行)の格子を検出し、色票の向きを補正します。 +明るい背景に囲まれた色票(`sample/TIAS26_MCC.jpg` など)にも対応しています。 + ```python import cv2 diff --git a/colorchecker.py b/colorchecker.py index a0c007d..6e72372 100644 --- a/colorchecker.py +++ b/colorchecker.py @@ -180,9 +180,9 @@ np.ones((5, 5), np.uint8) ) - contours, _ = cv2.findContours( + contours, hierarchy = cv2.findContours( mask, - cv2.RETR_EXTERNAL, + cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE ) @@ -192,7 +192,17 @@ candidates = [] - for contour in contours: + for index, contour in enumerate(contours): + + # 明るい背景の内側にある色票も探索する。 + # 奇数階層は暗い領域(穴)の輪郭なので候補に含めない。 + depth = 0 + parent = hierarchy[0, index, 3] + while parent != -1: + depth += 1 + parent = hierarchy[0, parent, 3] + if depth % 2: + continue x, y, w, h = cv2.boundingRect(contour) @@ -335,7 +345,9 @@ def warp_from_cells(image, cells, - cell_size=140): + cell_size=140, + rows=4, + cols=6): src_points = [] dst_points = [] @@ -349,10 +361,17 @@ ]) # 正規化画像上の色票中心 - dst_points.append([ - (col + 0.5) * cell_size, - (row + 0.5) * cell_size - ]) + if (rows, cols) == (6, 4): + # 縦向きの格子は90度回転した座標へ直接射影する。 + dst_points.append([ + (rows - row - 0.5) * cell_size, + (col + 0.5) * cell_size + ]) + else: + dst_points.append([ + (col + 0.5) * cell_size, + (row + 0.5) * cell_size + ]) src_points = np.asarray( src_points, @@ -741,27 +760,22 @@ ) ) - grid = fit_grid( - candidates - ) + for rows, cols in ((4, 6), (6, 4)): + grid = fit_grid(candidates, rows=rows, cols=cols) + if grid is None: + continue - if grid is None: - continue - - score, cells = grid - - if ( - best is None - or score > best["score"] - ): - - best = { - "score": score, - "threshold": threshold, - "candidates": candidates, - "cells": cells, - "mask": mask - } + score, cells = grid + if best is None or score > best["score"]: + best = { + "score": score, + "threshold": threshold, + "candidates": candidates, + "cells": cells, + "mask": mask, + "rows": rows, + "cols": cols, + } if best is None: @@ -775,7 +789,9 @@ warp_from_cells( image, best["cells"], - cell_size + cell_size, + rows=best["rows"], + cols=best["cols"], ) ) diff --git a/test_colorchecker.py b/test_colorchecker.py index 22fdf7e..acb2037 100644 --- a/test_colorchecker.py +++ b/test_colorchecker.py @@ -7,7 +7,7 @@ import cv2 import numpy as np -from colorchecker import ResultCode, detect_colorchecker +from colorchecker import ResultCode, detect_colorchecker, read_image class ColorCheckerTests(unittest.TestCase): @@ -90,16 +90,38 @@ np.testing.assert_allclose(measured, colors.T, atol=1) self.assertEqual(stdout.getvalue(), '') self.assertEqual(list(Path(directory).iterdir()), [path]) - for source in (str(path), image, image[:, ::-1]): + # 明るい背景に囲まれた色票と、縦向き・反転を含む全方向。 + surrounded = np.pad(image, ((30, 30), (30, 30), (0, 0)), + constant_values=240) + sources = [str(path), image, image[:, ::-1]] + sources.extend(np.rot90(surrounded, k) for k in range(4)) + sources.extend(np.rot90(surrounded[:, ::-1], k) for k in range(4)) + for source in sources: with self.subTest(input_type=type(source)): original = source.copy() if isinstance(source, np.ndarray) else None code, measured, images = detect_colorchecker(filename=source) self.assertEqual(code, ResultCode.SUCCESS) - self.assert_images(images, image.shape) + self.assert_images(images, source.shape if original is not None + else image.shape) np.testing.assert_allclose(measured, colors.T, atol=1) if original is not None: np.testing.assert_array_equal(source, original) + def test_tias26_checker(self): + path = Path(__file__).parent / 'sample' / 'TIAS26_MCC.jpg' + image = read_image(path) + code, measured, images = detect_colorchecker(image) + self.assertEqual(code, ResultCode.SUCCESS) + self.assert_images(images, image.shape) + + # 原画像で確認した24色票の内部と比較し、位置・向き・順序も検証。 + expected = np.empty((3, 24)) + for row, y in enumerate((600, 900, 1190, 1475, 1750, 2020)): + for col, x in enumerate((480, 800, 1110, 1420)): + rgb = image[y - 20:y + 20, x - 20:x + 20, ::-1].mean(axis=(0, 1)) + expected[:, (3 - col) * 6 + row] = rgb + np.testing.assert_allclose(measured, expected, atol=8) + if __name__ == '__main__': unittest.main()