ANN-计算机视觉应用构建指南-四-

124 阅读46分钟

ANN 计算机视觉应用构建指南(四)

原文:Building Computer Vision Applications Using Artificial Neural Networks

协议:CC BY-NC-SA 4.0

七、实际例子:视频中的对象跟踪

本章的重点是计算机视觉的两个关键能力:目标检测和目标跟踪。一般而言,在一组图像的背景下,对象检测提供了识别图像中的一个或多个对象的能力,而对象跟踪提供了在一组图像上跟踪检测到的对象的能力。在之前的章节中,我们探讨了训练深度学习模型以检测对象的技术方面。在这一章中,我们将探索一个简单的例子,将这些知识应用到视频中。

视频中的对象跟踪,或简称为视频跟踪,涉及检测和定位对象并随时间跟踪它。视频跟踪不仅要检测不同帧中的目标,还要跨帧跟踪目标。当第一次检测到一个对象时,它的唯一身份被提取,然后在后续的帧中被跟踪。

对象跟踪在现实世界中有许多应用,例如:

  • 自动驾驶汽车

  • 安全和监控

  • 交通控制

  • 增强现实

  • 犯罪侦查和犯罪追踪

  • 医学成像等

在这一章中,我们将学习如何实现视频跟踪,并完成代码示例。在本章结束时,你将拥有一个功能齐全的视频跟踪系统。

我们的高级实施计划如下:

  1. 视频源:我们将使用 OpenCV 从网络摄像头或笔记本电脑的内置摄像头读取实时视频流。您也可以从文件或 IP 摄像头读取视频。

  2. 对象检测模型:我们将使用在 COCO 数据集上预先训练的 SSD 模型。您可以为您的特定用例训练您自己的模型(查看第六章了解关于训练对象检测模型的信息)。

  3. 预测:我们将预测视频每一帧内的物体类别(检测)及其包围盒(定位)(查看第六章了解检测图像中物体的信息)。

  4. 唯一标识:我们将使用散列算法来创建每个对象的唯一标识。我们将在本章后面了解更多关于散列算法的知识。

  5. 跟踪:我们将使用汉明距离算法(本章稍后会详细介绍)来跟踪之前检测到的物体。

  6. Display :我们将流式输出视频,以便在 web 浏览器中显示。我们将为此使用烧瓶。Flask 是一个轻量级的 web 应用微框架。

准备工作环境

让我们建立一个目录结构,这样就可以很容易地遵循代码并完成下面的例子。我们将看到前面描述的六个步骤的代码片段。最后,我们将把所有的东西放在一起,使目标跟踪系统完整和可行。

我们有一个名为video_tracking的目录。其中有一个名为templates的子目录,里面有一个名为index.html的 HTML 文件。子目录templates是 Flask 查找 HTML 页面的标准位置。在video_tracking目录中,我们有四个 Python 文件:videoasync.pyobject_tracker.pytracker.pyvideo_server.py。图 7-1 显示了该目录结构。

img/493065_1_En_7_Fig1_HTML.jpg

图 7-1

代码目录结构

我们将把videoasync作为一个模块导入到object_tracker.py中。因此,目录video_tracking必须被认为是 PyCharm 中的源目录。要使其成为 PyCharm 中的源目录,点击屏幕左上方的 PyCharm 菜单选项,然后点击 Preferences,展开左侧面板中的 Project,点击 Project Structure,高亮显示video_tracking目录,点击 Mark as Source(位于屏幕上方),如图 7-2 所示。最后,单击确定关闭窗口。

img/493065_1_En_7_Fig2_HTML.png

图 7-2

在 PyCharm 中将目录标记为源

读取视频流

OpenCV 提供了连接视频源和从视频帧中读取图像的便捷方法。OpenCV 在内部将这些帧中的图像转换成 NumPy 数组。这些 NumPy 数组被进一步处理以检测和跟踪其中的对象。检测过程是计算密集型的,它可能跟不上读取帧的速度。因此,在主线程中读取帧和执行检测操作会表现出较低的性能,尤其是在处理高清(HD)视频时。在清单 7-1 中,我们将实现多线程来捕捉帧。我们称之为视频帧的异步读取

1    # file: videoasync.py
2    import threading
3    import cv2
4
5    class VideoCaptureAsync:
6       def __init__(self, src=0):
7           self.src = src
8           self.cap = cv2.VideoCapture(self.src)
9           self.grabbed, self.frame = self.cap.read()
10          self.started = False
11          self.read_lock = threading.Lock()
12
13      def set(self, key, value):
14          self.cap.set(key, value)
15
16      def start(self):
17          if self.started:
18              print('[Warning] Asynchronous video capturing is already started.')
19              return None
20          self.started = True
21          self.thread = threading.Thread(target=self.update, args=())
22          self.thread.start()
23          return self
24
25      def update(self):
26          while self.started:
27              grabbed, frame = self.cap.read()
28              with self.read_lock:
29                  self.grabbed = grabbed
30                  self.frame = frame
31
32      def read(self):
33          with self.read_lock:
34              frame = self.frame.copy()
35              grabbed = self.grabbed
36          return grabbed, frame
37
38      def stop(self):
39          self.started = False
40         self.thread.join()
41
42
43
44      def __exit__(self, exec_type, exc_value, traceback):
45          self.cap.release()

Listing 7-1Implementation of Asynchronous Reading of Video Frames

文件videoasync.py实现了类VidoCaptureAsync(第 5 行),它由一个构造函数和启动线程、读取帧和停止线程的函数组成。

第 6 行定义了一个将视频源作为参数的构造函数。该源的默认值src=0(也称为设备索引)代表来自笔记本电脑内置摄像头的输入。如果您有 USB 摄像头,请相应地设置此src的值。如果您的计算机端口上连接了多个摄像机,则没有标准的方法来查找设备索引。一种方法是从起始索引 0 开始循环,直到连接到设备。您可以打印设备属性来标识您想要连接的设备。对于基于 IP 的摄像机,传递 IP 地址或 URL。

如果您的视频源是一个文件,请传递视频文件的路径。

第 8 行使用 OpenCV 的VideoCapture()函数,并传递源 ID 来连接视频源。分配给self.cap变量的VideoCapture对象用于读取帧。

第 9 行读取第一帧,并占用与摄像机的连接。

第 10 行是用于管理锁的标志。第 11 行实际上获得了线程锁。

第 13 行和第 14 行实现了一个函数来设置VideoCapture对象的属性,比如帧高、宽度和每秒帧数(FPS)。

第 16 到 23 行实现了启动线程异步读取帧的函数。

第 25 到 30 行实现了一个update()函数来读取帧并更新类级别的帧变量。更新函数在第 21 行的开始函数中内部使用,以异步读取视频帧。

第 32 到 36 行实现了read()函数。read()功能只是返回在update()功能块中更新的帧。这还会返回一个布尔值来指示该帧是否被成功读取。

第 38 到 40 行实现了stop()函数来停止线程并将控制权返回给主线程。join()函数防止主线程关闭,直到子线程完成它的执行。

在退出时,视频源被释放(第 45 行)。

我们现在将编写代码来利用异步视频读取模块。在同一个目录video_tracking中,我们将创建一个名为object_tracker.py的 Python 文件,它实现了以下功能。

加载对象检测模型

我们将使用我们在第六章中使用的相同的预训练 SSD 模型来检测图像中的对象。如果您已经根据自己的图像训练了一个模型,则可以使用该模型。您所要做的就是提供模型目录的路径。清单 7-2 显示了如何从磁盘加载训练好的模型。回想一下,这是我们在第六章的清单 6-11 中使用的同一个函数。我们将只加载模型一次,并使用它来检测所有帧中的对象。

43   # # Model preparation
44   def load_model(model_path):
45      model_dir = pathlib.Path(model_path) / "saved_model"
46      model = tf.saved_model.load(str(model_dir))
47      model = model.signatures['serving_default']
48      return model
49
50   model = load_model(model_path)

Listing 7-2load_model() Function to Load Trained Model from the Disk

检测视频帧中的对象

探测物体的代码和我们在第六章中使用的代码几乎一样。不同之处在于,这里我们创建了一个无限循环,在这个循环中,我们一次读取一个图像帧,并调用函数track_object()来跟踪该帧中的对象。track_object()函数内部调用我们在第六章的清单 6-12 中实现的同一个run_inference_for_single_image()函数。

run_inference_for_single_image()函数的输出是一个包含detection_classesdetection_boxesdetection_scores的字典。我们将利用这些值来计算每个对象的唯一身份,并跟踪它们的位置。

清单 7-3 显示了streamVideo()函数,该函数实现了从视频源读取流帧的无限循环。

在清单 7-3 中,第 115 行启动了streamVideo()函数的块。第 116 行使用了带有线程锁的关键字global

第 117 行开始无限的while循环。在这个循环中,第一行,第 118 行,通过调用VideoCaptureAsync类的read()函数读取当前的视频帧(图像)。read()函数返回一个指示帧是否被成功读取的布尔值元组和一个图像帧的 NumPy 数组。

如果帧被成功检索(第 119 行),则获取锁(第 120 行),以便在当前线程的图像仍在被检测对象时,其他线程不会修改帧编号。

第 121 行通过传递模型对象和框架 NumPy 来调用track_object()函数。我们将在后面的清单 7-13 中看到这个track_object()函数的作用。在第 123 行,输出的 NumPy 数组被转换成压缩的.jpg图像,因此它是轻量级的,易于在网络上传输。我们使用cv2.imencode()将 NumPy 数组转换成图像。此函数返回一个布尔值元组,指示转换是否成功,并返回编码图像。

如果图像转换不成功,则跳过该帧(第 125 行)。

最后,在第 127 行,它产生了字节编码的图像。yield关键字从while循环返回一个只读一次的迭代器。

第 130 到 137 行是当程序被终止或者屏幕被按 Q 键退出时的清理函数。

114  # Function to implement infinite while loop to read video frames and generate the output   #for web browser
115  def streamVideo():
116     global lock
117     while (True):
118         retrieved, frame = cap.read()
119         if retrieved:
120             with lock:
121                 frame = track_object(model, frame)
122
123                 (flag, encodedImage) = cv2.imencode(".jpg", frame)
124                 if not flag:
125                     continue
126
127                 yield (b'--frame\r\n' b'Content-Type: img/jpeg\r\n\r\n' +
128                    bytearray(encodedImage) + b'\r\n')
129
130         if cv2.waitKey(1) & 0xFF == ord('q'):
131             cap.stop()
132             cv2.destroyAllWindows()
133             break
134
135     # When everything done, release the capture
136     cap.stop()
137     cv2.destroyAllWindows()

Listing 7-3Implementing Infinite Loop for Reading Streams of Video Frames and Internally Calling an Object Tracking Function for Each Frame

使用 dHash 为对象创建唯一标识

我们使用感知散列来创建在图像中检测到的对象的唯一身份。差分哈希,简称 dHash,是计算图像唯一哈希最常用的算法之一。dHash 提供了几个优点,使其成为识别和比较图像的合适选择。以下是使用 dHash 的一些好处:

  • 如果长宽比改变,图像哈希不会改变。

  • 亮度或对比度的变化将不会改变图像散列或稍微改变它。这意味着哈希仍然以不同的对比度接近其他哈希。

  • dHash 的计算速度非常快。

我们不使用加密哈希,如 MD-5 或 SHA-1。原因是,对于这些哈希算法,如果映像中有微小的变化,加密哈希将完全不同。即使是单个像素的变化,也会产生完全不同的哈希。因此,如果两个图像在感知上相似,它们的加密哈希将完全不同。这使得当我们必须比较两幅图像时,它不适合应用。

dHash 算法很简单。以下是计算 dHash 的步骤:

  1. 将图像或图像片段转换为灰度。这使得计算速度更快,如果颜色有轻微的变化,dHash 也不会改变太多。在对象检测中,我们使用边界框裁剪检测到的对象,并将裁剪后的图像转换为灰度。

  2. 调整灰度图像的大小。为了计算 64 位哈希,图像被调整为 9×8 像素,忽略其纵横比。纵横比被忽略,以确保得到的图像散列将匹配相似的图像,而不管它们的初始空间尺寸。

    为什么是 9×8 像素?在 dHash 中,该算法计算相邻像素的梯度差。9 行与相邻行的差异将在结果中仅产生 8 行,从而产生具有 8×8 像素的最终输出,这将给出 64 位散列。

  3. 通过应用“大于”公式将每个像素转换为 0 或 1 来构建哈希,如下所示:

如果 P[x=1] > P[x],那么 1 否则 0。

然后,二进制值被转换为整数哈希。

清单 7-4 展示了 dHash 的 Python 和 OpenCV 实现。

32      def getCropped(self, image_np, xmin, ymin, xmax, ymax):
33          return image_np[ymin:ymax, xmin:xmax]
34
35      def resize(self, cropped_image, size=8):
36          resized = cv2.resize(cropped_image, (size+1, size))
37          return resized
38
39      def getHash(self, resized_image):
40          diff = resized_image[:, 1:] > resized_image[:, :-1]
41          # convert the difference image to a hash
42          dhash = sum([2 ** i for (i, v) in enumerate(diff.flatten()) if v])
43          return int(np.array(dhash, dtype="float64"))

Listing 7-4Calculating the dHash from an Image

第 32 行和第 33 行实现了裁剪功能。我们传递完整图像帧的 NumPy 数组和围绕对象的边界框的四个坐标。该功能裁剪图像中包含检测到的对象的部分。

第 35 到 37 行用于将裁剪后的图像调整为 9×8 的大小。

第 39 到 43 行实现了 dHash 的计算。第 40 行通过应用前面描述的大于规则找到相邻像素的差异。第 42 行从二进制位值构建数字散列。第 43 行将散列转换成整数,并从函数中返回 dhash。

利用汉明距离确定图像相似度

汉明距离通常用于比较两个哈希。汉明距离测量两个散列中不同比特的数量。

如果两个散列的汉明距离为零,则意味着这两个散列是相同的。汉明距离越低,两个哈希越相似。

清单 7-5 展示了如何计算两个散列之间的汉明距离。

45      def hamming(self, hashA, hashB):
46          # compute and return the Hamming distance between the integers
47          return bin(int(hashA) ^ int(hashB)).count("1")

Listing 7-5Calculation of the Hamming Distance

第 45 行的函数hamming()将两个散列作为输入,并返回位数,这两个输入散列中的位数是不同的。

目标跟踪

在图像中检测到对象后,通过计算包含该对象的图像的裁剪部分的 dHash 来创建其唯一身份。通过计算物体的 dHash 的汉明距离,从一帧到另一帧跟踪物体。跟踪有许多用例。在我们的示例中,我们创建了两个跟踪函数来完成以下任务:

  1. 从对象在一帧中的第一次出现到后续帧中的所有出现,跟踪对象的路径。该函数跟踪边界框的中心,并绘制一条连接所有这些中心的线或路径。清单 7-6 展示了这个实现。函数createHammingDict()获取当前对象的轮廓、边界框的中心以及所有对象及其中心的历史。该函数将当前对象的数据与迄今为止看到的所有数据进行比较,并使用汉明距离来查找相似的对象,以跟踪其运动或路径。

  2. 获取对象的唯一标识符,并跟踪检测到的唯一对象的数量。清单 7-7 实现了一个名为getObjectCounter()的函数,它计算跨帧检测到的唯一对象的数量。它将当前对象的 dHash 与所有先前帧中计算的所有 dHash 进行比较。

49      def createHammingDict(self, dhash, center, hamming_dict):
50          centers = []
51          matched = False
52          matched_hash = dhash
53          # matched_classid = classid
54
55          if hamming_dict.__len__() > 0:
56              if hamming_dict.get(dhash):
57                  matched = True
58
59              else:
60                  for key in hamming_dict.keys():
61
62                      hd = self.hamming(dhash, key)
63
64                      if(hd < self.threshold):
65                          centers = hamming_dict.get(key)
66                          if len(centers) > self.max_track_frame:
67                              centers.pop(0)
68                          centers.append(center)
69                          del hamming_dict[key]
70                          hamming_dict[dhash] = centers
71                          matched = True
72                          break
73
74          if not matched:
75              centers.append(center)
76              hamming_dict[dhash] = centers
77
78          return  hamming_dict

Listing 7-6Tracking the Centers of Bounding Boxes of Detected Objects Between Multiple Frames

79
80      def getObjectCounter(self, dhash, hamming_dict):
81          matched = False
82          matched_hash = dhash
83          lowest_hamming_dist = self.threshold
84          object_counter = 0
85
86          if len(hamming_dict) > 0:
87              if dhash in hamming_dict:
88                  lowest_hamming_dist = 0
89                  matched_hash = dhash
90                  object_counter = hamming_dict.get(dhash)
91                  matched = True
92
93              else:
94                  for key in hamming_dict.keys():
95                      hd = self.hamming(dhash, key)
96                      if(hd < self.threshold):
97                          if hd < lowest_hamming_dist:
98                              lowest_hamming_dist = hd
99                              matched = True
100                             matched_hash = key
101                             object_counter = hamming_dict.get(key)
102         if not matched:
103             object_counter = len(hamming_dict)
104         if matched_hash in hamming_dict:
105             del hamming_dict[matched_hash]
106
107         hamming_dict[dhash] = object_counter
108         return  hamming_dict
109

Listing 7-7Function to Track Count of Unique Objects Detected in Video Frames

在网络浏览器中显示实况视频流

我们将把我们的视频跟踪代码发布到 Flask,一个轻量级的 web 框架。这将允许我们使用 URL 在 web 浏览器中查看带有跟踪对象的视频直播流。您可以使用其他框架,比如 Django,来发布可以从 web 浏览器访问的视频。我们选择 Flask 作为我们的示例,因为它是轻量级的、灵活的,并且只需要几行代码就可以轻松实现。

让我们探索一下如何在我们当前的上下文中使用 Flask。我们将从安装 Flask 到我们的 virtualenv 开始。

安装烧瓶

我们将使用pip命令来安装 Flask。确保激活 virtualenv 并执行命令pip install flask,如下所示:

 (cv_tf2) computername:~ username$ pip install flask

烧瓶目录结构

参见图 7-1 中的目录结构。我们在video_tracking目录中创建了一个名为templates的子目录。我们将创建一个 HTML 文件,index.html,它将包含显示视频流的代码。我们将把index.html保存到templates目录中。目录名必须是templates,因为 Flask 会在这个目录中查找 HTML 文件。

用于显示视频流的 HTML

清单 7-8 显示了保存在index.html页面中的 HTML 代码。第 7 行是显示实时视频流的最重要的一行。这是 HTML 的标准<img>标签,通常用于在 web 浏览器中显示图像。第 7 行代码的{{...}}部分是 Flask 符号,指示 Flask 从一个 URL 加载图像。当这个 HTML 页面被加载时,它将调用/video_feed URL 并从那里获取图像以显示在<img>标签中。

1    <html>
2     <head>
3       <title>Computer Vision</title>
4     </head>
5     <body>
6       <h1>Video Surveillance</h1>
7       <img src="{{ url_for('video_feed') }}" > </img>
8     </body>
9    </html>
10

Listing 7-8HTML Code for Displaying the Video Stream

现在我们需要一些服务于这个 HTML 页面的服务器端代码。我们还需要一个服务器端实现来在调用/video_feed URL 时提供图像。

我们将在一个单独的 Python 文件video_server.py中实现这两个函数,这个文件保存在video_tracking目录中。确保这个video_server.py文件和templates目录在同一个父目录下。

清单 7-9 展示了 Flask 服务的服务器端实现。2 号线进口烧瓶及其相关包装。第 3 行导入了我们的object_tracker包,它实现了对象检测和跟踪。

第 4 行使用构造函数app = Flask(__name__)创建了一个 Flask 应用,它将当前模块作为参数。通过调用构造函数,我们实例化 Flask web 应用框架,并将其赋给一个名为app的变量。我们将把所有的服务器端服务绑定到这个app

所有 Flask 服务都是通过 URL 提供的,我们必须将 URL 或路由绑定到它将提供的服务。下面是我们需要为我们的示例实现的两个服务:

  • 将从主页 URL 呈现index.html的服务,例如http://localhost:5019/

  • 将从/video_feed URL 提供视频流的服务,例如http://localhost:5019/video_feed

用于加载 HTML 页面的 Flask

清单 7-9 的第 6 行有一个路由绑定/,它表示 home URL。当从 web 浏览器调用 home URL 时,调用函数index()来服务请求(第 7 行)。index()函数只是从模板index.html中呈现一个 HTML 页面,我们在清单 7-8 中创建了这个模板。

提供视频流的烧瓶

清单 7-9 的第 11 行将/video_feed URL 绑定到 Python 函数video_feed()。这个函数反过来调用我们实现的用于检测和跟踪视频中物体的streamVideo()函数。第 15 行从视频帧中创建Response对象,并向调用者发送一个多部分 HTTP 响应。

1    # video_server.py
2    from flask import Flask, render_template, Response
3    import object_tracker as ot
4    app = Flask(__name__)
5
6    @app.route("/")
7    def index():
8       # return the rendered template
9    return render_template("index.html")
10
11   @app.route("/video_feed")
12   def video_feed():
13       # return the response generated along with the specific media
14       # type (mime type)
15       return Response(ot.streamVideo(),mimetype = "multipart/x-mixed-replace; boundary=frame")
16
17   if __name__ == '__main__':
18       app.run(host="localhost", port="5019", debug=True,
19                    threaded=True, use_reloader=False)
20

Listing 7-9Flask Server-Side Code to Launch index.html and Serve Video Stream

运行 Flask 服务器

通过从video_tracking目录键入命令python video_server.py从终端执行video_server.py文件。确保您已经激活了 virtualenv。

(cv) computername:video_tracking username$ python video_server.py

这将启动 Flask 服务器并在host="localhost"port="5019"上运行(清单 7-9 的第 18 行)。您应该为您的生产环境更改主机和端口。此外,通过在第 18 行设置debug=False来关闭调试模式。

当服务器启动时,将您的网络浏览器指向 URL http://localhost:5019/以查看带有对象跟踪的实时视频流。

把所有的放在一起

我们已经探索了视频跟踪系统的构建模块。让我们把它们放在一起,形成一个功能齐全的系统。图 7-3 显示了我们的视频跟踪系统的高级函数调用序列。

img/493065_1_En_7_Fig3_HTML.jpg

图 7-3

视频跟踪系统的功能调用序列示意图

当使用 URL http://localhost:5019/启动 web 浏览器时,Flask 后端服务器服务于index.html页面,该页面在内部调用调用服务器端函数video_feed()的 URL http://localhost:5019/video_feed。其余的函数调用,如图 7-3 所示,完成后将检测到物体的视频帧及其跟踪信息发送到网页浏览器显示。清单 7-10 到 7-14 提供了视频跟踪系统的完整源代码。

清单 7-10 的文件路径为video_tracking/templates/index.html

<html>
 <head>
   <title>Computer Vision</title>
 </head>
 <body>
   <h1>Video Surveillance</h1>
   <img src="{{ url_for('video_feed') }}" > </img>
 </body>
</html>

Listing 7-10index.html

清单 7-11 的文件路径为video_tracking/video_server.py

# video_server.py
from flask import Flask, render_template, Response
import object_tracker as ot
app = Flask(__name__)

@app.route("/")
def index():
   # return the rendered template
  return render_template("index.html")

@app.route("/video_feed")
def video_feed():
  # return the response generated along with the specific media
  # type (mime type)
  return Response(ot.streamVideo(),mimetype = "multipart/x-mixed-replace; boundary=frame")

if __name__ == '__main__':
  app.run(host="localhost", port="5019", debug=True,
        threaded=True, use_reloader=False)

Listing 7-11video_server.py

清单 7-12 的文件路径为video_tracking/object_tracker.py

import os
import pathlib
import random
import numpy as np
import tensorflow as tf
import cv2
import threading

# Import the object detection module.
from object_detection.utils import ops as utils_ops
from object_detection.utils import label_map_util

from videoasync import VideoCaptureAsync
import tracker as hasher

lock = threading.Lock()

# to make gfile compatible with v2
tf.gfile = tf.io.gfile

model_path = "./../model/ssd_inception_v2_coco_2018_01_28"
labels_path = "./../model/mscoco_label_map.pbtxt"

# List of the strings that is used to add correct label for each box

.
category_index = label_map_util.create_category_index_from_labelmap(labels_path, use_display_name=True)
class_num =len(category_index)+100
object_ids = {}
hasher_object = hasher.ObjectHasher()

#Function to create color table for each object class
def get_color_table(class_num, seed=50):
   random.seed(seed)
   color_table = {}
   for i in range(class_num):
       color_table[i] = [random.randint(0, 255) for _ in range(3)]
   return color_table

colortable = get_color_table(class_num)

# Initialize and start the asynchronous video capture thread
cap = VideoCaptureAsync().start()

# # Model preparation
def load_model(model_path):
   model_dir = pathlib.Path(model_path) / "saved_model"
   model = tf.saved_model.load(str(model_dir))
   model = model.signatures['serving_default']
   return model

model = load_model(model_path)

# Predict objects and bounding boxes and format the result
def run_inference_for_single_image(model, image):
   # The input needs to be a tensor, convert it using `tf.convert_to_tensor`.
   input_tensor = tf.convert_to_tensor(image)
   # The model expects a batch of images, so add an axis with `tf.newaxis`.
   input_tensor = input_tensor[tf.newaxis, ...]

   # Run prediction from the model
   output_dict = model(input_tensor)

   # Input to model is a tensor, so the output is also a tensor
   # Convert to NumPy arrays, and take index [0] to remove the batch dimension.
   # We're only interested in the first num_detections.
   num_detections = int(output_dict.pop('num_detections'))
   output_dict = {key: value[0, :num_detections].numpy()
                  for key, value in output_dict.items()}
   output_dict['num_detections'] = num_detections

   # detection_classes should be ints.
   output_dict['detection_classes'] = output_dict['detection_classes'].astype(np.int64)

   return output_dict

# Function to draw bounding boxes and tracking information on the image frame
def track_object(model, image_np):
   global object_ids, lock
   # Actual detection.
   output_dict = run_inference_for_single_image(model, image_np)

   # Visualization of the results of a detection.
   for i in range(output_dict['detection_classes'].size):

       box = output_dict['detection_boxes'][i]
       classes = output_dict['detection_classes'][i]
       scores = output_dict['detection_scores'][i]

       if scores > 0.5:
           h = image_np.shape[0]
           w = image_np.shape[1]

           classname = category_index[classes]['name']
           classid =category_index[classes]['id']
           #Draw bounding boxes
           cv2.rectangle(image_np, (int(box[1] * w), int(box[0] * h)), (int(box[3] * w), int(box[2] * h)), colortable[classid], 2)

           #Write the class name on top of the bounding box
           font = cv2.FONT_HERSHEY_COMPLEX_SMALL

           hash, object_ids = hasher_object.getObjectId(image_np, int(box[1] * w), int(box[0] * h), int(box[3] * w),
                                            int(box[2] * h), object_ids)

           size = cv2.getTextSize(str(classname) + ":" + str(scores)+"[Id: "+str(object_ids.get(hash))+"]", font, 0.75, 1)[0][0]

           cv2.rectangle(image_np,(int(box[1] * w), int(box[0] * h-20)), ((int(box[1] * w)+size+5), int(box[0] * h)), colortable[classid],-1)
           cv2.putText(image_np, str(classname) + ":" + str(scores)+"[Id: "+str(object_ids.get(hash))+"]",
                   (int(box[1] * w), int(box[0] * h)-5), font, 0.75, (0,0,0), 1, 1)

           cv2.putText(image_np, "Number of objects detected: "+str(len(object_ids)),
                       (10,20), font, 0.75, (0, 0, 0), 1, 1)
       else:
           break
   return image_np

# Function to implement infinite while loop to read video frames and generate the output for web browser
def streamVideo():
   global lock
   while (True):
       retrieved, frame = cap.read()
       if retrieved:
           with lock:
               frame = track_object(model, frame)

               (flag, encodedImage) = cv2.imencode(".jpg", frame)
               if not flag:
                   continue

               yield (b'--frame\r\n' b'Content-Type: img/jpeg\r\n\r\n' +
                  bytearray(encodedImage) + b'\r\n')

       if cv2.waitKey(1) & 0xFF == ord('q'):
           cap.stop()
           cv2.destroyAllWindows()
           break

   # When everything done, release the capture
   cap.stop()
   cv2.destroyAllWindows()

Listing 7-12object_tracker.py

清单 7-13 的文件路径为video_tracking/videoasync.py

# file: videoasync.py
import threading
import cv2

class VideoCaptureAsync:
   def __init__(self, src=0):
       self.src = src
       self.cap = cv2.VideoCapture(self.src)
       self.grabbed, self.frame = self.cap.read()
       self.started = False
       self.read_lock = threading.Lock()

   def set(self, var1, var2):
       self.cap.set(var1, var2)

   def start(self):
       if self.started:
           print('[Warning] Asynchronous video capturing is already started.')
           return None
       self.started = True
       self.thread = threading.Thread(target=self.update, args=())
       self.thread.start()
       return self

   def update(self):
       while self.started:
           grabbed, frame = self.cap.read()
           with self.read_lock:
               self.grabbed = grabbed
               self.frame = frame

   def read(self):
       with self.read_lock:
           frame = self.frame.copy()
           grabbed = self.grabbed
       return grabbed, frame

   def stop(self):
       self.started = False
       # self.cap.release()
       # cv2.destroyAllWindows()
       self.thread.join()

   def __exit__(self, exec_type, exc_value, traceback):
       self.cap.release()

Listing 7-13videoasync.py

清单 7-14 的文件路径为video_tracking/tracker.py

# tracker.py
import numpy as np
import cv2

class ObjectHasher:
   def __init__(self, threshold=20, size=8, max_track_frame=10, radius_tracker=5):
       self.threshold = 20
       self.size = 8
       self.max_track_frame = 10
       self.radius_tracker = 5

   def getCenter(self, xmin, ymin, xmax, ymax):
       x_center = int((xmin + xmax)/2)
       y_center = int((ymin+ymax)/2)
       return (x_center, y_center)

   def getObjectId(self, image_np, xmin, ymin, xmax, ymax, hamming_dict={}):
       croppedImage = self.getCropped(image_np,int(xmin*0.8), int(ymin*0.8), int(xmax*0.8), int(ymax*0.8))
       croppedImage = cv2.cvtColor(croppedImage, cv2.COLOR_BGR2GRAY)

       resizedImage = self.resize(croppedImage, self.size)

       hash = self.getHash(resizedImage)
       center = self.getCenter(xmin*0.8, ymin*0.8, xmax*0.8, ymax*0.8)

       # hamming_dict = self.createHammingDict(hash, center, hamming_dict)
       hamming_dict = self.getObjectCounter(hash, hamming_dict)
       return hash, hamming_dict

   def getCropped(self, image_np, xmin, ymin, xmax, ymax):
       return image_np[ymin:ymax, xmin:xmax]

   def resize(self, cropped_image, size=8):
       resized = cv2.resize(cropped_image, (size+1, size))
       return resized

   def getHash(self, resized_image):
       diff = resized_image[:, 1:] > resized_image[:, :-1]
       # convert the difference image to a hash
       dhash = sum([2 ** i for (i, v) in enumerate(diff.flatten()) if v])
       return int(np.array(dhash, dtype="float64"))

   def hamming(self, hashA, hashB):
       # compute and return the Hamming distance between the integers
       return bin(int(hashA) ^ int(hashB)).count("1")

   def createHammingDict(self, dhash, center, hamming_dict):
       centers = []
       matched = False
       matched_hash = dhash
       # matched_classid = classid

       if hamming_dict.__len__() > 0:
           if hamming_dict.get(dhash):
               matched = True

           else:
               for key in hamming_dict.keys():

                   hd = self.hamming(dhash, key)

                   if(hd < self.threshold):
                       centers = hamming_dict.get(key)
                       if len(centers) > self.max_track_frame:
                           centers.pop(0)
                       centers.append(center)
                       del hamming_dict[key]
                       hamming_dict[dhash] = centers
                       matched = True
                       break

       if not matched:
           centers.append(center)
           hamming_dict[dhash] = centers

       return  hamming_dict

   def getObjectCounter(self, dhash, hamming_dict):
       matched = False
       matched_hash = dhash
       lowest_hamming_dist = self.threshold
       object_counter = 0

       if len(hamming_dict) > 0:
           if dhash in hamming_dict:
               lowest_hamming_dist = 0
               matched_hash = dhash
               object_counter = hamming_dict.get(dhash)
               matched = True

           else:
               for key in hamming_dict.keys():
                   hd = self.hamming(dhash, key)
                   if(hd < self.threshold):
                       if hd < lowest_hamming_dist:
                           lowest_hamming_dist = hd
                           matched = True
                           matched_hash = key
                           object_counter = hamming_dict.get(key)
       if not matched:
           object_counter = len(hamming_dict)
       if matched_hash in hamming_dict:
           del hamming_dict[matched_hash]

       hamming_dict[dhash] = object_counter

       return  hamming_dict

   def drawTrackingPoints(self, image_np, centers, color=(0,0,255)):
       image_np = cv2.line(image_np, centers[0], centers[len(centers) - 1], color)
       return image_np

Listing 7-14tracker.py

通过从终端执行命令python video_server.py来运行 Flask 服务器。要观看实时视频流,启动您的网络浏览器并指向 URL http://localhost:5019

摘要

在本章中,我们使用预先训练的 SSD 模型开发了一个全功能视频跟踪系统。我们还学习了差分哈希(dHash)算法,并使用汉明距离来确定图像的相似性。我们将我们的系统部署到 Flask microweb 框架,以在 web 浏览器中呈现实时视频跟踪。

八、实际例子:人脸识别

人脸识别是在图像或视频中检测和识别人脸的计算机视觉问题。面部识别的第一步是在输入图像中检测和定位面部的位置。这是一个典型的对象检测任务,就像我们在前面的章节中了解到的那样。检测到面部后,从面部的各个关键点创建特征集,也称为面部足迹面部嵌入。一张人脸有 80 个节点或区分标志,用于创建特征集(USPTO 专利号 US7634662B2, https://patents.google.com/patent/US7634662B2/ )。然后将嵌入的人脸与数据库进行比较,以确定人脸的身份。

面部识别在现实世界中有许多应用,例如:

  • 作为进入高安全区域的密码

  • 在机场海关和边境保护方面

  • 在识别遗传疾病方面

  • 作为预测个人年龄和性别的一种方式(例如,用于控制基于年龄的访问,如酒精购买)

  • 在执法中(例如,警察通过扫描数百万张照片来发现潜在的犯罪嫌疑人和证人)。

  • 在组织数字相册(例如,社交媒体上的照片)时

在这一章中,我们将探索由谷歌工程师开发的流行的人脸识别算法 FaceNet。我们将学习如何训练基于 FaceNet 的神经网络来开发人脸识别模型。最后,我们将编写代码来开发一个全功能的人脸识别系统,该系统可以从视频流中实时检测人脸。

FaceNet(网面)

FaceNet 是由三位谷歌工程师 Florian Schroff、Dmitry Kalenichenko 和 James Philbin 发明的。他们于 2015 年在一篇题为“FaceNet:人脸识别和聚类的统一嵌入”( https://arxiv.org/pdf/1503.03832.pdf )的论文中发表了他们的工作。

FaceNet 是一个统一的系统,提供以下功能:

  • 人脸验证(这是同一个人吗?)

  • 认可(这个人是谁?)

  • 聚类(有相似的脸吗?)

FaceNet 是一种深度神经网络,具有以下功能:

  • 从输入图像中计算 128D 紧凑特征向量,称为面部嵌入。回想一下第四章中的内容,特征向量包含描述物体重要特征的信息。128D 特征向量是 128 个实数值的列表,表示试图量化面部的输出。

  • 通过优化三重损失函数来学习。我们将在本章后面探讨损失函数。

FaceNet 神经网络体系结构

图 8-1 显示了 FaceNet 架构。

img/493065_1_En_8_Fig1_HTML.png

图 8-1

FaceNet 神经网络体系结构

以下部分描述了 FaceNet 网络的组件。

输入图像

训练集由从图像中裁剪的面部缩略图组成。除了平移和缩放之外,不需要对面裁剪进行其他对齐。

深度 CNN

使用具有反向传播的 SGD 和 AdaGrad 优化器,使用深度卷积神经网络来训练 FaceNet。初始学习率取为 0.05,并随着迭代减少以最终确定模型。培训是在基于 CPU 的集群上进行的,时间为 1,000 到 2,000 小时。

FaceNet 论文描述了具有不同权衡的深度卷积神经网络的两种不同架构。第一个架构的灵感来自泽勒和弗格斯,第二个架构来自谷歌。这两种体系结构主要在两个方面不同:参数的数量和每秒浮点运算次数(FLOPS)。FLOPS 是衡量需要浮点计算的计算机性能的标准。

泽勒和弗格斯 CNN 架构由 22 层组成,在 1.4 亿个参数上训练,每幅图像 16 亿次浮点运算。这种 CNN 架构称为 NN1,其输入大小为 220×220。

表 8-1 显示了 FaceNet 中使用的基于泽勒和弗格斯的网络配置。

表 8-1

深度 CNN 基于泽勒和弗格斯网络架构(来源:施罗夫等人, https://arxiv.org/pdf/1503.03832.pdf )

| - ![img/493065_1_En_8_Figa_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4835f9cf808d489d81aa4cbba9934e8e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771255783&x-signature=zJZS6JtmlJkiHdDd1a7Jo3cIDco%3D) |

第二种类型的网络是基于 GoogLeNet 的初始模型。该模型的参数减少了 20 倍(约 660 万至 750 万),FLOPS 减少了 5 倍(约 5 亿至 16 亿)。

基于输入的大小,有一些初始模型的变体。这里对它们进行了简要描述:

  • 这是一个初始模型,拍摄尺寸为 224×224 的图像,并以每幅图像 16 亿次浮点运算对 750 万个参数进行训练。

表 8-2 显示了 FaceNet 中使用的 NN2 初始模型。

表 8-2

基于 GoogLeNet 的 Inception 模型架构(来源:Schroff 等人, https://arxiv.org/pdf/1503.03832.pdf )

| - ![img/493065_1_En_8_Figb_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/801492bf6fbe49d2ad46105fb8312575~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771255783&x-signature=Sdgn6Ny4DZ2AytNC9UmZm0X9C3E%3D) |
  • NN3 :与 NN2 相比,它在架构上完全相同,只是它使用 160×160 的输入尺寸,导致网络尺寸更小。

  • NN4 :该网络具有 96×96 的输入大小,导致参数大幅减少,每个图像仅需要 2.85 亿次浮点运算(相比之下,NN1 和 NN2 需要 16 亿次浮点运算)。由于 NN4 的尺寸更小,FLOPS 要求的 CPU 时间更少,因此适合移动设备。

  • NNS1 :由于其尺寸较小,这也被称为“迷你”初始。它具有 165×165 的输入大小和 2600 万个参数,每个图像只需要 2.2 亿次浮点运算。

  • NNS2 :这被称为“微小的”开始。它的输入大小为 140×116,有 430 万个参数,需要 2000 万个触发器。

NN4、NNS1 和 NNS2 适用于移动设备,因为其参数数量较少,要求每个映像的 CPU FLOPS 较低。

值得一提的是,FLOPS 越大,模型精度越高。一般来说,FLOPS 越低的网络运行速度越快,消耗的内存越少,但精度也越低。

图 8-2 显示了不同类型 CNN 架构的 FLOPS 与精度的关系图。

img/493065_1_En_8_Fig2_HTML.jpg

图 8-2

FLOPS 与准确性(来源:FaceNet,arxiv . org/pdf/1503 . 03832 . pdf)

人脸嵌入

从深度 CNN 的 L2 归一化层生成大小为 1×1×128 的人脸嵌入(如图 8-1 和表 8-1 和 8-2 )。

在计算嵌入之后,通过计算嵌入之间的欧几里德距离并基于以下找到相似的面部来执行面部验证(或找到相似的面部):

  • 同一个人的脸之间的距离较小

  • 不同人的脸有更大的距离

通过标准的 K-最近邻(K-NN)分类来执行人脸识别。

使用像 K-means 或凝聚聚类技术这样的算法来完成聚类。

三重损失函数

FaceNet 中使用的损失函数被称为三重损失函数

相同人脸的嵌入称为,不同人脸的嵌入称为。被分析的人脸被称为主播。为了计算损失,形成由锚、正和负嵌入组成的三元组,并分析它们的欧几里德距离。FaceNet 的学习目标是最小化锚和正面之间的距离,最大化锚和负面之间的距离。

图 8-3 说明了三重损失函数和学习过程。

img/493065_1_En_8_Fig3_HTML.jpg

图 8-3

三重损失最小化了锚和具有相同身份的正片之间的距离,并且最大化了锚和不同身份的负片之间的距离。(来源:FaceNet,arxiv . org/pdf/1503 . 03832 . pdf。)

每一张人脸图像都是一个特征向量,代表一个 d 维欧氏超球面,用函数| |f(x)| |2= 1来表示。

假设人脸图像{x}_i^a(主播)比不同人的{x}_i^n(硬负)人脸更接近同一个人的人脸{x}_i^p(硬正)。此外,假设在训练集中有 N 个三元组。三重损失函数由下式表示:

{\sum}_i^N\left[\ {\left\Vert\ f\left({x}_i^a\right)-f\left({x}_i^p\right)\ \right\Vert}_2²-{\left\Vert f\left({x}_i^a\right)-f\left({x}_i^n\right)\ \right\Vert}_2²+\alpha\ \right]其中 α 是正嵌入和负嵌入之间的距离余量。

如果我们考虑三元组的每一种可能的组合,将会有很多三元组,而前面的函数可能需要很长时间才能收敛。此外,并不是每个三元组都有助于模型学习。因此,我们需要一种方法来选择正确的三元组,以便我们的模型训练是有效的,并且精度是最佳的。

三联体选择

理想情况下,我们应该以这样的方式选择三元组:{\left\Vert f\left({x}_i^a\right)-f\left({x}_i^p\right)\ \right\Vert}_2²最小,{\left\Vert f\left({x}_i^a\right)-f\left({x}_i^n\right)\ \right\Vert}_2²最大。但是计算所有数据集的最小值和最大值可能是不可行的。因此,我们需要一种有效计算最小和最大距离的方法。这可以离线完成,然后馈送给算法,或者使用一些算法在线确定。

在在线方法中,我们将嵌入分成小批量。每个小批量包含一小组阳性和一些随机选择的阴性。FaceNet 的发明人使用了由 40 个阳性和随机选择的阴性嵌入组成的小批量。计算每个小批量的最小和最大距离,以创建三元组。

在接下来的部分中,我们将学习如何基于 FaceNet 训练我们自己的模型,并构建一个实时人脸识别系统。

训练人脸识别模型

FaceNet 最流行的 TensorFlow 实现之一是由大卫·桑德伯格实现的。这是一个开源版本,可以在 GitHubhttps://github.com/davidsandberg/facenet的 MIT 许可下免费获得。我们已经分叉了原始的 GitHub 库,并提交了一个稍微修改的版本到我们位于 https://github.com/ansarisam/facenet 的 GitHub 库。我们没有修改核心神经网络和三重损失函数的实现。我们修改过的 FaceNet 版本来自大卫·桑德伯格的知识库,使用 OpenCV 来读取和操作图像。我们还升级了 TensorFlow 的部分库函数。FaceNet 的这个实现需要 TensorFlow 版本 1。并且目前不能在版本 2 上运行。

在下面的例子中,我们将使用 Google Colab 来训练我们的人脸检测模型。值得注意的是,人脸检测模型是计算密集型的,可能需要几天时间来学习,即使在 GPU 上也是如此。因此,Colab 不是训练长期运行模型的理想平台,因为在 Colab 会话到期后,您将丢失所有数据和设置。您应该考虑使用基于云的 GPU 环境来训练生产质量的人脸识别模型。第十章将向您展示如何在云上扩展您的模型训练。现在,出于学习的目的,让我们使用 Colab。

在开始之前,创建一个新的 Colab 项目,并给它起一个有意义的名字,比如 FaceNet Training。

从 GitHub 查看 FaceNet

查看 FaceNet 的 TensorFlow 实现的源代码。在 Colab 中,通过单击+Code 图标添加一个代码单元格。编写命令来克隆 GitHub 库,如清单 8-1 所示。单击执行按钮运行命令。成功执行后,您应该在 Colab 文件浏览器面板中看到目录facenet

1    %%shell
2    git clone https://github.com/ansarisam/facenet.git

Listing 8-1Cloning the GitHub Repository of TensorFlow Implementation of FaceNet

资料组

我们将使用 VGGFace2 数据集来训练我们的人脸识别模型。VGGFace2 是用于人脸识别的大规模图像数据集,由视觉几何组 https://www.robots.ox.ac.uk/~vgg/data/vgg_face2/ 提供。

VGGFace2 数据集由 9000 多人的 330 万张人脸组成(简称为身份)。数据样本中每个身份有 362 张图片(平均)。该数据集在 2018 年由 Q. Cao,L. Shen,W. Xie,O. M. Parkhi 和 A. Zisserman 发表的论文 http://www.robots.ox.ac.uk/~vgg/publications/2018/Cao18/cao18.pdf 中进行了描述。

训练集的大小为 35GB,测试集的大小为 1.9GB。数据集以压缩文件的形式提供。面部图像被组织在子目录中。每个子目录的名称是格式为n< classID >的身份类 ID。图 8-4 显示了包含训练图像的样本目录结构。

img/493065_1_En_8_Fig4_HTML.jpg

图 8-4

包含图像的子目录

提供了 CSV 格式的单独元数据文件。该元数据文件的文件头如下:

身份 ID、姓名、样本号、训练/测试标志、性别

下面是一个简短的描述:

  • Identity ID映射到子目录名称。

  • name是包含人脸图像的人的名字。

  • sample number代表子目录中图片的数量。

  • train/test flag表示身份是在训练集还是测试集中。训练集由标志 1 表示,测试集为 0。

  • gender是人的性别。

需要注意的是,这个数据集的大小太大,不适合 Google Colab 或 Google Drive 的免费版本。

如果整个数据集不适合 Colab 的免费版本,您可以使用数据的一个子集(可能是几百个身份)来学习。

当然,如果你想建立一个自定义的人脸识别模型,你可以使用你自己的图像。你需要做的就是将同一个人的图像保存在一个目录中,每个人都有自己的目录,并将目录结构匹配成如图 8-4 所示。确保您的目录名和图像文件名没有任何空格。

正在下载 VGGFace2 数据

要下载图像,您需要在 http://zeus.robots.ox.ac.uk/vgg_face2/signup/ 注册。注册完成后,直接从 http://www.robots.ox.ac.uk/~vgg/data/vgg_face2/ 登录下载数据,将压缩后的训练和测试文件保存到您的本地驱动器,然后上传到 Colab。

如果您喜欢直接在 Colab 中下载图像,您可以使用清单 8-2 中的代码。使用正确的 URL 运行程序,下载训练集和测试集。

1    import sys
2    import getpass
3    import requests
4
5    VGG_FACE_URL = "http://zeus.robots.ox.ac.uk/vgg_face2/login/"
6    IMAGE_URL = "http://zeus.robots.ox.ac.uk/vgg_face2/get_file?fname=vggface2_train.tar.gz"
7    TEST_IMAGE_URL="http://zeus.robots.ox.ac.uk/vgg_face2/get_file?fname=vggface2_test.tar.gz"
8
9    print('Please enter your VGG Face 2 credentials:')
10   user_string = input('    User: ')
11   password_string = getpass.getpass(prompt='    Password: ')
12
13   credential = {
14      'username': user_string,
15      'password': password_string
16   }
17
18   session = requests.session()
19   r = session.get(VGG_FACE_URL)
20
21   if 'csrftoken' in session.cookies:
22      csrftoken = session.cookies['csrftoken']
23   elif 'csrf' in session.cookies:
24      csrftoken = session.cookies['csrf']
25   else:
26      raise ValueError("Unable to locate CSRF token.")
27
28   credential['csrfmiddlewaretoken'] = csrftoken
29
30   r = session.post(VGG_FACE_URL, data=credential)
31
32   imagefiles = IMAGE_URL.split('=')[-1]

33
34   with open(imagefiles, "wb") as files:
35      print(f"Downloading the file: `{imagefiles}`")
36      r = session.get(IMAGE_URL, data=credential, stream=True)
37      bytes_written = 0
38      for data in r.iter_content(chunk_size=400096):
39          files.write(data)
40          bytes_written += len(data)
41          MegaBytes = bytes_written / (1024 * 1024)
42          sys.stdout.write(f"\r{MegaBytes:0.2f} MiB downloaded...")
43          sys.stdout.flush()
44
45   print("\n Images are successfully downloaded. Exiting the process.")

Listing 8-2Python Code to Download VGGFace2 Images (Source: https://github.com/MistLiao/jgitlib/blob/master/download.py)

下载完训练集和测试集后,按照图 8-4 所示的结构解压缩得到训练和测试目录及其子目录。要解压缩,您可以执行清单 8-3 中的命令。

1    %%shell
2    tar xvzf vggface2_train.tar.gz
3    tar xvzf vggface2_test.tar.gz

Listing 8-3Commands to Uncompress Files

数据准备

FaceNet 的训练集应该只是脸部的图像。因此,如果需要的话,我们需要裁剪图像来提取人脸,对齐它们,并调整它们的大小。我们将使用一种称为多任务级联卷积网络 (MTCNNs)的算法,该算法已被证明在保持实时性能的同时优于许多人脸检测基准。

我们从 GitHub 存储库中克隆的 FaceNet 源代码有一个 MTCNN 的 TensorFlow 实现。这个模型的实现超出了本书的范围。我们将使用align模块中可用的 Python 程序align_dataset_mtcnn.py来获取在训练和测试集中检测到的所有人脸的边界框。该程序将保留目录结构,并将裁剪后的图像保存在相同的目录层次中,如图 8-4 所示。

清单 8-4 显示了执行面裁剪和对齐的脚本。

1    %%shell
2    %tensorflow_version 1.x
3    export PYTHONPATH=$PYTHONPATH:/content/facenet
4    export PYTHONPATH=$PYTHONPATH:/content/facenet/src
5    for N in {1..10}; do \
6    python facenet/src/align/align_dataset_mtcnn.py \
7    /content/train \
8    /content/train_aligned \
9    --image_size 182 \
10   --margin 44 \
11   --random_order \
12   --gpu_memory_fraction 0.10 \
13   & done

Listing 8-4Code for Face Detection Using MTCNN, Cropping and Alignment

在清单 8-4 中,第 1 行激活 shell,第 2 行将 TensorFlow 版本设置为 1。 x 让 Colab 知道我们不想使用版本 2,这是 Colab 中的默认版本。

第 3 行和第 4 行将环境变量PYTHONPATH设置为facenetfacenet/src目录。如果您使用的是虚拟机或物理机,并且可以直接访问操作系统,那么您应该考虑在~/.bash_profile文件中设置环境变量。

为了加速面部检测和对齐过程,我们创建了十个并行过程(第 5 行),对于每个过程,我们使用 10%的 GPU 内存(第 12 行)。如果数据集较小,并且希望在单个进程中处理 MTCNN,只需删除第 5、12 和 13 行。

第 6 行调用文件align_dataset_mtcnn.py并传递以下参数:

  • 第一个参数/content/train是训练图像所在的目录路径。

  • 第二个参数/content/train_aligned是存储对齐图像的目录路径。

  • 第三个参数--image_size,是裁剪图像的大小。我们将其设置为 182×182 像素。

  • 参数--margin设置为 44,在裁剪图像的所有四边创建一个边距。

  • 下一个参数--random_order,如果存在,将通过并行处理以随机顺序选择图像。

  • 最后一个参数--gpu_memory_fraction用于告诉算法每个并行进程使用 GPU 内存的多少部分。

在前面的脚本中,裁剪后的图像大小为 182×182 像素。Inception-ResNet-v1 的输入只有 160×160。这为随机作物提供了额外的利润。附加页边空白 44 的使用用于向模型添加任何上下文信息。额外的 44 页边空白应该根据您的具体情况进行调整,并且应该评估裁剪性能。

执行前面的脚本开始裁剪和对齐过程。请注意,这是一个计算密集型过程,可能需要几个小时才能完成。

对测试图像重复前面的过程。

模特培训

清单 8-5 用于训练具有三重损失函数的面网模型。

%tensorflow_version 1.x
!export PYTHONPATH=$PYTHONPATH:/content/facenet/src
!python facenet/src/train_tripletloss.py \
--logs_base_dir logs/facenet/ \
--models_base_dir /content/drive/'My Drive'/chapter8/facenet_model/ \
--data_dir /content/drive/'My Drive'/chapter8/train_aligned/ \
--image_size 160 \
--model_def models.inception_resnet_v1 \
--optimizer ADAGRAD \
--learning_rate 0.01 \
--weight_decay 1e-4 \
--max_nrof_epochs 10 \
--epoch_size 200

Listing 8-5Script to Train the FaceNet Model with the Triplet Loss Function

如前所述,FaceNet 的当前实现运行在 TensorFlow 版本 1 上。 x 与 TensorFlow 2 不兼容(1 号线设置版本 1。 x

第 2 行是将PYTHONPATH环境变量设置到facenet/src目录。

第 3 行使用三元组损失函数执行 FaceNet 训练。可以为训练设置许多参数,但我们将在此仅列出重要的参数。有关参数及其解释的详细列表,请查看位于facenet/src目录中的train_tripletloss.py的源代码。

为模型定型传递了以下参数:

  • --logs_base_dir:这是保存训练日志的目录。我们将 TensorBoard 连接到此目录,以使用 TensorBoard 仪表板评估模型。

  • --model_base_dir:这是存储模型检查点的基本目录。注意,我们已经提供了路径/content/drive/'My Drive'/chapter8/facenet_model/来存储 Google Drive 的模型检查点。这是为了将模型检查点永久保存到 Google Drive,避免因为 Colab 的会话终止而丢失模型。如果 Colab 会话终止,我们可以从它停止的地方重新启动模型。请注意,由于名称中有空格,所以我的驱动器用单引号括起来。

  • --data_dir:这是用于训练的对齐图像的基础目录。

  • --image_size:训练用的输入图像将根据该参数调整大小。Inception-ResNet-v1 采用 160×160 像素的输入图像尺寸。

  • --model_def:这是型号的名称。在这个例子中,我们使用了inception_resnet_v1

  • --optimizer:这是要使用的优化算法。您可以使用任何优化器ADAGRADADADELTAADAMRMSPROPMOM,默认为ADAGRAD

  • --learning_rate:我们设定学习率为 0.01。根据需要进行调整。

  • 这可以防止重量变得太大。

  • --max_nrof_epochs:训练应该运行的最大时期数。

  • --epoch_size:这是每个时期的批次数。

单击 Colab 中的 Run 按钮执行培训。根据您的训练规模和训练参数,完成模型可能需要几个小时甚至几天。

在模型被成功训练之后,检查点被保存在目录--model_base_dir中,这是我们之前在清单 8-5 ,第 5 行中配置的。

估价

当模型运行时,每个时期和每个批次的损失将打印到控制台。这应该能让你了解模型是如何学习的。理想情况下,损耗应该减少,并且应该稳定在非常低的值,接近于零。图 8-5 显示了训练进行过程中的样本输出。

img/493065_1_En_8_Fig5_HTML.jpg

图 8-5

训练过程中的 Colab 控制台输出。它显示了每批每时期的损失

您还可以使用 TensorBoard 评估模型性能。使用清单 8-6 中的命令启动 TensorBoard 仪表板。

1 %tensorflow_version 2.x
2 %load_ext tensorboard
3 %tensorboard --logdir /content/logs/facenet

Listing 8-6Launching TensorBoard by Pointing to the logs Directory

开发实时人脸识别系统

人脸识别系统需要三个重要的条件。

  • 人脸检测模型

  • 分类模型

  • 图像或视频源

人脸检测模型

在上一节中,我们学习了如何训练人脸检测模型。我们可以使用我们构建的模型,也可以使用符合我们要求的预训练模型。表 8-3 列出了公开免费提供的预训练模型。

表 8-3

大卫·桑德伯格提供的人脸识别预训练模型

|

型号名称

|

训练数据集

|

下载位置

| | --- | --- | --- | | 20180408-102900 | CASIA-WebFace | https://drive.google.com/open?id=1R77HmFADxe87GmoLwzfgMu_HY0IhcyBz | | 20180402-114759 | VGGFace2 | https://drive.google.com/open?id=1EXPBSXwTaqrSC0OhUdXNmKSh9qJUQ55- |

这些模型可在以下位置免费下载。

针对在 http://vis-www.cs.umass.edu/lfw/ 可用的野生(LFW)数据集中的标记人脸来评估模型。表 8-4 显示了模型架构和精度。

表 8-4

在 CASIA-WebFace 和 VGGFace2 数据集上训练的 FaceNet 模型的准确性评估结果(由大卫·桑德伯格提供的信息)

|

型号名称

|

LFW 准确度

|

训练数据集

|

体系结构

| | --- | --- | --- | --- | | 20180408-102900 | 0.9905 | CASIA-WebFace | Inception ResNet v1 | | 20180402-114759 | 0.9965 | VGGFace2 | Inception ResNet v1 |

对于我们的示例,我们将使用 VGGFace2 模型。

人脸识别分类器

我们将建立一个模型来识别人脸(这个人是谁)。我们将训练模型来识别乔治·w·布什、巴拉克·奥巴马和唐纳德·特朗普这三位最近的美国总统。

为了简单起见,我们将下载三位总统的一些图片,并将它们组织在子目录中,看起来如图 8-6 所示。

img/493065_1_En_8_Fig6_HTML.jpg

图 8-6

输入图像目录结构

我们将在我们的个人电脑/笔记本电脑上开发人脸检测器。在我们训练分类器之前,我们需要克隆 FaceNet GitHub 存储库。执行以下命令:

git 克隆 https://github.com/ansarisam/facenet.git

克隆 FaceNet 源代码后,将PYTHONPATH设置为facenet/src,并将其添加到环境变量中。

  • 汇出 python path = $ python path:/home/user/facenet/src

src目录的路径必须是您电脑中的实际目录路径。

面部对齐

在本节中,我们将执行图像的面部对齐。我们将使用与上一节相同的 MTCNN 模型。由于我们有一个小的图像集,我们将使用一个单一的过程来对齐这些脸。清单 8-7 显示了面部对齐的脚本。

1    python facenet/src/align/align_dataset_mtcnn.py \
2    ~/presidents/ \
3   ~/presidents_aligned \
4    --image_size 182 \
5    --margin 44

Listing 8-7Script for Face Alignment Using MTCNN

Note

在基于 Mac 的计算机上,图像目录可能有一个名为.DS_Store的隐藏文件。确保从包含输入图像的所有子目录中删除该文件。另外,确保子目录只包含图像,不包含其他文件。

执行前面的脚本来裁剪和对齐面。图 8-7 显示了一些样本输出。

img/493065_1_En_8_Fig7_HTML.png

图 8-7

三位美国总统修剪整齐的脸

分类器训练

有了这个最小的设置,我们就可以训练分类器了。清单 8-8 显示了启动分类器训练的脚本。

1    python facenet/src/classifier.py TRAIN \
2    ~/presidents_aligned \
3    ~/20180402-114759/20180402-114759.pb \
4    ~/presidents_aligned/face_classifier.pkl \
5    --batch_size 1000 \
6    --min_nrof_images_per_class 40 \
7    --nrof_train_images_per_class 35 \
8    --use_split_dataset

Listing 8-8Script to Launch the Face Classifier Training

在清单 8-8 中,第 1 行调用classifier.py并传递参数TRAIN,表示我们要训练一个分类器。该 Python 脚本的其他参数如下:

  • 包含对齐的面部图像的输入基本目录(第 2 行)。

  • 我们自己构建的或者从上一节提供的 Google Drive 链接下载的预训练人脸检测模型的路径(第 3 行)。如果您已经训练了自己的保存检查点的模型,请提供包含检查点的目录的路径。在清单 8-8 中,我们提供了冻结模型的路径(*.pb)。

  • 第 4 行是我们的分类器模型将被保存的路径。注意,这是一个扩展名为.pkl的 Pickle 文件。Pickle 是一个 Python 序列化和反序列化模块。

分类器模型成功执行后,训练好的分类器存储在清单 8-8 第 4 行提供的文件中。

视频流中的人脸识别

在清单 7-1 中,我们使用 OpenCV 的便利函数cv2.VideoCapture()从计算机的内置摄像头或 USB 或 IP 摄像头读取视频帧。VideoCapture()函数的参数 0 通常用于从内置摄像机中读取帧。在这一节中,我们将讨论如何使用 YouTube 作为我们的视频源。

为了阅读 YouTube 视频,我们将使用一个名为pafy的 Python 库,内部使用了youtube_dl库。在您的开发环境中使用 PIP 安装这些库。只需执行清单 8-9 中的命令来安装pafy

pip install pafy
pip install youtube_dl

Listing 8-9Commands to Install YouTube-Related Libraries

我们为这个练习克隆的 FaceNet 存储库在contributed模块中提供了源代码real_time_face_recognition.py,用于识别视频中的人脸。清单 8-10 展示了如何使用 Python API 从视频中检测和识别人脸。

1   python real_time_face_recognition.py \
2   --source youtube \
3   --url https://www.youtube.com/watch?v=ZYkxVbYxy-c \
4   --facenet_model_checkpoint ~/20180402-114759/20180402-114759.pb \
5   --classfier_model ~/presidents_aligned/face_classifier.pkl

Listing 8-10Script to Call Real-Time Face Recognition API

在清单 8-10 中,第 1 行调用real_time_face_recognition.py并传递以下参数:

  • 第 2 行设置参数--source的值,在本例中是youtube。如果您跳过此参数,它将默认为计算机的内置摄像头。您可以显式地传递参数webcam来从内置相机读取帧。

  • 第 3 行是传递 YouTube 视频 URL。在摄像机源的情况下,不需要这个参数。

  • 第 4 行提供了到预训练的 FaceNet 模型的路径。您可以提供检查点目录或冻结的*.pb模型的路径。

  • 第 5 行提供了我们在上一节中训练的分类器模型的文件路径,例如用于识别三位美国总统的脸的分类器模型。

当您执行清单 8-10 时,它将读取 YouTube 视频帧并显示带有边框的已识别人脸。图 8-8 显示了一个样本识别。

img/493065_1_En_8_Fig8_HTML.jpg

图 8-8

从人脸识别视频中截取的示例截图。视频的输入源是 YouTube

摘要

人脸检测是一个有趣的计算机视觉问题,涉及检测分类人脸嵌入,以识别图像中的人是谁。在这一章中,我们探讨了 FaceNet,一种基于 ResNet 的流行的人脸识别算法。我们学习了使用 MTCNN 算法来裁剪图像的面部部分的技术。我们还训练了自己的分类器,并通过一个例子对三位美国总统的面部进行了分类。最后,我们从 YouTube 上获取视频流,并实现了一个实时人脸识别系统。

九、工业应用:工业制造中的实时缺陷检测

计算机视觉在工业制造中有许多应用。一个这样的应用是用于质量控制和保证的视觉检查的自动化。

大多数制造公司培训他们的人员手动执行目视检查,这是一个手动的检查过程,可能是主观的,导致准确性取决于个别检查员的经验和意见。还应该注意到,这个过程是劳动密集型的。

如果出现机器校准问题、环境设置或设备故障,整批产品都可能出现故障。在这种情况下,事后的人工检查可能会被证明是昂贵的,因为产品可能已经生产出来,并且整批(可能数百或数千)有缺陷的产品可能需要被丢弃。

总之,手动检查过程缓慢、不准确且成本高昂。

基于计算机视觉的视觉检测系统可以通过分析视频帧流来实时检测表面缺陷。当检测到一个缺陷或一系列缺陷时,系统可以实时发送警报,以便停止生产,避免任何损失。

在这一章中,我们将开发一个基于深度学习的计算机视觉系统来检测表面缺陷,如补丁,划痕,坑洼表面和银纹。

我们将使用包含热轧钢带标记图像的数据集。我们将首先转换数据集,训练 SSD 模型,并利用该模型来构建缺陷检测器。我们还将学习如何为任何对象检测任务标记我们自己的图像。

实时表面缺陷检测系统

在本节中,我们将首先检查用于训练和测试表面缺陷检测模型的数据集。我们将把图像和注释转换成 TFRecord 文件,并在 Google Colab 上训练一个 SSD 模型。我们将应用第六章中介绍的目标检测概念。

资料组

我们将利用东北大学(NEU)的 K. Song 和 Y. Yan 提供的数据集。该数据集由六种类型的热轧钢带表面缺陷组成。这些缺陷标记如下:

  • 轧制氧化皮(RS),通常在轧制过程中将氧化皮轧制成金属时出现。

  • 补片(Pa),可以是不规则的曲面补片。

  • 银纹(Cr),即表面上的网状裂纹。

  • 麻面(PS)由许多小的浅孔组成。

  • 夹杂物(In),它是嵌入钢内部的复合材料

  • 划痕(Sc)

图 9-1 显示了带有这六种缺陷的钢表面的标记图像。

img/493065_1_En_9_Fig1_HTML.jpg

图 9-1

具有六种不同类型缺陷的表面的标记图像样本。东北大学。edu。cn/云燕/ NEU_ 表面 _ 缺陷 _ 数据库。html

数据集包括 1800 幅灰度图像,每种缺陷类别有 300 个样本。

该数据集可在 https://drive.google.com/file/d/1qrdZlaDi272eA79b0uCwwqPrm2Q_WI3k/view 免费下载,用于教育和研究目的。从这个链接下载数据集并解压缩。未压缩的数据集组织在如图 9-2 所示的目录结构中。图像在子目录IMAGES中。ANNOTATIONS子目录包含边界框注释的 XML 文件和 PASCAL VOC 注释格式的缺陷类。

img/493065_1_En_9_Fig2_HTML.jpg

图 9-2

NEU-DET 数据集目录结构

Google Colab 笔记本电脑

首先在 Google Colab 上创建一个新的笔记本,并给它起一个名字(例如,表面缺陷检测 1.0 版)。

由于 NEU 数据集位于 Google Drive 上,我们可以直接将其复制到我们的私有 Google Drive 上。在 Colab 上,我们将挂载私有的 Google Drive,解压缩数据集,并设置开发环境(清单 9-1 )。请回顾第六章以刷新您对实施的理解。

1    # Code block 1: Mount Google Drive
2    from google.colab import drive
3    drive.mount('/content/drive')
4
5    # Code block 2: uncompress NEU data
6    %%shell
7    ls /content/drive/'My Drive'/NEU-DET.zip
8    unzip /content/drive/'My Drive'/NEU-DET.zip
9
10   # Code block 3: Clone github repository of Tensorflow model project
11   !git clone https://github.com/ansarisam/models.git
12
13   # Code block 4: Install Google protobuf compiler and other dependencies
14   !sudo apt-get install protobuf-compiler python-pil python-lxml python-tk
15
16   # Code block 4: Install dependencies
17   %%shell
18   cd models/research
19   pwd
20   protoc object_detection/protos/*.proto --python_out=.
21   pip install --user Cython
22   pip install --user contextlib2
23   pip install --user pillow
24   pip install --user lxml
25   pip install --user jupyter
26   pip install --user matplotlib
27
28   # Code block 5: Build models project

29   %%shell
30   export PYTHONPATH=$PYTHONPATH:/content/models/research:/content/models/research/slim
31   cd /content/models/research
32   python setup.py build
33   python setup.py install

Listing 9-1Mounting Google Drive, Downloading, Building, and Installing TensorFlow Models

数据转换

我们将把 NEU 数据集转换成 TFRecord 格式(查看第六章的 SSD 模型训练部分)。清单 9-2 是基于 TensorFlow 的代码,用于将图像和注释转换成 TFRecord。

File name: generic_xml_to_tf_record.py
1    from __future__ import absolute_import
2    from __future__ import division
3    from __future__ import print_function
4
5    import hashlib
6    import io
7    import logging
8    import os
9
10   from lxml import etree
11   import PIL.Image
12   import tensorflow as tf
13
14   from object_detection.utils import dataset_util
15   from object_detection.utils import label_map_util
16   import random
17
18   flags = tf.app.flags
19   flags.DEFINE_string('data_dir', '', 'Root directory to raw PASCAL VOC dataset.')
20
21   flags.DEFINE_string('annotations_dir', 'annotations',
22                     '(Relative) path to annotations directory.')
23   flags.DEFINE_string('image_dir', 'images',
24                     '(Relative) path to images directory.')
25
26   flags.DEFINE_string('output_path', '', 'Path to output TFRecord')
27   flags.DEFINE_string('label_map_path', 'data/pascal_label_map.pbtxt',
28                     'Path to label map proto')
29   flags.DEFINE_boolean('ignore_difficult_instances', False, 'Whether to ignore '
30                      'difficult instances')
31   FLAGS = flags.FLAGS
32
33   # This function generates a list of images for training and validation.
34   def create_trainval_list(data_dir):
35     trainval_filename = os.path.abspath(os.path.join(data_dir,"trainval.txt"))
36     trainval = open(os.path.abspath(trainval_filename), "w")
37     files = os.listdir(os.path.join(data_dir, FLAGS.image_dir))
38     for f in files:
39         absfile =os.path.abspath(os.path.join(data_dir, FLAGS.image_dir, f))
40         trainval.write(absfile+"\n")
41         print(absfile)
42     trainval.close()
43
44
45   def dict_to_tf_example(data,
46                        dataset_directory,
47                        label_map_dict,
48                        ignore_difficult_instances=False,
49                        image_subdirectory=FLAGS.image_dir):
50   """Convert XML derived dict to tf.Example proto.
51
52   Notice that this function normalizes the bounding box coordinates provided
53   by the raw data.
54
55   Args:
56     data: dict holding PASCAL XML fields for a single image
57     dataset_directory: Path to root directory holding PASCAL dataset
58     label_map_dict: A map from string label names to integers ids.
59     ignore_difficult_instances: Whether to skip difficult instances in the
60       dataset  (default: False).
61     image_subdirectory: String specifying subdirectory within the
62       PASCAL dataset directory holding the actual image data.
63
64   Returns:
65     example: The converted tf.Example.
66
67   Raises:
68     ValueError: if the image pointed to by data['filename'] is not a valid JPEG
69   """
70   filename = data['filename']
71
72   if filename.find(".jpg") < 0:
73       filename = filename+".jpg"
74   img_path = os.path.join("",image_subdirectory, filename)
75   full_path = os.path.join(dataset_directory, img_path)
76
77   with tf.gfile.GFile(full_path, 'rb') as fid:
78     encoded_jpg = fid.read()
79   encoded_jpg_io = io.BytesIO(encoded_jpg)
80   image = PIL.Image.open(encoded_jpg_io)
81   if image.format != 'JPEG':
82     raise ValueError('Image format not JPEG')
83   key = hashlib.sha256(encoded_jpg).hexdigest()
84
85   width = int(data['size']['width'])
86   height = int(data['size']['height'])
87
88   xmin = []
89   ymin = []
90   xmax = []
91   ymax = []
92   classes = []
93   classes_text = []
94   truncated = []
95   poses = []
96   difficult_obj = []
97   if 'object' in data:
98     for obj in data['object']:
99       difficult = bool(int(obj['difficult']))
100      if ignore_difficult_instances and difficult:
101        continue
102
103      difficult_obj.append(int(difficult))
104
105      xmin.append(float(obj['bndbox']['xmin']) / width)
106      ymin.append(float(obj['bndbox']['ymin']) / height)
107      xmax.append(float(obj['bndbox']['xmax']) / width)
108      ymax.append(float(obj['bndbox']['ymax']) / height)
109      classes_text.append(obj['name'].encode('utf8'))
110      classes.append(label_map_dict[obj['name']])
111      truncated.append(int(obj['truncated']))
112      poses.append(obj['pose'].encode('utf8'))
113
114  example = tf.train.Example(features=tf.train.Features(feature={
115      'img/height': dataset_util.int64_feature(height),
116      'img/width': dataset_util.int64_feature(width),
117      'img/filename': dataset_util.bytes_feature(
118          data['filename'].encode('utf8')),
119      'img/source_id': dataset_util.bytes_feature(
120          data['filename'].encode('utf8')),
121      'img/sha256': dataset_util.bytes_feature(key.encode('utf8')),
122      'img/encoded': dataset_util.bytes_feature(encoded_jpg),
123      'img/format': dataset_util.bytes_feature('jpeg'.encode('utf8')),
124      'img/xmin': dataset_util.float_list_feature(xmin),
125      'img/xmax': dataset_util.float_list_feature(xmax),
126      'img/ymin': dataset_util.float_list_feature(ymin),
127      'img/ymax': dataset_util.float_list_feature(ymax),
128      'img/text': dataset_util.bytes_list_feature(classes_text),
129      'img/label': dataset_util.int64_list_feature(classes),
130      'img/difficult': dataset_util.int64_list_feature(difficult_obj),
131      'img/truncated': dataset_util.int64_list_feature(truncated),
132      'img/view': dataset_util.bytes_list_feature(poses),
133  }))
134  return example
135
136  def create_tf(examples_list, annotations_dir, label_map_dict, dataset_type):
137    writer = None
138    if not os.path.exists(FLAGS.output_path+"/"+dataset_type):
139        os.mkdir(FLAGS.output_path+"/"+dataset_type)
140
141    j = 0
142    for idx, example in enumerate(examples_list):
143
144        if idx % 100 == 0:
145            logging.info('On image %d of %d', idx, len(examples_list))
146            print((FLAGS.output_path + "/tf_training_" + str(j) + ".record"))
147            writer = tf.python_io.TFRecordWriter(FLAGS.output_path + "/"+dataset_type+"/tf_training_" + str(j) + ".record")
148            j = j + 1
149
150        path = os.path.join(annotations_dir, os.path.basename(example).replace(".jpg", '.xml'))
151
152        with tf.gfile.GFile(path, 'r') as fid:
153            xml_str = fid.read()
154        xml = etree.fromstring(xml_str)
155        data = dataset_util.recursive_parse_xml_to_dict(xml)['annotation']
156
157        tf_example = dict_to_tf_example(data, FLAGS.data_dir, label_map_dict,
158                                    FLAGS.ignore_difficult_instances)
159        writer.write(tf_example.SerializeToString())
160
161  def main(_):
162
163    data_dir = FLAGS.data_dir
164    create_trainval_list(data_dir)
165
166    label_map_dict = label_map_util.get_label_map_dict(FLAGS.label_map_path)
167
168    examples_path = os.path.join(data_dir,'trainval.txt')
169    annotations_dir = os.path.join(data_dir, FLAGS.annotations_dir)
170    examples_list = dataset_util.read_examples_list(examples_path)
171
172    random.seed(42)
173    random.shuffle(examples_list)
174    num_examples = len(examples_list)
175    num_train = int(0.7 * num_examples)
176    train_examples = examples_list[:num_train]
177    val_examples = examples_list[num_train:]
178
179    create_tf(train_examples, annotations_dir, label_map_dict, "train")
180    create_tf(val_examples, annotations_dir, label_map_dict, "val")
181
182  if __name__ == '__main__':
183    tf.app.run()
184

Listing 9-2Transforming Images and Annotations in PASCAL VOC Format into TFRecord

清单 9-2 执行以下操作:

  1. 首先,调用函数create_trainval_list()创建一个文本文件,其中包含来自IMAGES子目录的所有图像的绝对路径列表。

  2. 将图像路径列表拆分为 70:30 的比例,以便为训练集和验证集生成单独的图像列表。

  3. 对于训练集中的每个图像,使用函数dict_to_tf_example()创建一个 TFRecord。TFRecord 包含图像的字节、边界框、带注释的类名和其他几个关于图像的元数据。TFRecord 被序列化并写入文件。创建多个 TFRecord 文件,文件数量取决于图像总数和每个 TFRecord 文件中包含的图像数量。

  4. 类似地,为每个验证图像创建 TFRecords 并序列化到文件中。

  5. 训练集和验证集保存在output目录下的两个独立的子目录中——分别是trainval

如果您克隆清单 9-1 中提到的 GitHub 库,Python 文件generic_xml_to_tf_record.py已经包含在内了。但是如果您克隆官方 TensorFlow 模型的存储库,那么您需要将清单 9-2 中的代码保存到generic_xml_to_tf_record.py中,并将其上传到您的 Colab 环境中(例如,上传到/content目录中)。

我们需要一个映射文件来映射类索引和类名。该文件包含 JSON 内容,通常具有扩展名.pbtxt。我们有六个缺陷类,我们可以手动编写标签映射文件,如下所示:

File name: steel_label_map.pbtxt
item {
  id: 1
  name: 'rolled-in_scale'
}

item {
  id: 2
  name: 'patches'
}

item {
  id: 3
  name: 'crazing'
}

item {
  id: 4
  name: 'pitted_surface'
}

item {
  id: 5
  name: 'inclusion'
}

item {
  id: 6
  name: 'scratches'
}

steel_label_map.pbtxt文件上传到您的 Colab 环境中的/content目录(或者您想要的任何其他目录,只要您在清单 9-3 中提供正确的路径)。

清单 9-3 中的脚本通过提供以下参数来执行generic_xml_to_tf_record.py:

  • --label_map_path:到steel_label_map.pbtxt的路径。

  • --data_dir:图像和注释目录所在的根目录。

  • --output_path:保存生成的 TFRecord 文件的路径。请确保该目录存在。如果没有,请在执行该脚本之前创建该目录。

  • --annotations_dir:标注 XML 文件所在的子目录名。

  • --image_dir:图像所在的子目录名。

1    %%shell
2    %tensorflow_version 1.x
3
4    python /content/generic_xml_to_tf_record.py \
5       --label_map_path=/content/steel_label_map.pbtxt \
6       --data_dir=/content/NEU-DET \
7       --output_path=/content/NEU-DET/out \
8       --annotations_dir=ANNOTATIONS \
9       --image_dir=IMAGES

Listing 9-3Executing generic_xml_to_tf_record.py That Creates TFRecord Files

运行清单 9-3 中的脚本,在输出目录中创建 TFRecord 文件。您将看到两个子目录——trainval——保存用于训练和验证的 TFRecords。

请注意,输出目录必须存在。否则,在执行清单 9-3 中的代码之前创建一个。

培训 SSD 模型

我们现在已经准备好了 TFRecord 格式的正确输入集来训练我们的 SSD 模型。培训步骤与我们在第六章中遵循的步骤完全相同。首先,根据我们之前创建的训练和验证集,下载一个预训练的 SSD 模型进行迁移学习。

清单 9-4 显示了我们在第六章中使用的相同代码(清单 6-5)。

1    %%shell
2    %tensorflow_version 1.x
3    mkdir pre-trained-model
4    cd pre-trained-model
5    wget http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2018_01_28.tar.gz
6    tar -xvf ssd_inception_v2_coco_2018_01_28.tar.gz

Listing 9-4Downloading a Pre-trained Object Detection Model

我们现在将编辑pipeline.config文件,如第六章“配置对象检测流水线”一节所述。清单 9-5 显示了根据当前配置编辑的pipeline.config文件的各个部分。

model {
  ssd {
    num_classes: 6
    image_resizer {
      fixed_shape_resizer {
        height: 300
        width: 300
      }
    }
   ......
        batch_norm {
          decay: 0.999700009823
          center: true
          scale: true
          epsilon: 0.0010000000475
          train: true
        }
      }
            override_base_feature_extractor_hyperparams: true
    }
    .....
    matcher {
      argmax_matcher {
        matched_threshold: 0.5
        unmatched_threshold: 0.5
        ignore_thresholds: false
        negatives_lower_than_unmatched: true
        force_match_for_each_row: true
      }
    }
   ......

  fine_tune_checkpoint: "/content/pre-trained-model/ssd_inception_v2_coco_2018_01_28/model.ckpt"
  from_detection_checkpoint: true
  num_steps: 100000

}
train_input_reader {
  label_map_path: "/content/steel_label_map.pbtxt"
  tf_record_input_reader {
    input_path: "/content/NEU-DET/out/train/*.record"
  }
}
eval_config {
  num_examples: 8000
  max_evals: 10
  use_moving_averages: false
}
eval_input_reader {
  label_map_path: "/content/steel_label_map.pbtxt"
  shuffle: false
  num_readers: 1
  tf_record_input_reader {
    input_path: "/content/NEU-DET/out/val/*.record"
  }
}

Listing 9-5Section of pipeline.config That Must to Be Edited to Point to the Appropriate Directory Structure

如清单 9-5 所示,我们必须编辑清单 9-5 中用黄色突出显示的部分。

num_classes: 6
fine_tune_checkpoint: path to pre-trained model checkpoint
label_map_path: path to .pbtxt file
input_path: path to the training TFRecord files.
label_map_path: path to the .pbtxt file
input_path: path to the validation TFRecord files.

编辑pipeline.config文件并上传到 Colab 环境。

使用清单 9-6 中所示的脚本执行模型训练。回顾第六章中的清单 6-6 来更新概念。

1    %%shell
2    %tensorflow_version 1.x
3    export PYTHONPATH=$PYTHONPATH:/content/models/research:/content/models/research/slim
4    cd models/research/
5    PIPELINE_CONFIG_PATH=/content/pre-trained-model/ssd_inception_v2_coco_2018_01_28/steel_defect_pipeline.config
6    MODEL_DIR=/content/neu-det-models/
7    NUM_TRAIN_STEPS=10000
8    SAMPLE_1_OF_N_EVAL_EXAMPLES=1
9    python object_detection/model_main.py \
10      --pipeline_config_path=${PIPELINE_CONFIG_PATH} \
11      --model_dir=${MODEL_DIR} \
12      --num_train_steps=${NUM_TRAIN_STEPS} \
13      --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \
14      --alsologtostderr

Listing 9-6Executing the Model Training

当模型学习时,日志被打印在 Colab 控制台上。记下每个时期的损失,并根据需要调整模型的超参数。

导出模型

训练成功完成后,检查点保存在清单 9-6 第 6 行指定的目录中。

为了利用该模型进行实时检测,我们需要导出 TensorFlow 图。查看第六章的“导出 TensorFlow 图”部分,了解详细信息。

清单 9-7 展示了如何导出我们刚刚训练的 SSD 模型。

1    %%shell
2    %tensorflow_version 1.x
3    export PYTHONPATH=$PYTHONPATH:/content/models/research
4    export PYTHONPATH=$PYTHONPATH:/content/models/research/slim
5    cd /content/models/research
6
7    python object_detection/export_inference_graph.py \
8       --input_type image_tensor \
9       --pipeline_config_path /content/pre-trained-model/ssd_inception_v2_coco_2018_01_28/steel_defect_pipeline.config \
10      --trained_checkpoint_prefix /content/neu-det-models/model.ckpt-10000 \
11      --output_directory /content/NEU-DET/final_model

Listing 9-7Exporting the Model to the TensorFlow Graph

导出模型后,应该保存到 Google Drive。从 Google Drive 下载最终模型到你的本地电脑。我们可以使用这个模型从视频帧中实时检测表面缺陷。回顾第七章中介绍的概念。

模型评估

启动 TensorBoard 仪表板以评估模型质量。清单 9-8 展示了如何启动 TensorBoard 仪表板。

1    %tensorflow_version 2.x
2    %load_ext tensorboard
3    %tensorboard --logdir /drive/'My Drive'/NEU-DET-models/

Listing 9-8Launching the TensorBoard Dashboard

图 9-3 显示了 TensorBoard 的样本训练输出。

img/493065_1_En_9_Fig3_HTML.jpg

图 9-3

表面缺陷检测模型训练的张量板输出显示

预报

如果您已经按照第六章“使用训练模型检测物体”一节所述设置了工作环境,那么您应该已经具备了预测图像中表面缺陷所需的一切。只需更改清单 6-15 中的变量,并执行清单 9-9 中所示的 Python 代码。

model_path = "/Users/sansari/Downloads/neu-det-models/final_model"

labels_path = "/Users/sansari/Downloads/steel_label_map.pbtxt"

image_dir = "/Users/sansari/Downloads/NEU-DET/test/IMAGES"

image_file_pattern = "*.jpg"

output_path="/Users/sansari/Downloads/surface_defects_out"

Listing 9-9Variable Initialization Portion of Code from Listing 6-15

图 9-4 显示了不同类别缺陷预测的一些样本输出。

img/493065_1_En_9_Fig4_HTML.png

图 9-4

具有包围盒的缺陷表面的样本预测输出

实时缺陷检测器

遵循第七章中提供的说明,部署检测系统,该系统将从摄像机读取视频图像并实时检测表面缺陷。如果有多台摄像机连接到同一个设备,请为函数cv2.VideoCapture(x)中的参数x使用适当的值。默认情况下,x=0从电脑的内置摄像头读取视频。x=1x=2等的值。,将读取连接到计算机端口的视频。对于基于 IP 的摄像机,x的值应该是 IP 地址。

图像注释

在前面的所有例子中,我们使用了已经被标注和标记的图像。在本节中,我们将探讨如何为对象检测或人脸识别的图像添加注释。

有几个用于图像标注的开源和商业工具。我们将探索微软视觉对象标记工具(VoTT),这是一个用于图像和视频资产的开源注释和标记工具。VoTT 的源代码可以在 https://github.com/microsoft/VoTT 获得。

安装 VoTT

vott 需要 node.js 和 npm

要安装 NodeJS,请从官方网站 https://nodejs.org/en/download/ 下载您的操作系统的可执行二进制文件。比如下载安装 Windows Installer ( .msi)在 Windows OS 上安装 NodeJS,下载安装 macOS Installer ( .pkg)在 Mac 上安装,或者选择 Linux 二进制(x64)用于 Linux。

NPM 安装了 NodeJS。要检查您的计算机上是否安装了 NodeJS 和 NPM,请在您的终端窗口中执行以下命令:

node -v
npm -v

不同操作系统的 VoTT 安装程序在 GitHub ( https://github.com/Microsoft/VoTT/releases )维护。为您的操作系统下载安装程序。在撰写本书时,最新的 VoTT 版本是 2.1.1,可以从以下位置下载:

通过运行下载的可执行文件在您的计算机上安装 VoTT。

要从源运行 VoTT,请在终端上执行以下命令:

git clone https://github.com/Microsoft/VoTT.git
 cd VoTT
 npm ci
 npm start

npm start命令运行 VoTT 将启动电子版和浏览器版。两个版本的主要区别在于浏览器版本不能访问本地文件系统,而电子版本可以。

由于我们的图像在本地文件系统中,我们将探索 VoTT 的电子版本。

当您启动 VoTT 用户界面时,您将看到主屏幕,可以创建一个新项目,打开一个本地项目,或者打开一个云项目。

要注释图像,我们将按照下一节中的步骤进行。

创建连接

我们将创建两个连接:一个用于输入,另一个用于输出。

输入连接到存储未标记图像的目录。

输出连接是存储注释的地方。

目前,VoTT 支持连接到以下设备:

  • Azure Blob 存储

  • 必应图片搜索

  • 本地文件系统

我们将创建一个到本地文件系统的连接。要创建新连接,请单击左侧导航栏中的新建连接图标,以启动连接屏幕。单击左上角面板中与标签“连接”对应的加号图标。参见图 9-5 。

img/493065_1_En_9_Fig5_HTML.jpg

图 9-5

创建新连接

为提供者字段选择本地文件系统。单击选择文件夹打开本地文件系统目录结构。选择包含需要标记的输入图像的目录。单击保存连接按钮。

类似地,创建另一个连接来存储输出。

创建新项目

图像注释和标记的任务在一个项目下管理。要创建项目,请单击主页图标,然后单击新建项目以打开项目设置页面。参见图 9-6 。

img/493065_1_En_9_Fig6_HTML.jpg

图 9-6

“项目设置”页来创建新项目

“项目设置”页面上的两个重要字段是源连接和目标连接。为输入和输出目录选择我们在上一步中创建的适当连接。单击保存项目按钮。

创建类别标签

保存项目设置后,屏幕切换到主标签页面。要创建类别标签,请单击位于右侧面板右上角的标签标签对应的(+)图标(如图 9-7 所示)。创建所有的类别标签,如网纹、补丁、包含等。

img/493065_1_En_9_Fig7_HTML.jpg

图 9-7

创建分类标签

给图像贴标签

从左侧面板中选择一个图像缩略图,该图像将在主标记区域中打开。在图像的缺陷区域周围绘制矩形或多边形,并选择适当的标签来注释图像。参见图 9-8 。

img/493065_1_En_9_Fig8_HTML.jpg

图 9-8

在缺陷区域周围绘制矩形并选择类别标签来注释图像

同样,逐个注释所有图像。

出口标签

VoTT 支持以下导出格式:

  • Azure 自定义视觉服务

  • 微软认知工具包(CNTK)

  • TensorFlow(帕斯卡 VOC 和 TFRecords)

  • 通用 JSON 模式

  • 逗号分隔值(CSV)

我们将配置设置,以 TensorFlow TFRecord 文件格式导出我们的注释。

要进行配置,请单击左侧导航栏中的导出图标。导出图标看起来像一个向上倾斜的箭头。将打开“导出设置”页面。对于提供者字段,选择 TensorFlow 记录并点击保存导出设置按钮(图 9-9 )。

img/493065_1_En_9_Fig9_HTML.jpg

图 9-9

导出设置页面

返回到项目页面(单击标记编辑器图标)。点击顶部工具栏中的img/493065_1_En_9_Figb_HTML.gif图标,将注释导出到 TensorFlow 记录文件。

检查本地文件系统的输出文件夹。您会注意到,在输出目录中已经创建了一个名为包含TFRecords-export的目录。

导出到 TFRecord 格式还会生成一个包含类和索引映射的tf_label_map.pbtxt文件。

有关图像标签的最新信息和说明,请访问由微软维护的 VoTT 项目的官方 GitHub 页面: https://github.com/microsoft/VoTT

摘要

在这一章中,我们开发了一个表面缺陷检测系统。我们在具有六类缺陷的热轧钢带的已标记图像集上训练 SSD 模型。我们使用训练好的模型来预测图像和视频中的表面缺陷。我们还探索了一个名为 VoTT 的图像注释工具,它可以帮助注释图像并将标签导出为 TFRecord 格式。