Files
MaaAssistantArknights/tools/ImageCropper/colormatcher.py
不留 d025969c82 refactor: 改进 ImageCropper (#14762)
* fix: 修复 ImageCropper 循环异常退出的问题

* feat: 允许 ADB 为空

* fix: 修复 input 为空时的异常

* chore: Auto update by pre-commit hooks [skip changelog]

* chore: 规范变量命名

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-27 18:25:42 +08:00

197 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import cv2
import numpy as np
def kmeansClusterColors(
img,
method: int = -1,
K: int = 3,
criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2),
) -> list:
"""
将图片颜色类聚
Args:
img:
图片
method: int = -1
颜色匹配方式。即 cv::ColorConversionCodes。可选默认 4 (RGB)。
常用值4 (RGB, 3 通道), 40 (HSV, 3 通道), 6 (GRAY, 1 通道)。
默认不进行转换。
K: int = 3
颜色聚类个数
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
K-means 参数,迭代停止的模式选择
Return:
[( center color, array([color, ...]), ... )]
"""
if method >= 0:
img = cv2.cvtColor(img, method)
# 将图像数据转换为一维数组,保留颜色通道
pixels = img.reshape((-1, img.shape[-1]))
# 聚类
_, labels, centers = cv2.kmeans(
pixels.astype(np.float32),
K,
None,
criteria,
10,
cv2.KMEANS_RANDOM_CENTERS,
)
# 将颜色值转换为原来的颜色通道类型
centers = centers.astype(img.dtype)
# __show(centers[labels.reshape(img.shape[:-1])])
# 返回结果
ret = []
for i, center in enumerate(centers):
colors = pixels[(labels == i).flatten()]
ret.append((center, colors))
return ret
def getCount(img, lower, upper, connected: bool, method: int = -1) -> int:
"""
获取匹配成功的数量
Args:
img:
图片
lower:
颜色下限值
upper:
颜色上限值
connected: bool
是否是相连的点才会被计数
method: int = -1
颜色匹配方式。即 cv::ColorConversionCodes。可选默认 4 (RGB)。
常用值4 (RGB, 3 通道), 40 (HSV, 3 通道), 6 (GRAY, 1 通道)。
默认不进行转换。
Return:
int
"""
# https://github.com/MaaXYZ/MaaFramework/blob/main/source/MaaFramework/Vision/ColorMatcher.cpp
# ColorMatcher::color_match
if method >= 0:
img = cv2.cvtColor(img, method)
bin = cv2.inRange(img, np.array(lower, img.dtype), np.array(upper, img.dtype))
# __show(img)
# __show(bin)
if connected:
return __count_non_zero_with_connected(bin)
else:
return cv2.countNonZero(bin)
def __count_non_zero_with_connected(bin):
number, labels, stats, centroids = cv2.connectedComponentsWithStats(
bin, connectivity=8, ltype=cv2.CV_16U
)
count = 0
for i in range(1, number):
x = stats[i][cv2.CC_STAT_LEFT]
y = stats[i][cv2.CC_STAT_TOP]
width = stats[i][cv2.CC_STAT_WIDTH]
height = stats[i][cv2.CC_STAT_HEIGHT]
count = max(
count,
cv2.countNonZero(bin[int(y) : int(y + height), int(x) : int(x + width)]),
)
return count
def showClusterColors(cluster_colors):
"""debug用"""
for center, colors in cluster_colors:
# 创建一个用于显示颜色的图像
img = np.zeros((100, len(colors), 3), dtype=np.uint8)
# 填充颜色
img[:50, :] = center
for i, color in enumerate(colors):
img[50:, i] = color
# 显示图像
__show(img)
def __show(img):
"""debug用"""
cv2.imshow("debug show", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
def __getBoxPlotValues(img, threshold: float = 1.5):
"""return: minimum, lower quartile, median, upper quartile, maximum"""
channel = img.shape[-1]
colors = img.reshape((-1, channel))
ret = ([], [], [], [], [])
for i in range(channel):
ccs = colors[:, i]
# 计算各项数据
q1 = np.percentile(ccs, 25)
q2 = np.percentile(ccs, 50)
q3 = np.percentile(ccs, 75)
# 计算上下边界
iqr = q3 - q1
lower_bound = q1 - iqr * threshold
upper_bound = q3 + iqr * threshold
# 排除异常值
q0 = max(ccs.min(), lower_bound)
q4 = min(ccs.max(), upper_bound)
# 返回结果
ret[0].append(q0.astype(img.dtype))
ret[1].append(q1.astype(img.dtype))
ret[2].append(q2.astype(img.dtype))
ret[3].append(q3.astype(img.dtype))
ret[4].append(q4.astype(img.dtype))
return ret
# 简易方法
def Simple(cluster_colors) -> list[tuple[list[int]]]:
"""
基于四分位数的一种简易匹配方法
Args:
cluster_colors
方法 clusterColors() 返回的结果
Return:
[(center, lower, upper), ...]
"""
ret = []
for center, colors in cluster_colors:
_, lower, _, upper, _ = __getBoxPlotValues(colors)
ret.append((list(center), list(lower), list(upper)))
return ret
def RGBDistance(cluster_colors, threshold: int = 50) -> list[tuple[list[int]]]:
"""
基于 RGB 通道的一种加权欧式距离匹配方法
Args:
cluster_colors
方法 clusterColors() 返回的结果
threshold: int = 50
阈值 0 - 765 (255 * 3)
Return:
[(center, lower, upper), ...]
"""
# https://www.compuphase.com/cmetric.htm
ret = []
for center, colors in cluster_colors:
center = center.astype(np.int32)
colors = colors.astype(np.int32)
rmean = ((colors[:, 0] + center[0]) / 2).astype(np.int32)
r = colors[:, 0] - center[0]
g = colors[:, 1] - center[1]
b = colors[:, 2] - center[2]
distances = np.sqrt(
(((512 + rmean) * r**2) >> 8) + 4 * g**2 + (((767 - rmean) * b**2) >> 8)
)
matched = colors[distances < threshold]
lower, _, _, _, upper = __getBoxPlotValues(matched)
ret.append((list(center), list(lower), list(upper)))
return ret