如何用OpenCV在Python中进行物体检测

744 阅读12分钟

简介

Python在计算机视觉领域有许多应用,通常是通过深度学习。从对文件进行OCR到让机器人 "看见"--计算机视觉是一个令人兴奋和具有挑战性的领域

OpenCV是一个开源的、跨平台的框架,作为一个面向实时计算机视觉的库而开发。由于是跨平台的,你可以通过C++、Python和Java与之对接,而不需要考虑你的操作系统!

计算机视觉是一个广泛的领域,有许多单独的任务/问题你可以尝试解决。其中一个大问题是物体检测

**注:**物体检测是指对图像、视频或流中的物体进行分类(标记)、位置检测轮廓检测(通常是粗略的,如边界框)。这是三个不同的任务,可以从各自的角度来讨论。
非粗略的轮廓检测也可以
称为
图像分割
,如果你将图像分割成每个不同的对象,虽然,图像分割并不限于这种应用。

在本指南中,你将学习如何用OpenCV在Python中进行物体检测。我们将介绍如何使用预先训练好的Haar-Cascade分类器,在图像视频文件实时读取、检测和显示检测到的物体。

让我们从安装OpenCV开始吧!

使用OpenCV进行物体检测

如果你还没有安装OpenCV--安装它的Python驱动很容易,pip

$ pip install opencv-python

就这样吧!OpenCV和它的所有依赖项都将被安装。

**注意:**如果你在安装时遇到了错误,可以尝试安装opencv-contrib-python

现在我们已经建立了我们的库,我们识别物体的第一步是用OpenCV读取和显示一张图片。你可以使用任何你喜欢的图像,在本指南中,我们将使用face_image.jpg

image of a person

cv2 模块的imread() 方法(代表OpenCV)可以用来加载一个图像。然后--我们可以在一个窗口中显示它。

import cv2

image_path = "generic-face.webp" # Put an absolute/relative path to your image
window_name = f"Detected Objects in {image_path}" # Set name of window that shows image
original_image = cv2.imread(image_path)  # Read image in memory
cv2.namedWindow(window_name, cv2.WINDOW_KEEPRATIO) # Create window and set title
cv2.imshow(window_name, original_image)  # Load image in window
cv2.resizeWindow(window_name, (400, 400))  # Resize window
cv2.waitKey(0)  # Keep window open indefinitely until any keypress
cv2.destroyAllWindows()  # Destroy all open OpenCV windows

运行这段代码将带出一个这样的窗口。

OpenCV window displaying an image of person

注意:有时你的操作系统可能不会把窗口带到屏幕的前面,使得代码看起来像是在无限期地运行。如果你在运行该代码后没有看到一个窗口,请确保循环浏览你打开的窗口。

imread() 方法加载图像,imshow() 方法用于在窗口上显示图像。namedWindow()resizeWindow() 方法用于为图像创建一个自定义的窗口,以防止与窗口和图像的大小有关的任何差异。

waitKey() 方法使窗口在给定的毫秒内保持开放,或直到按下一个键。如果数值为0 ,意味着OpenCV将无限期地保持窗口打开,直到我们按下一个键来关闭它。destroyAllWindows() 方法告诉OpenCV关闭它所打开的所有窗口。

有了基本的设置,让我们进行下一步的工作,用OpenCV检测物体。我们需要了解。

  1. 如何用OpenCV画图(在检测到物体时对其进行 "定位"/划线)
  2. 哈尔级联分类器(OpenCV如何区分物体)

如何使用OpenCV绘图?

OpenCV可以绘制各种形状,包括矩形、圆形和直线。我们甚至可以使用putText() ,将标签与形状放在一起。让我们使用rectangle() 方法在图像中画一个简单的矩形,该方法需要位置参数、颜色和形状的厚度。

在读取图像后和命名窗口前,添加一个新行来创建一个矩形。

# Reading the image
...

original_image = cv2.imread(image_path)
rectangle = cv2.rectangle(original_image, 
                          (200, 100), # X-Y start
                          (900, 800), # X-Y end
                          (0, 255, 0), 
                          2)
cv2.namedWindow(window_name, cv2.WINDOW_KEEPRATIO)

# Naming the window
...

现在,重新运行你的代码,可以看到在图像上画出一个矩形。

OpenCV window displaying an image of a person with a rectangle drawn to identify their face

这里,我们用cv2.rectangle() 调用固定了矩形的位置。这些位置是要从图像中推断出来的,而不是猜出来的。这就是OpenCV可以做的繁重工作一旦它做到了--我们就可以用这个确切的方法在检测到的物体周围画一个矩形来代替。

像这样画矩形(或圆形)是物体检测中的一个重要步骤,因为它可以让我们以一种清晰的方式来说明(标注)我们检测到的物体。

现在我们已经完成了OpenCV的绘制工作,让我们来看看Haar级联分类器的概念,它是如何工作的,以及它是如何让我们识别图像中的物体的。

哈尔级联分类器

Haar-级联分类器是一种机器学习分类器,它使用Haar特征。它体现在cv2.CascadeClassifier 类中。OpenCV预装了几个XML文件,每个文件都有不同对象的Haar特征

Haar特征的工作方式与普通卷积神经网络(CNN)特征图相似。

这些特征是针对图像的许多区域计算的,其中的像素强度被加总,然后再计算这些加总之间的差异。这种对图像的下采样,导致了一个简化的特征图,可用于检测图像中的模式。

注意:现在有很多模式识别的选择,包括极其强大的网络,它们比Haar-级联分类器有更好的准确性和更大的灵活性。Haar特征和Haar-级联分类器的主要吸引力在于它的速度。它确实很适合于实时物体检测,在这里它的用途最大。

当你安装OpenCV的时候,你可以获得包含Haar特征的XML文件。

  1. 眼睛
  2. 正面
  3. 全身
  4. 上半身
  5. 下半身
  6. 猫咪
  7. 停车标志
  8. 车牌,等等。

你可以在GitHub的官方仓库中找到它们的文件名。

这些文件涵盖了相当广泛的使用范围!例如,让我们加载眼睛的分类器,并尝试在我们加载的图像中检测眼睛,在检测到的物体周围画一个矩形。

import cv2

image_path = "face_image.jpg"
window_name = f"Detected Objects in {image_path}"
original_image = cv2.imread(image_path)

# Convert the image to grayscale for easier computation
image_grey = cv2.cvtColor(original_image, cv2.COLOR_RGB2GRAY)

cascade_classifier = cv2.CascadeClassifier(
    f"{cv2.data.haarcascades}haarcascade_eye.xml")
detected_objects = cascade_classifier.detectMultiScale(image_grey, minSize=(50, 50))

# Draw rectangles on the detected objects
if len(detected_objects) != 0:
    for (x, y, width, height) in detected_objects:
        cv2.rectangle(original_image, (x, y),
                      (x + height, y + width),
                      (0, 255, 0), 2)

cv2.namedWindow(window_name, cv2.WINDOW_KEEPRATIO)
cv2.imshow(window_name, original_image)
cv2.resizeWindow(window_name, 400, 400)
cv2.waitKey(0)
cv2.destroyAllWindows()

运行这段代码应该显示出与此类似的东西。

OpenCV window displaying an image of a person with their eyes detected by code

这里,我们对分类器的图像进行了灰度处理,以减少计算成本(更多的信息意味着更多的计算)。颜色对这个检测来说并不重要,因为定义眼睛的图案无论是否有颜色,看起来都是一样的。

cascade_classifier 是一个CascadeClassifier 实例,为眼睛加载了Haar特征。我们正在通过f-字符串动态地定位文件!

detectMultiScale() 方法是进行实际检测的,可以检测图像上的同一个物体,而不考虑比例。它返回一个检测到的物体的坐标列表,以矩形(图元)的形式。这使得我们可以很自然地用矩形来勾勒它们的轮廓。detected_objects对于每个位于(x, y, width, height) 的元组,我们可以画一个矩形。

minSize 参数定义了要考虑的对象的最小尺寸。如果你把尺寸设置得非常小,分类器很可能会在图像上发现大量的假阳性。这通常取决于你所处理的图像的分辨率和物体的平均尺寸。在实践中,它可以归结为合理的测试尺寸,直到它表现良好。

让我们把最小尺寸设置为(0, 0) ,看看有什么东西被选中。

changing the minsize argument of multiScaleDetect()

在这张图片中,没有其他可以被误判为眼睛的绒毛,所以我们实际上只有两个误判。一个在眼睛本身,一个在下巴上。根据图像的分辨率以及内容,设置一个低尺寸可能最终会错误地突出图像的很大一部分。

为所有其他图像检测物体的过程是一样的。你加载经过正确训练的分类器,运行detectMultiScale() ,然后在detected_objects

值得注意的是,你可以将多个分类器结合起来!例如,你可以分别检测一个人的正面、眼睛和嘴巴,然后在它们上面作画。让我们加载这些分类器,并使用相同的图像,对每个对象类型使用不同的颜色。

import cv2

image_path = "face_image.jpg"
window_name = f"Detected Objects in {image_path}"
original_image = cv2.imread(image_path)

# Convert the image to grayscale for easier computation
image_grey = cv2.cvtColor(original_image, cv2.COLOR_RGB2GRAY)

eye_classifier = cv2.CascadeClassifier(
    f"{cv2.data.haarcascades}haarcascade_eye.xml")

face_classifier = cv2.CascadeClassifier(
    f"{cv2.data.haarcascades}haarcascade_frontalface_alt.xml")

smile_classifier = cv2.CascadeClassifier(
    f"{cv2.data.haarcascades}haarcascade_smile.xml")


detected_eyes = eye_classifier.detectMultiScale(image_grey, minSize=(50, 50))
detected_face = face_classifier.detectMultiScale(image_grey, minSize=(50, 50))
detected_smile = smile_classifier.detectMultiScale(image_grey, minSize=(200, 200))

# Draw rectangles on eyes
if len(detected_eyes) != 0:
    for (x, y, width, height) in detected_eyes:
        cv2.rectangle(original_image, (x, y),
                      (x + height, y + width),
                      (0, 255, 0), 2)
# Draw rectangles on eyes
if len(detected_face) != 0:
    for (x, y, width, height) in detected_face:
        cv2.rectangle(original_image, (x, y),
                      (x + height, y + width),
                      (255, 0, 0), 2)
        
# Draw rectangles on eyes
if len(detected_smile) != 0:
    for (x, y, width, height) in detected_smile:
        cv2.rectangle(original_image, (x, y),
                      (x + height, y + width),
                      (0, 0, 255), 2)

cv2.namedWindow(window_name, cv2.WINDOW_KEEPRATIO)
cv2.imshow(window_name, original_image)
cv2.resizeWindow(window_name, 400, 400)
cv2.waitKey(0)
cv2.destroyAllWindows()

在这里,我们已经加载了三个分类器--一个用于微笑,一个用于眼睛,一个用于脸部。每个分类器都在图像上运行,我们在所有检测到的物体周围画出矩形,根据物体的类别给矩形着色。

Running multiple haar-cascade classifiers on the same image

笑容没有被很好地识别出来--也许是因为图像中的笑容是相当中性的。这不是一个宽大的微笑,这可能会使分类器失去作用。

使用OpenCV进行视频中的物体检测

随着图像中的物体检测的结束--让我们转到视频。无论如何,视频只是短时连续的图像,所以应用的过程基本相同。不过这一次,它们被应用于每一帧。

要检测视频中的物体,首要步骤是在程序中加载视频文件。加载视频文件后,我们必须逐帧分离视频数据,并像以前一样进行物体检测。

使用OpenCV加载视频

在本指南中,我们将使用一个免费提供的猫在树上的视频,保存为cat-on-tree.mp4 。根据视频创建者的说法,该文件是可以免费使用的,所以我们可以开始了!首先让我们加载视频。

让我们首先加载视频并显示它。

import cv2
import time

video_path = "cat-on-tree.mp4"
window_name = f"Detected Objects in {video_path}"
video = cv2.VideoCapture(video_path)

while True:
    # read() returns a boolean alongside the image data if it was successful
    ret, frame = video.read()
    # Quit if no image can be read from the video
    if not ret:
        break
    # Resize window to fit screen, since it's vertical and long
    cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
    cv2.imshow(window_name, frame)
    if cv2.waitKey(1) == 27:
        break
    # Sleep for 1/30 seconds to get 30 frames per second in the output
    time.sleep(1/30)

video.release()
cv2.destroyAllWindows()

这段代码将读取视频文件并显示其内容,直到按下键EscVideoCapture() 是用来从路径中读取视频文件的,如果我们在方法中给出值0 ,它将打开网络摄像头并从输入中读取帧。我们以后会这样做,现在先处理一个本地视频文件。

现在,我们可以像以前一样对视频中的每张图片应用Haar-Cascade分类器。

import cv2
import time

video_path = "cat-on-tree.mp4"
window_name = f"Detected Objects in {video_path}"
video = cv2.VideoCapture(video_path)

while True:
    # read() returns a boolean alongside the image data if it was successful
    ret, frame = video.read()
    # Quit if no image can be read from the video
    if not ret:
        break
    cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
    # Greyscale image for classification
    image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # Define classifier
    cascade_classifier = cv2.CascadeClassifier(
        f"{cv2.data.haarcascades}haarcascade_frontalcatface.xml")
    # Detect objects
    detected_objects = cascade_classifier.detectMultiScale(
        image, minSize=(50, 50))
    # Draw rectangles
    if len(detected_objects) != 0:
        for (x, y, height, width) in detected_objects:
            cv2.rectangle(
                frame, (x, y), ((x + height), (y + width)), (0, 255, 0), 15)
    #Show image
    cv2.imshow(window_name, frame)
    
    if cv2.waitKey(1) == 27:
        break

video.release()
cv2.destroyAllWindows()

这个分类器是在猫的正面图像上训练的,这意味着它不能真正检测到轮廓。在视频的大部分时间里,猫都是以轮廓的方式定位的,所以在它把脸移向摄像机之前,肯定会有大量的错误分类。

碰巧的是,模糊的背景有一些特征,分类器发现这些特征可能是猫脸。不过,一旦它移动了头--它就清楚地锁定了它的脸。

这就是它在猫看向侧面时的分类结果。

Haar Cascade Classifier not detecting cat when looking to the side

而当猫咪面对镜头时,它又是如何正确识别的。

Haar Cascade Classifier correctly detects cat when facing the camera

实际上,我们是在视频中实时检测这些盒子。我们还可以保存这些检测到的物体(同样,只是一个数字列表),并在每一帧中 "离线 "绘制它们,在检测过程中重新渲染视频以节省CPU功率。

使用OpenCV进行实时的物体检测

检测实时视频中的物体与从视频或图像中检测没什么不同。我们已经在视频中实时检测了猫脸,虽然,视频是本地的。

让我们从网络摄像头获取视频流吧!为了获取来自网络摄像头的输入,我们必须对VideoCapture() 的调用做一点改变。如前所述,我们不是给它一个文件路径,而是给它一个数字(在大多数情况下,0 ,当你有一个网络摄像头时)。

import cv2

window_name = "Detected Objects in webcam"
video = cv2.VideoCapture(0)

while video.isOpened():
    ret, frame = video.read()
    if not ret:
        break
    cv2.imshow(window_name, frame)
    if cv2.waitKey(1) == 27:
        break

video.release()
cv2.destroyAllWindows()

注意:在macOS上,你可能必须给终端或运行终端的程序以使用网络摄像头的权限,这样才能工作。

现在,为了进行实时物体检测,我们可以采用与视频文件相同的方法,即对每一帧进行隔离,逐帧检测物体并统一显示。

import cv2

window_name = "Detected Objects in webcam"
video = cv2.VideoCapture(0)

while video.isOpened():
    ret, frame = video.read()

    if not ret:
        break

    image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    cascade_classifier = cv2.CascadeClassifier(
        f"{cv2.data.haarcascades}haarcascade_frontalface_default.xml")
    detected_objects = cascade_classifier.detectMultiScale(
        image, minSize=(20, 20))

    if len(detected_objects) != 0:
        for (x, y, height, width) in detected_objects:
            cv2.rectangle(
                frame, (x, y), ((x + height), (y + width)), (0, 255, 0), 5)
    cv2.imshow(window_name, frame)

    if cv2.waitKey(1) == 27:
        break

video.release()
cv2.destroyAllWindows()

当你运行上述代码时,一个窗口会从你的网络摄像头中弹出,你会看到一个突出你脸部的矩形这段代码很可能比前一段代码运行得更快,因为网络摄像头通常没有真正的高分辨率,所以这些图像的计算成本要低得多。

如果你坐在一个光线充足的房间里,或者至少有一个光源对着你的脸,就会有帮助。

结论

在本指南中,我们使用OpenCV在Python中进行了物体检测,使用了Haar-Cascade分类器。

我们已经介绍了分类器、Haar特征,并对图像、实时视频以及来自网络摄像头的视频流进行了物体检测。

使用OpenCV进行物体检测的下一步是探索其他分类器,如Yolomobilenetv3,因为你从Haar Cascades得到的准确度与深度神经网络的替代品相比是很不理想的。