Python OpenCV
聚焦Python和OpenCV的图像处理,3D场景重建,对象检测和跟踪
除了安装OpenCV,您还需要安装TensorFlow。
可以从此链接下载Oxford-IIIT-Pet数据集,以及我们的数据集准备脚本,该脚本将自动为您下载。
最终的应用将由模块组成,这些模块用于准备数据集,训练模型以及使用相机输入来推断模型。 这将需要以下组件:
在准备好数据集进行训练之后,我们将执行以下操作以完成我们的应用程序:
让我们从学习如何准备将运行我们的应用程序的推理脚本开始。 该脚本将连接到您的摄像机,并使用我们将创建的定位本地化模型在视频流的每一帧中找到头部位置,然后实时显示结果。
我们的推理脚本非常简单。 它将首先准备绘图功能,然后加载模型并将其连接到相机。 然后,它将循环播放视频流中的帧。 在循环中,对于流的每一帧,它将使用导入的模型进行推理,并使用绘图功能显示结果。 让我们使用以下步骤创建一个完整的脚本:
首先,导入所需模块:
import numpy as np
import cv2
import tensorflow.keras as K
在这段代码中,除了导入NumPy和OpenCV,我们还导入了Keras。 我们将使用Keras在此脚本中进行预测; 此外,我们将使用它来创建和训练我们的模型。
然后,我们定义一个函数以在框架上绘制定位边界框:
def draw_box(frame: np.ndarray, box: np.ndarray) -> np.ndarray:
h, w = frame.shape[0:2]
pts = (box.reshape((2, 2)) * np.array([w, h])).astype(np.int)
cv2.rectangle(frame, tuple(pts[0]), tuple(pts[1]), (0, 255, 0),
2)
return frame
前面的draw_box函数将边框和边界框的两个角的标准化坐标接受为四个数字的数组。 该函数首先将盒子的一维数组整形为二维数组,其中第一个索引表示点,第二个索引表示x和y坐标。 然后,它通过将标准化坐标与图像宽度和高度组成的数组相乘,将标准化坐标转换为图像坐标,并将结果转换为同一行中的整数值。 最后,它使用cv2.rectangle函数绘制绿色边框,并返回帧。
然后,导入准备的模型并连接到摄像机:
model = K.models.load_model("localization.h5")
cap = cv2.VideoCapture(0)
模型将存储在一个二进制文件中,该文件是使用Keras的便捷功能导入的。
之后,我们遍历相机中的帧,将每个帧调整为标准尺寸(即我们将创建的模型的默认图像尺寸),然后将帧转换为RGB(红色,绿色,蓝色)颜色 空间,因为我们将在RGB图像上训练模型:
for _, frame in iter(cap.read, (False, None)):
input = cv2.resize(frame, (224, 224))
input = cv2.cvtColor(input, cv2.COLOR_BGR2RGB)
在同一循环中,我们对图像进行归一化,并在模型接受批次图像时将其添加到框架的形状中。 然后,将结果传递给模型以进行推断:
box, = model.predict(input[None] / 255)
我们通过使用先前定义的函数绘制预测的边界框来继续循环,显示结果,然后设置终止条件:
cv2.imshow("res", frame)
if(cv2.waitKey(1) == 27):
break
我们将使用Oxford-IIIT-Pet数据集。 将数据集的准备工作封装在单独的data.py脚本中是个好主意,然后可以在本文中使用它。 与其他任何脚本一样,首先,我们必须导入所有必需的模块,如以下代码片段所示:
import glob
import os
from itertools import count
from collections import defaultdict, namedtuple
import cv2
import numpy as np
import tensorflow as tf
import xml.etree.ElementTree as ET
为了准备我们的数据集以供使用,我们将首先下载数据集并将其解析到内存中。 然后,从解析的数据中,我们将创建一个TensorFlow数据集,该数据集使我们能够以方便的方式使用数据集以及在后台准备数据,以便数据的准备不会中断神经网络训练处理。
首先,我们首先从官方网站下载数据集,然后将其解析为方便的格式。 在此阶段,我们将省去占用大量内存的图像。 我们通过以下步骤介绍此过程:
定义我们要存储宠物数据集的位置,并在Keras中使用便捷的get_file函数下载它:
DATASET_DIR = "dataset"
for type in ("annotations", "images"):
tf.keras.utils.get_file(
type,
f"https://www.robots.ox.ac.uk/~vgg/data/pets/data/{type}.tar.gz",
untar=True,
cache_dir=".",
cache_subdir=DATASET_DIR)
由于我们的数据集位于存档中,因此我们还通过传递untar = True提取了它。 我们还将cache_dir指向当前目录。 保存文件后,随后执行get_file函数将不执行任何操作。
数据集的重量超过半GB,并且在第一次运行时,您将需要具有良好带宽的稳定Internet连接。
下载并提取数据集后,让我们为数据集和批注文件夹定义常量,并将要调整图像大小的图像大小设置为:
IMAGE_SIZE = 224
IMAGE_ROOT = os.path.join(DATASET_DIR,"images")
XML_ROOT = os.path.join(DATASET_DIR,"annotations")
224通常是在其上训练图像分类网络的默认大小。 因此,最好保持该大小以提高准确性。
该数据集的注释包含有关XML格式图像的信息。 在解析XML之前,让我们首先定义我们想要拥有的数据:
Data = namedtuple("Data","image,box,size,type,breed")
namedtuple是Python中标准元组的扩展,并允许您通过其名称引用元组的元素。 我们定义的名称与我们感兴趣的数据元素相对应。即,图像本身(图像),宠物的头部边框(框),图像大小,类型(猫或狗)和品种(有37个品种)。
品种和类型是注释中的字符串; 我们想要的是与品种相对应的数字。 为此,我们定义了两个字典:
types = defaultdict(count().__next__ )
breeds = defaultdict(count().__next__ )
defaultdict是一个字典,它返回未定义键的默认值。 在这里,它将根据要求返回从零开始的下一个数字。
接下来,我们定义一个函数,给定XML文件的路径,该函数将返回我们的数据实例:
def parse_xml(path: str) -> Data:
先前定义的功能包括以下步骤:
打开XML文件并进行解析:
with open(path) as f:
xml_string = f.read()
root = ET.fromstring(xml_string)
使用ElementTree模块解析XML文件的内容,该模块以易于浏览的格式表示XML。
然后,获取相应图像的名称并提取品种的名称:
img_name = root.find("./filename").text
breed_name = img_name[:img_name.rindex("_")]
之后,使用先前定义的breeds将品种转换为数字,为每个未定义的键分配下一个数字:
breed_id = breeds[breed_name]
同样,获取类型的ID:
type_id = types[root.find("./object/name").text]
然后,提取边界框并将其标准化:
box =np.array([int(root.find(f"./object/bndbox/{tag}").text)
for tag in "xmin,ymin,xmax,ymax".split(",")])
size = np.array([int(root.find(f"./size/{tag}").text)
for tag in "width,height".split(",")])
normed_box = (box.reshape((2, 2)) / size).reshape((4))
返回Data的实例:
return Data(img_name,normed_box,size,type_id,breed_id)
现在我们已经下载了数据集并准备了一个解析器,让我们继续解析数据集:
xml_paths = glob.glob(os.path.join(XML_ROOT,"xmls","*.xml"))
xml_paths.sort()
parsed = np.array([parse_xml(path) for path in xml_paths])
我们还对路径进行了排序,以便它们在不同的运行时环境中以相同的顺序出现。
解析完数据集后,我们可能希望打印出可用的品种和类型进行说明:
print(f"{len(types)} TYPES:", *types.keys(), sep=", ")
print(f"{len(breeds)} BREEDS:", *breeds.keys(), sep=", ")
先前的代码段输出两种类型,即猫和狗,以及它们的品种:
2 TYPES:, cat, dog
37 BREEDS:, Abyssinian, Bengal, Birman, Bombay, British_Shorthair,
Egyptian_Mau, Maine_Coon, Persian, Ragdoll, Russian_Blue, Siamese, Sphynx,
american_bulldog, american_pit_bull_terrier, basset_hound, beagle, boxer,
chihuahua, english_cocker_spaniel, english_setter, german_shorthaired,
great_pyrenees, havanese, japanese_chin, keeshond, leonberger,
miniature_pinscher, newfoundland, pomeranian, pug, saint_bernard, samoyed,
scottish_terrier, shiba_inu, staffordshire_bull_terrier, wheaten_terrier,
yorkshire_terrier
我们将不得不在训练集和测试集上拆分数据集。 为了进行良好的分割,我们应该从数据集中随机选择数据元素,以便在训练和测试集中按比例分配品种。
现在,我们可以混合数据集,这样我们以后就不必担心它了,如下所示:
np.random.seed(1)
np.random.shuffle(parsed)
先前的代码首先设置一个随机种子,每次执行代码时都需要获得相同的结果。 seed方法接受一个参数,该参数是指定随机序列的数字。
设置种子方法后,使用随机数的函数中将具有相同的随机数序列。 这样的数字称为伪随机数。 这意味着,尽管它们看起来是随机的,但它们是预定义的。 在我们的例子中,我们使用shuffle方法,该方法混合了解析数组中元素的顺序。
现在我们已经将数据集解析为方便的NumPy数组,让我们继续并从中创建TensorFlow数据集。
我们将使用TensorFlow数据集适配器来训练我们的模型。 当然,我们可以从数据集中创建一个NumPy数组,但可以想象将所有图像保留在内存中需要多少内存。
相反,数据集适配器可让您在需要时将数据加载到内存中。 而且,数据是在后台加载和准备的,因此不会成为我们培训过程中的瓶颈。 我们将解析后的数组转换如下:
ds = tuple(np.array(list(i)) for i in np.transpose(parsed))
ds_slices = tf.data.Dataset.from_tensor_slices(ds)
从前面的代码段中,from_tensor_slices创建了Dataset,其元素是给定tensor的切片。 在我们的例子中,tensor是标签的NumPy数组(盒子,品种,图像位置等)。
在后台,它与Python zip函数的概念相似。 首先,我们准备了相应的输入。 现在让我们从数据集中打印一个元素以查看其外观:
for el in ds_slices.take(1):
print(el)
这给出以下输出:
(<tf.Tensor: id=14, shape=(), dtype=string,
numpy=b'american_pit_bull_terrier_157.jpg'>, <tf.Tensor: id=15, shape=(4,),
dtype=float64, numpy=array([0.07490637, 0.07 , 0.58426966, 0.44333333])>,
<tf.Tensor: id=16, shape=(2,), dtype=int64, numpy=array([267, 300])>,
<tf.Tensor: id=17, shape=(), dtype=int64, numpy=1>, <tf.Tensor: id=18,
shape=(), dtype=int64, numpy=13>)
TensorFlow-tensor包含我们已经从单个XML文件中解析的所有信息。 给定数据集,我们可以检查所有边界框是否正确:
for el in ds_slices:
b = el[1].numpy()
if(np.any((b>1) |(b<0)) or np.any(b[2:]-b[:2] < 0)):
print(f"Invalid box found {b} image: {el[0].numpy()}")
当我们将框标准化后,它们应该在[0,1]的范围内。 此外,我们确保框的第一个点的坐标小于或等于第二个点的坐标。
现在,我们定义一个函数,该函数将转换我们的数据元素,以便可以将其输入到神经网络中:
def prepare(image,box,size,type,breed):
image = tf.io.read_file(IMAGE_ROOT+"/"+image)
image = tf.image.decode_png(image,channels=3)
image = tf.image.resize(image,(IMAGE_SIZE,IMAGE_SIZE))
image /= 255
return
Data(image,box,size,tf.one_hot(type,len(types)),tf.one_hot(breed,len(breeds
)))
详情参阅http://viadean.com/opencv_python_object.html