"""
Colour Checker Detection - Segmentation
=======================================
Defines the objects for colour checker detection using segmentation:
- :attr:`colour_checker_detection.SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC`
- :attr:`colour_checker_detection.SETTINGS_SEGMENTATION_COLORCHECKER_SG`
- :func:`colour_checker_detection.colour_checkers_coordinates_segmentation`
- :func:`colour_checker_detection.extract_colour_checkers_segmentation`
- :func:`colour_checker_detection.detect_colour_checkers_segmentation`
References
----------
- :cite:`Abecassis2011` : Abecassis, F. (2011). OpenCV - Rotation
(Deskewing). Retrieved October 27, 2018, from http://felix.abecassis.me/\
2011/10/opencv-rotation-deskewing/
"""
from __future__ import annotations
import cv2
import numpy as np
from dataclasses import dataclass
from colour.hints import (
Any,
ArrayLike,
Boolean,
Dict,
DTypeFloating,
Floating,
Integer,
List,
Literal,
NDArray,
Tuple,
Type,
Union,
cast,
)
from colour.models import cctf_encoding
from colour.utilities import (
MixinDataclassIterable,
Structure,
as_float_array,
as_int_array,
as_int,
orient,
usage_warning,
)
from colour.utilities.documentation import (
DocstringDict,
is_documentation_building,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2018 Colour Developers"
__license__ = "New BSD License - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC",
"SETTINGS_SEGMENTATION_COLORCHECKER_SG",
"FLOAT_DTYPE_DEFAULT",
"swatch_masks",
"as_8_bit_BGR_image",
"adjust_image",
"is_square",
"contour_centroid",
"scale_contour",
"crop_and_level_image_with_rectangle",
"DataColourCheckersCoordinatesSegmentation",
"colour_checkers_coordinates_segmentation",
"extract_colour_checkers_segmentation",
"DataDetectColourCheckersSegmentation",
"detect_colour_checkers_segmentation",
]
SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC: Dict = {
"aspect_ratio": 1.5,
"aspect_ratio_minimum": 1.5 * 0.9,
"aspect_ratio_maximum": 1.5 * 1.1,
"swatches": 24,
"swatches_horizontal": 6,
"swatches_vertical": 4,
"swatches_count_minimum": int(24 * 0.75),
"swatches_count_maximum": int(24 * 1.25),
"swatches_chromatic_slice": slice(0 + 1, 0 + 6 - 1, 1),
"swatches_achromatic_slice": slice(18 + 1, 18 + 6 - 1, 1),
"swatch_minimum_area_factor": 200,
"swatch_contour_scale": 1 + 1 / 3,
"cluster_contour_scale": 0.975,
"working_width": 1440,
"fast_non_local_means_denoising_kwargs": {
"h": 10,
"templateWindowSize": 7,
"searchWindowSize": 21,
},
"adaptive_threshold_kwargs": {
"maxValue": 255,
"adaptiveMethod": cv2.ADAPTIVE_THRESH_MEAN_C,
"thresholdType": cv2.THRESH_BINARY,
"blockSize": int(1440 * 0.015) - int(1440 * 0.015) % 2 + 1,
"C": 3,
},
"interpolation_method": cv2.INTER_CUBIC,
}
if is_documentation_building(): # pragma: no cover
SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC = DocstringDict(
SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC
)
SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC.__doc__ = """
Settings for the segmentation of the *X-Rite* *ColorChecker Classic* and
*X-Rite* *ColorChecker Passport*.
"""
SETTINGS_SEGMENTATION_COLORCHECKER_SG: Dict = (
SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC.copy()
)
SETTINGS_SEGMENTATION_COLORCHECKER_SG.update(
{
"aspect_ratio": 1.4,
"aspect_ratio_minimum": 1.4 * 0.9,
"aspect_ratio_maximum": 1.4 * 1.1,
"swatches": 140,
"swatches_horizontal": 14,
"swatches_vertical": 10,
"swatches_count_minimum": int(140 * 0.50),
"swatches_count_maximum": int(140 * 1.5),
"swatch_minimum_area_factor": 200,
"swatches_chromatic_slice": slice(48, 48 + 5, 1),
"swatches_achromatic_slice": slice(115, 115 + 5, 1),
"swatch_contour_scale": 1 + 1 / 3,
"cluster_contour_scale": 1,
}
)
if is_documentation_building(): # pragma: no cover
SETTINGS_SEGMENTATION_COLORCHECKER_SG = DocstringDict(
SETTINGS_SEGMENTATION_COLORCHECKER_SG
)
SETTINGS_SEGMENTATION_COLORCHECKER_SG.__doc__ = """
Settings for the segmentation of the *X-Rite* *ColorChecker SG**.
"""
FLOAT_DTYPE_DEFAULT: Type[DTypeFloating] = np.float32
"""Dtype used for the computations."""
def swatch_masks(
width: Integer,
height: Integer,
swatches_h: Integer,
swatches_v: Integer,
samples: Integer,
) -> Tuple[NDArray, ...]:
"""
Return swatch masks for given image width and height and swatches count.
Parameters
----------
width
Image width.
height
Image height.
swatches_h
Horizontal swatches count.
swatches_v
Vertical swatches count.
samples
Samples count.
Returns
-------
:class:`tuple`
Tuple of swatch masks.
Examples
--------
>>> from pprint import pprint
>>> pprint(swatch_masks(16, 8, 4, 2, 1)) # doctest: +ELLIPSIS
(array([2, 2, 2, 2]...),
array([2, 2, 6, 6]...),
array([ 2, 2, 10, 10]...),
array([ 2, 2, 14, 14]...),
array([6, 6, 2, 2]...),
array([6, 6, 6, 6]...),
array([ 6, 6, 10, 10]...),
array([ 6, 6, 14, 14]...))
"""
samples_half = as_int(samples / 2)
masks = []
offset_h = width / swatches_h / 2
offset_v = height / swatches_v / 2
for j in np.linspace(offset_v, height - offset_v, swatches_v):
for i in np.linspace(offset_h, width - offset_h, swatches_h):
masks.append(
as_int_array(
[
j - samples_half,
j + samples_half,
i - samples_half,
i + samples_half,
]
)
)
return tuple(masks)
def as_8_bit_BGR_image(image: ArrayLike) -> NDArray:
"""
Convert and encodes given linear float *RGB* image to 8-bit *BGR* with
*sRGB* reverse OETF.
Parameters
----------
image
Image to convert.
Returns
-------
:class:`numpy.ndarray`
Converted image.
Notes
-----
- In the eventuality where the image is already an integer array, the
conversion is by-passed.
Examples
--------
>>> from colour.algebra import random_triplet_generator
>>> prng = np.random.RandomState(4)
>>> image = list(random_triplet_generator(8, random_state=prng))
>>> image = np.reshape(image, [4, 2, 3])
>>> print(image)
[[[ 0.96702984 0.25298236 0.0089861 ]
[ 0.54723225 0.43479153 0.38657128]]
<BLANKLINE>
[[ 0.97268436 0.77938292 0.04416006]
[ 0.71481599 0.19768507 0.95665297]]
<BLANKLINE>
[[ 0.69772882 0.86299324 0.43614665]
[ 0.2160895 0.98340068 0.94897731]]
<BLANKLINE>
[[ 0.97627445 0.16384224 0.78630599]
[ 0.00623026 0.59733394 0.8662893 ]]]
>>> image = as_8_bit_BGR_image(image)
>>> print(image)
[[[ 23 137 251]
[167 176 195]]
<BLANKLINE>
[[ 59 228 251]
[250 122 219]]
<BLANKLINE>
[[176 238 217]
[249 253 128]]
<BLANKLINE>
[[229 112 252]
[239 203 18]]]
>>> as_8_bit_BGR_image(image)
array([[[ 23, 137, 251],
[167, 176, 195]],
<BLANKLINE>
[[ 59, 228, 251],
[250, 122, 219]],
<BLANKLINE>
[[176, 238, 217],
[249, 253, 128]],
<BLANKLINE>
[[229, 112, 252],
[239, 203, 18]]], dtype=uint8)
"""
image = np.asarray(image)[..., :3]
if image.dtype == np.uint8:
return image
return cv2.cvtColor(
cast(NDArray, cctf_encoding(image) * 255).astype(np.uint8),
cv2.COLOR_RGB2BGR,
)
def adjust_image(
image: ArrayLike,
target_width: Integer,
interpolation_method: Literal[ # type: ignore[misc]
cv2.INTER_AREA,
cv2.INTER_BITS,
cv2.INTER_BITS2,
cv2.INTER_CUBIC,
cv2.INTER_LANCZOS4,
cv2.INTER_LINEAR,
] = cv2.INTER_CUBIC,
) -> NDArray:
"""
Adjust given image so that it is horizontal and resizes it to given target
width.
Parameters
----------
image
Image to adjust.
target_width
Width the image is resized to.
interpolation_method
Interpolation method.
Returns
-------
:class:`numpy.ndarray`
Resized image.
Examples
--------
>>> from colour.algebra import random_triplet_generator
>>> prng = np.random.RandomState(4)
>>> image = list(random_triplet_generator(8, random_state=prng))
>>> image = np.reshape(image, [2, 4, 3])
>>> adjust_image(image, 5) # doctest: +ELLIPSIS
array([[[ 0.9925325..., 0.2419374..., -0.0139522...],
[ 0.6174497..., 0.3460756..., 0.3189758...],
[ 0.7447774..., 0.678666 ..., 0.1652180...],
[ 0.9476452..., 0.6550805..., 0.2609945...],
[ 0.6991505..., 0.1623470..., 1.0120867...]],
<BLANKLINE>
[[ 0.7269885..., 0.8556784..., 0.4049920...],
[ 0.2666565..., 1.0401633..., 0.8238320...],
[ 0.6419699..., 0.5442698..., 0.9082211...],
[ 0.7894426..., 0.1944301..., 0.7906868...],
[-0.0526997..., 0.6236685..., 0.8711483...]]], dtype=float32)
"""
image = as_float_array(image, FLOAT_DTYPE_DEFAULT)[..., :3]
width, height = image.shape[1], image.shape[0]
if width < height:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
height, width = width, height
ratio = width / target_width
if np.allclose(ratio, 1):
return cast(NDArray, image)
else:
return cv2.resize(
image,
(as_int(target_width), as_int(height / ratio)),
interpolation=interpolation_method,
)
def is_square(contour: ArrayLike, tolerance: Floating = 0.015) -> Boolean:
"""
Return if given contour is a square.
Parameters
----------
contour
Shape to test whether it is a square.
tolerance
Tolerance under which the contour is considered to be a square.
Returns
-------
:class:`bool`
Whether given contour is a square.
Examples
--------
>>> shape = np.array([[0, 0], [1, 0], [1, 1], [0, 1]])
>>> is_square(shape)
True
>>> shape = np.array([[0.5, 0], [1, 0], [1, 1], [0, 1]])
>>> is_square(shape)
False
"""
return (
cv2.matchShapes(
contour,
np.array([[0, 0], [1, 0], [1, 1], [0, 1]]),
cv2.CONTOURS_MATCH_I2,
0.0,
)
< tolerance
)
def contour_centroid(contour: ArrayLike) -> Tuple[Floating, Floating]:
"""
Return the centroid of given contour.
Parameters
----------
contour
Contour to return the centroid of.
Returns
-------
:class:`tuple`
Contour centroid.
Notes
-----
- A :class:`tuple` class is returned instead of a :class:`ndarray` class
for convenience with *OpenCV*.
Examples
--------
>>> contour = np.array([[0, 0], [1, 0], [1, 1], [0, 1]])
>>> contour_centroid(contour)
(0.5, 0.5)
"""
moments = cv2.moments(contour)
centroid = (
moments["m10"] / moments["m00"],
moments["m01"] / moments["m00"],
)
return cast(Tuple[Floating, Floating], centroid)
def scale_contour(contour: ArrayLike, factor: Floating) -> NDArray:
"""
Scale given contour by given scale factor.
Parameters
----------
contour
Contour to scale.
factor
Scale factor.
Returns
-------
:class:`numpy.ndarray`
Scaled contour.
Examples
--------
>>> contour = np.array([[0, 0], [1, 0], [1, 1], [0, 1]])
>>> scale_contour(contour, 2)
array([[ 0., 0.],
[ 2., 0.],
[ 2., 2.],
[ 0., 2.]])
"""
centroid = as_int_array(contour_centroid(contour))
scaled_contour = (as_float_array(contour) - centroid) * factor + centroid
return scaled_contour
def crop_and_level_image_with_rectangle(
image: ArrayLike,
rectangle: Tuple[Tuple, Tuple, Floating],
interpolation_method: Literal[ # type: ignore[misc]
cv2.INTER_AREA,
cv2.INTER_BITS,
cv2.INTER_BITS2,
cv2.INTER_CUBIC,
cv2.INTER_LANCZOS4,
cv2.INTER_LINEAR,
] = cv2.INTER_CUBIC,
):
"""
Crop and rotate/level given image using given rectangle.
Parameters
----------
image
Image to crop and rotate/level.
rectangle
Rectangle used to crop and rotate/level the image.
interpolation_method
Interpolation method.
Returns
-------
:class:`numpy.ndarray`
Cropped and rotated/levelled image.
References
----------
:cite:`Abecassis2011`
Examples
--------
>>> import os
>>> from colour import read_image
>>> from colour_checker_detection import TESTS_RESOURCES_DIRECTORY
>>> path = os.path.join(TESTS_RESOURCES_DIRECTORY,
... 'colour_checker_detection', 'detection',
... 'IMG_1967.png')
>>> image = adjust_image(read_image(path), 1440)
>>> rectangle = (
... (723.29608154, 465.50939941),
... (461.24377441, 696.34759522),
... -88.18692780,
... )
>>> print(image.shape)
(958, 1440, 3)
>>> image = crop_and_level_image_with_rectangle(image, rectangle)
>>> print(image.shape)
(461, 696, 3)
"""
image = as_float_array(image, FLOAT_DTYPE_DEFAULT)[..., :3]
width, height = image.shape[1], image.shape[0]
width_r, height_r = rectangle[1]
centroid = contour_centroid(cv2.boxPoints(rectangle))
angle = rectangle[-1]
width_r, height_r = as_int_array([width_r, height_r])
M_r = cv2.getRotationMatrix2D(centroid, angle, 1)
image_r = cv2.warpAffine(image, M_r, (width, height), interpolation_method)
image_c = cv2.getRectSubPix(
image_r, (width_r, height_r), (centroid[0], centroid[1])
)
if image_c.shape[0] > image_c.shape[1]:
image_c = orient(image_c, "90 CW")
return image_c
@dataclass
class DataColourCheckersCoordinatesSegmentation(MixinDataclassIterable):
"""
Colour checkers detection data used for plotting, debugging and further
analysis.
Parameters
----------
colour_checkers
Colour checker bounding boxes, i.e., the. clusters that have the
relevant count of swatches.
clusters
Detected swatches clusters.
swatches
Detected swatches.
segmented_image
Thresholded/Segmented image.
"""
colour_checkers: Tuple[NDArray, ...]
clusters: Tuple[NDArray, ...]
swatches: Tuple[NDArray, ...]
segmented_image: NDArray
[docs]def colour_checkers_coordinates_segmentation(
image: ArrayLike, additional_data: Boolean = False, **kwargs: Any
) -> Union[DataColourCheckersCoordinatesSegmentation, Tuple[NDArray, ...]]:
"""
Detect the colour checkers coordinates in given image :math:`image` using
segmentation.
This is the core detection definition. The process is a follows:
- Input image :math:`image` is converted to a grayscale image
:math:`image_g`.
- Image :math:`image_g` is denoised.
- Image :math:`image_g` is thresholded/segmented to image
:math:`image_s`.
- Image :math:`image_s` is eroded and dilated to cleanup remaining noise.
- Contours are detected on image :math:`image_s`.
- Contours are filtered to only keep squares/swatches above and below
defined surface area.
- Squares/swatches are clustered to isolate region-of-interest that are
potentially colour checkers: Contours are scaled by a third so that
colour checkers swatches are expected to be joined, creating a large
rectangular cluster. Rectangles are fitted to the clusters.
- Clusters with an aspect ratio different to the expected one are
rejected, a side-effect is that the complementary pane of the
*X-Rite* *ColorChecker Passport* is omitted.
- Clusters with a number of swatches close to the expected one are
kept.
Parameters
----------
image
Image to detect the colour checkers in.
additional_data
Whether to output additional data.
Other Parameters
----------------
aspect_ratio
Colour checker aspect ratio, e.g. 1.5.
aspect_ratio_minimum
Minimum colour checker aspect ratio for detection: projective geometry
might reduce the colour checker aspect ratio.
aspect_ratio_maximum
Maximum colour checker aspect ratio for detection: projective geometry
might increase the colour checker aspect ratio.
swatches
Colour checker swatches total count.
swatches_horizontal
Colour checker swatches horizontal columns count.
swatches_vertical
Colour checker swatches vertical row count.
swatches_count_minimum
Minimum swatches count to be considered for the detection.
swatches_count_maximum
Maximum swatches count to be considered for the detection.
swatches_chromatic_slice
A `slice` instance defining chromatic swatches used to detect if the
colour checker is upside down.
swatches_achromatic_slice
A `slice` instance defining achromatic swatches used to detect if the
colour checker is upside down.
swatch_minimum_area_factor
Swatch minimum area factor :math:`f` with the minimum area :math:`m_a`
expressed as follows: :math:`m_a = image_w * image_h / s_c / f` where
:math:`image_w`, :math:`image_h` and :math:`s_c` are respectively the
image width, height and the swatches count.
swatch_contour_scale
As the image is filtered, the swatches area will tend to shrink, the
generated contours can thus be scaled.
cluster_contour_scale
As the swatches are clustered, it might be necessary to adjust the
cluster scale so that the masks are centred better on the swatches.
working_width
Size the input image is resized to for detection.
fast_non_local_means_denoising_kwargs
Keyword arguments for :func:`cv2.fastNlMeansDenoising` definition.
adaptive_threshold_kwargs
Keyword arguments for :func:`cv2.adaptiveThreshold` definition.
interpolation_method
Interpolation method used when resizing the images, `cv2.INTER_CUBIC`
and `cv2.INTER_LINEAR` methods are recommended.
Returns
-------
:class:`colour_checker_detection.detection.segmentation.\
DataColourCheckersCoordinatesSegmentation` or :class:`tuple`
Tuple of colour checkers coordinates or
:class:`DataColourCheckersCoordinatesSegmentation` class
instance with additional data.
Notes
-----
- Multiple colour checkers can be detected if presented in ``image``.
Examples
--------
>>> import os
>>> from colour import read_image
>>> from colour_checker_detection import TESTS_RESOURCES_DIRECTORY
>>> path = os.path.join(TESTS_RESOURCES_DIRECTORY,
... 'colour_checker_detection', 'detection',
... 'IMG_1967.png')
>>> image = read_image(path)
>>> colour_checkers_coordinates_segmentation(image) # doctest: +ELLIPSIS
(array([[ 369, 688],
[ 382, 226],
[1078, 246],
[1065, 707]]...)
"""
image = as_float_array(image, FLOAT_DTYPE_DEFAULT)[..., :3]
settings = Structure(**SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC)
settings.update(**kwargs)
image = as_8_bit_BGR_image(
adjust_image(
image, settings.working_width, settings.interpolation_method
)
)
width, height = image.shape[1], image.shape[0]
maximum_area = width * height / settings.swatches
minimum_area = (
width
* height
/ settings.swatches
/ settings.swatch_minimum_area_factor
)
# Thresholding/Segmentation.
image_g = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
image_g = cv2.fastNlMeansDenoising(
image_g, None, **settings.fast_non_local_means_denoising_kwargs
)
image_s = cv2.adaptiveThreshold(
image_g, **settings.adaptive_threshold_kwargs
)
# Cleanup.
kernel = np.ones([3, 3], np.uint8)
image_c = cv2.erode(image_s, kernel, iterations=1)
image_c = cv2.dilate(image_c, kernel, iterations=1)
# Detecting contours.
contours, _hierarchy = cv2.findContours(
image_c, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE
)
# Filtering squares/swatches contours.
swatches = []
for contour in contours:
curve = cv2.approxPolyDP(
contour, 0.01 * cv2.arcLength(contour, True), True
)
if minimum_area < cv2.contourArea(curve) < maximum_area and is_square(
curve
):
swatches.append(
as_int_array(cv2.boxPoints(cv2.minAreaRect(curve)))
)
# Clustering squares/swatches.
contours = np.zeros(image.shape, dtype=np.uint8)
for swatch in [
as_int_array(scale_contour(swatch, settings.swatch_contour_scale))
for swatch in swatches
]:
cv2.drawContours(contours, [swatch], -1, [255] * 3, -1)
contours = cv2.cvtColor(contours, cv2.COLOR_RGB2GRAY)
contours, _hierarchy = cv2.findContours(
contours, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
)
clusters = [
as_int_array(
scale_contour(
cv2.boxPoints(cv2.minAreaRect(cluster)),
settings.cluster_contour_scale,
)
)
for cluster in contours
]
# Filtering clusters using their aspect ratio.
filtered_clusters = []
for cluster in clusters[:]:
rectangle = cv2.minAreaRect(cluster)
width = max(rectangle[1][0], rectangle[1][1])
height = min(rectangle[1][0], rectangle[1][1])
ratio = width / height
if (
settings.aspect_ratio_minimum
< ratio
< settings.aspect_ratio_maximum
):
filtered_clusters.append(as_int_array(cluster))
clusters = filtered_clusters
# Filtering swatches within cluster.
counts = []
for cluster in clusters:
count = 0
for swatch in swatches:
if (
cv2.pointPolygonTest(cluster, contour_centroid(swatch), False)
== 1
):
count += 1
counts.append(count)
indexes = np.where(
np.logical_and(
as_int_array(counts) >= settings.swatches_count_minimum,
as_int_array(counts) <= settings.swatches_count_maximum,
)
)[0]
colour_checkers = tuple(clusters[i] for i in indexes)
if additional_data:
return DataColourCheckersCoordinatesSegmentation(
tuple(colour_checkers), tuple(clusters), tuple(swatches), image_c
)
else:
return colour_checkers
@dataclass
class DataDetectColourCheckersSegmentation(MixinDataclassIterable):
"""
Colour checker swatches data used for plotting, debugging and further
analysis.
Parameters
----------
swatch_colours
Colour checker swatches colours.
colour_checker_image
Cropped and levelled Colour checker image.
swatch_masks
Colour checker swatches masks.
"""
swatch_colours: Tuple[NDArray, ...]
colour_checker_image: NDArray
swatch_masks: Tuple[NDArray, ...]
[docs]def detect_colour_checkers_segmentation(
image: ArrayLike,
samples: Integer = 16,
additional_data: Boolean = False,
**kwargs: Any,
) -> Union[
Tuple[DataDetectColourCheckersSegmentation, ...],
Tuple[NDArray, ...],
]:
"""
Detect the colour checkers swatches in given image using segmentation.
Parameters
----------
image : array_like
Image to detect the colour checkers swatches in.
samples : int
Samples count to use to compute the swatches colours. The effective
samples count is :math:`samples^2`.
additional_data : bool, optional
Whether to output additional data.
Other Parameters
----------------
aspect_ratio
Colour checker aspect ratio, e.g. 1.5.
aspect_ratio_minimum
Minimum colour checker aspect ratio for detection: projective geometry
might reduce the colour checker aspect ratio.
aspect_ratio_maximum
Maximum colour checker aspect ratio for detection: projective geometry
might increase the colour checker aspect ratio.
swatches
Colour checker swatches total count.
swatches_horizontal
Colour checker swatches horizontal columns count.
swatches_vertical
Colour checker swatches vertical row count.
swatches_count_minimum
Minimum swatches count to be considered for the detection.
swatches_count_maximum
Maximum swatches count to be considered for the detection.
swatches_chromatic_slice
A `slice` instance defining chromatic swatches used to detect if the
colour checker is upside down.
swatches_achromatic_slice
A `slice` instance defining achromatic swatches used to detect if the
colour checker is upside down.
swatch_minimum_area_factor
Swatch minimum area factor :math:`f` with the minimum area :math:`m_a`
expressed as follows: :math:`m_a = image_w * image_h / s_c / f` where
:math:`image_w`, :math:`image_h` and :math:`s_c` are respectively the
image width, height and the swatches count.
swatch_contour_scale
As the image is filtered, the swatches area will tend to shrink, the
generated contours can thus be scaled.
cluster_contour_scale
As the swatches are clustered, it might be necessary to adjust the
cluster scale so that the masks are centred better on the swatches.
working_width
Size the input image is resized to for detection.
fast_non_local_means_denoising_kwargs
Keyword arguments for :func:`cv2.fastNlMeansDenoising` definition.
adaptive_threshold_kwargs
Keyword arguments for :func:`cv2.adaptiveThreshold` definition.
interpolation_method
Interpolation method used when resizing the images, `cv2.INTER_CUBIC`
and `cv2.INTER_LINEAR` methods are recommended.
Returns
-------
:class`tuple`
Tuple of :class:`DataDetectColourCheckersSegmentation` class
instances or colour checkers swatches.
Examples
--------
>>> import os
>>> from colour import read_image
>>> from colour_checker_detection import TESTS_RESOURCES_DIRECTORY
>>> path = os.path.join(TESTS_RESOURCES_DIRECTORY,
... 'colour_checker_detection', 'detection',
... 'IMG_1967.png')
>>> image = read_image(path)
>>> detect_colour_checkers_segmentation(image) # doctest: +SKIP
(array([[ 0.361626... , 0.2241066..., 0.1187837...],
[ 0.6280594..., 0.3950883..., 0.2434766...],
[ 0.3326232..., 0.3156182..., 0.2891038...],
[ 0.3048414..., 0.2738973..., 0.1069985...],
[ 0.4174869..., 0.3199669..., 0.3081552...],
[ 0.347873 ..., 0.4413193..., 0.2931614...],
[ 0.6816301..., 0.3539050..., 0.0753397...],
[ 0.2731050..., 0.2528467..., 0.3312920...],
[ 0.6192335..., 0.2703833..., 0.1866387...],
[ 0.3068567..., 0.1803366..., 0.1919807...],
[ 0.4866354..., 0.4594004..., 0.0374186...],
[ 0.6518523..., 0.4010608..., 0.0171886...],
[ 0.1941571..., 0.1855801..., 0.2750632...],
[ 0.2799946..., 0.3854609..., 0.1241038...],
[ 0.5537481..., 0.2139004..., 0.1267332...],
[ 0.7208045..., 0.5152904..., 0.0061946...],
[ 0.5778360..., 0.2578533..., 0.2687992...],
[ 0.1809450..., 0.3174742..., 0.2959902...],
[ 0.7427522..., 0.6107554..., 0.4398439...],
[ 0.6296108..., 0.5177606..., 0.3728032...],
[ 0.5139589..., 0.4216307..., 0.2992694...],
[ 0.3704401..., 0.3033927..., 0.2093089...],
[ 0.2641854..., 0.2154007..., 0.1441267...],
[ 0.1650098..., 0.1345239..., 0.0817437...]], dtype=float32),)
"""
image = as_float_array(image, FLOAT_DTYPE_DEFAULT)[..., :3]
settings = Structure(**SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC)
settings.update(**kwargs)
image = adjust_image(
image, settings.working_width, settings.interpolation_method
)
swatches_h, swatches_v = (
settings.swatches_horizontal,
settings.swatches_vertical,
)
colour_checkers_colours = []
colour_checkers_data = []
for colour_checker in extract_colour_checkers_segmentation(
image, **settings
):
width, height = colour_checker.shape[1], colour_checker.shape[0]
masks = swatch_masks(width, height, swatches_h, swatches_v, samples)
swatch_colours = []
for mask in masks:
swatch_colours.append(
np.mean(
colour_checker[mask[0] : mask[1], mask[2] : mask[3], ...],
axis=(0, 1),
)
)
# The colour checker might be flipped: The mean standard deviation
# of some expected normalised chromatic and achromatic neutral
# swatches is computed. If the chromatic mean is lesser than the
# achromatic mean, it means that the colour checker is flipped.
std_means = []
for slice_ in [
settings.swatches_chromatic_slice,
settings.swatches_achromatic_slice,
]:
swatch_std_mean = as_float_array(swatch_colours[slice_])
swatch_std_mean /= swatch_std_mean[..., 1][..., np.newaxis]
std_means.append(np.mean(np.std(swatch_std_mean, 0)))
if std_means[0] < std_means[1]:
usage_warning(
"Colour checker was seemingly flipped,"
" reversing the samples!"
)
swatch_colours = swatch_colours[::-1]
colour_checkers_colours.append(np.asarray(swatch_colours))
colour_checkers_data.append((colour_checker, masks))
if additional_data:
return tuple(
DataDetectColourCheckersSegmentation(
tuple(colour_checkers_colours[i]), *colour_checkers_data[i]
)
for i, colour_checker_colours in enumerate(colour_checkers_colours)
)
else:
return tuple(colour_checkers_colours)