使用 K-Means 聚类来识别球员球衣颜色

足球是世界上最受欢迎的运动。在洪都拉斯,足球能够吸引大众的注意力,并在 90 分钟内让人群陷入情绪的漩涡。
多年来,我们看到各种技术被实施,以获取有关比赛内事件和球员表现的各种统计数据和信息。
通常,不仅为足球,而且为许多其他运动开发/实施的最有趣的技术应用程序之一是计算机视觉。计算机视觉 (CV) 是有关开发能够理解图像或视频等视觉数据的算法和/或人工智能的领域。CV 非常强大,在 Instagram 过滤器、自动驾驶汽车、MRI 重建、癌症检测等许多应用中都很常见。
在这个项目中,我们在不同的足球比赛中拍摄了一系列视频片段,并使用 K-Means 聚类算法确定了球员球衣颜色的颜色。
本文将详细介绍实现该目标的过程。这里开发的例程将视频片段作为输入,并生成一个包含聚类过程结果的 pandas 数据帧作为输出。
这个项目需要执行数据清理、聚类、图像/视频处理、图像中对象的基本分类、读取 JSON 文件以及各种 pandas/numpy 数组/列表操作。
本文目录:
图像处理基础
从图像中提取颜色
从视频文件中提取帧
从JSON文件中提取播放器边界框
实现K-Means聚类确定球员球衣颜色
制作用于快速可视化聚类结果的GUI
结论
让我们开始吧!
图像/视频处理基础
本节将介绍对本项目很重要的图像和视频处理/操作的基础知识。
使用足球历史上我最喜欢的时刻之一作为参考图像来尝试各种可用的处理技术。那一刻是罗纳尔迪尼奥在 2005 年 11 月 19 日效力于巴塞罗那足球俱乐部时,对阵皇家马德里的精彩进球,如下图所示。

2005 年 11 月 19 日,罗纳尔迪尼奥对皇家马德里的进球。
使用 OpenCV 加载图像
需要做的第一件事是将图像加载到笔记本中。如果你将图像保存在计算机上,则可以简单地使用cv2.imread
函数。但是,对于在这部分工作中使用的图像,是通过 URL 获取的。然后,加载图像需要我们:
将我们的 URL 传入
urllib.request.urlopen
从 URL 中的图像创建一个 numpy 数组
cv2.imdecode
用于从内存缓存中读取图像数据,并将其转换为图像格式。由于
cv2.imdecode
默认以 BGR 格式加载图像,因此我将使用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
原始 RGB 处理和渲染图像。
#Render image from URL
req = urllib.request.urlopen('https://www.sportbible.com/cdn-cgi/image/width=648,quality=70,format=webp,fit=pad,dpr=1/https%3A%2F%2Fs3-images.sportbible.com%2Fs3%2Fcontent%2Fcf2701795dd2a49b4d404d9fa38f99fd.jpg')
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
bgr_img = cv2.imdecode(arr, -1) # 'Load it as it is'
# Determine the figures size in inches to fit image
dpi = plt.rcParams['figure.dpi']
height, width, depth = bgr_img.shape
figsize = width / float(dpi), height / float(dpi)
plt.figure(figsize=figsize)
plt.imshow(bgr_img)
plt.show()
此过程的结果如下所示:

使用 OpenCV 在 BGR 空间中加载的图像。
如你所见,加载的此图像中的颜色与原始图像中看到的颜色不匹配。这是因为 OpenCV 在 BGR 颜色空间中默认加载的图像。
不过问题不大,因为切换到 RGB 颜色空间可以通过快速的代码行来完成,如下所示:
#Convert image to RGB from BGR
rgb_img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)
plt.figure(figsize=figsize)
plt.imshow(rgb_img)
plt.show()

使用 OpenCV 在 BGR 空间中加载的图像。
现在可以开始使用各种图像处理/操作技术了。将在这里展示其中的一些!
旋转图像
有几种不同的方法可以旋转图像。imutils 包通过imutils.rotate_bound
函数具有最简单的实现,因为它所需要的只是要旋转的图像,以及我们要旋转图像的角度。
除此之外,此功能确保显示的旋转图像不会被裁剪并完全包含在边界内。其他方法需要首先构建旋转矩阵,然后应用旋转矩阵。
#Rotating an image
rotated0 = imutils.rotate_bound(rgb_img,0)
rotated45 = imutils.rotate_bound(rgb_img,45)
rotated90 = imutils.rotate_bound(rgb_img,90)
fig,axs = plt.subplots(1,3, figsize=(30,15))
axs[0].imshow(rotated0)
axs[1].imshow(rotated45)
axs[2].imshow(rotated90)
plt.show()
此操作的结果如下所示:

在 Python 中旋转图像。
裁剪图像
通过 OpenCV 加载图像时,图像被加载为 numpy 数组。然后,要裁剪图像,我们可以简单地使用 numpy 切片来裁剪内容。
我们有多种裁剪的方法。将在这里展示一个简单的示例,我们可以按不同的高度和宽度百分比裁剪图像。通过定义感兴趣区域 (ROI) 和轮廓,将在后面的部分中展示更多的方法来裁剪。
#Need to find the starting/ending column and row index first for the desired cropping
cropIni = [0.15,0.3,0.45]
#Crop width and height of image by 15% each
startRow1 = int(height*cropIni[0]) ;startCol1 = int(width*cropIni[0])
endRow1 = int(height*(1-cropIni[0])) ;endCol1 = int(width*(1-cropIni[0]))
#Crop width and height of image by 30% each
startRow2= int(height*cropIni[1]) ;startCol2 = int(width*cropIni[1])
endRow2 = int(height*(1-cropIni[1])) ;endCol2 = int(width*(1-cropIni[1]))
#Crop width and height of image by 40% each
startRow3 = int(height*cropIni[2]) ;startCol3 = int(width*cropIni[2])
endRow3 = int(height*(1-cropIni[2])) ;endCol3 = int(width*(1-cropIni[2]))
#This is just slicing the array
fig,axs = plt.subplots(1,3, figsize=(30,15))
crop1 = rgb_img[startRow1:endRow1, startCol1:endCol1]
crop2 = rgb_img[startRow2:endRow2, startCol2:endCol2]
crop3 = rgb_img[startRow3:endRow3, startCol3:endCol3]
axs[0].imshow(crop1)
axs[1].imshow(crop2)
axs[2].imshow(crop3)
plt.show()

通过 Python 中的 numpy 切片裁剪图像。
调整图像大小
调整图像大小的方法有很多。在这里,将展示如何使用 OpenCV 中的 resize 函数调整图像大小。尽管图像看起来相同,但可以看出,当我们调整图像大小时,图像的大小(高度和宽度)会发生变化。
#Resizing an image
#cv2.resize(src, dsize[, dst[, fx[, fy[, interpolation]]]])
xscale = [0.75,0.5,0.25]
yscale = [0.75,0.5,0.25]
rimg1 = cv2.resize(rgb_img, (0,0), fx=xscale[0], fy=yscale[0])
rimg2 = cv2.resize(rgb_img, (0,0), fx=xscale[1], fy=yscale[1])
rimg3 = cv2.resize(rgb_img, (0,0), fx=xscale[2], fy=yscale[2])
fig,axs = plt.subplots(1,3, figsize=(30,15))
axs[0].imshow(rimg1)
axs[1].imshow(rimg2)
axs[2].imshow(rimg3)
plt.show()
print("The width, height and depth of this image are ",rimg1.shape)
print("The width, height and depth of this image are ",rimg2.shape)
print("The width, height and depth of this image are ",rimg3.shape)

在 Python 中调整图像大小。
The width, height and depth of this image are (304, 486, 3)
The width, height and depth of this image are (202, 324, 3)
The width, height and depth of this image are (101, 162, 3)
调整图像的亮度/对比度
可以通过OpenCV 中的addWeighted
功能来调整图像的亮度/对比度。这是一个称为混合的过程。此函数使用以下转换对图像进行这些调整:
result = αsrc1 + βsrc2 + γ
在上面的等式中,通过将α
值应用于源图像、将β
值应用于其他图像(它可以是相同的源图像)并将其值增加来修改混合图像γ
。
混合效果如下图所示。
第一行图显示了α
在保持其他两个参数不变的情况下变化的效果(α
从左到右递减)。
第二行图显示了β
在保持其他两个参数不变的情况下变化的效果(β
从左到右增加)。
第三行图显示了γ
在保持其他两个参数不变的情况下变化的效果(γ
从左到右增加)。
减小
α
使图像变暗。增加
β
使图像具有更强的对比度。减小
γ
使图像柔化。
#cv2.addWeighted(source_img1, alpha, source_img2, beta, gamma)
alpha = [0.75, 0.5, 0.25]
beta = [0, 1 , 10]
gamma = [0, 10 ,100]
#Vary alpha
alpha_img1 = cv2.addWeighted(rgb_img, alpha[0], rgb_img, beta[0], gamma[0])
alpha_img2 = cv2.addWeighted(rgb_img, alpha[1], rgb_img, beta[0], gamma[0])
alpha_img3 = cv2.addWeighted(rgb_img, alpha[2], rgb_img, beta[0], gamma[0])
#Vary beta
beta_img1 = cv2.addWeighted(rgb_img, alpha[0], rgb_img, beta[0], gamma[0])
beta_img2 = cv2.addWeighted(rgb_img, alpha[0], rgb_img, beta[1], gamma[0])
beta_img3 = cv2.addWeighted(rgb_img, alpha[0], rgb_img, beta[2], gamma[0])
#Vary gamma
gamma_img1 = cv2.addWeighted(rgb_img, alpha[0], rgb_img, beta[0], gamma[0])
gamma_img2 = cv2.addWeighted(rgb_img, alpha[0], rgb_img, beta[0], gamma[1])
gamma_img3 = cv2.addWeighted(rgb_img, alpha[0], rgb_img, beta[0], gamma[2])

在 Python 中更改图像的亮度和对比度。
更改图像的色彩空间
图像处理中使用了多种颜色空间,可以促进各种任务,例如边缘检测、颜色检测和应用蒙版等等。
使用 OpenCV 通过cvtColor
函数可以很容易地在颜色空间之间进行转换
下面列出了一些常见的色彩空间:
RGB -> 许多图像最初都是使用这种格式编码的
HSV -> 提供对颜色色调的更好控制
灰色 -> 使许多图像处理方法更准确
改变颜色空间的一些示例如下所示:
gray_img = cv2.cvtColor(rgb_img, cv2.COLOR_RGB2GRAY)
bgr_img = cv2.cvtColor(rgb_img, cv2.COLOR_