ML | Неконтролируемый конвейер кластеризации лиц

Опубликовано: 4 Января, 2022

Распознавание лиц в реальном времени - проблема, с которой до сих пор сталкиваются автоматизированные подразделения безопасности. Благодаря достижениям в области сверточных нейронных сетей и особенно творческим способам использования Region-CNN, уже подтверждено, что с нашими текущими технологиями мы можем выбрать варианты контролируемого обучения, такие как FaceNet, YOLO, для быстрого и живого распознавания лиц в реальной среде. .
Чтобы обучить контролируемую модель, нам нужно получить наборы данных наших целевых меток, что по-прежнему является утомительной задачей. Нам нужно эффективное и автоматизированное решение для создания наборов данных с минимальными усилиями по маркировке при вмешательстве пользователя.

Предложенное решение -

Введение: мы предлагаем конвейер генерации наборов данных, который берет видеоклип в качестве источника, извлекает все лица и группирует их в ограниченные и точные наборы изображений, представляющих отдельного человека. Каждый набор можно легко пометить с помощью человеческого ввода.

Технические детали: мы собираемся использовать opencv lib для посекундного извлечения кадров из входного видеоклипа. 1 секунда кажется подходящей для покрытия соответствующих данных и ограниченных рамок для обработки.
Мы будем использовать face_recognition (поддерживаемую dlib ) для извлечения лиц из кадров и выравнивания их для извлечения функций.
Затем мы извлечем наблюдаемые человеком функции и сгруппируем их с помощью кластеризации DBSCAN, предоставляемой scikit-learn .
Для решения мы вырежем все лица, создадим метки и сгруппируем их в папки, чтобы пользователи могли адаптировать их в качестве набора данных для своих учебных сценариев использования.

Проблемы при реализации: Для более широкой аудитории мы планируем реализовать решение для выполнения в ЦП, а не в графическом процессоре NVIDIA. Использование графического процессора NVIDIA может повысить эффективность конвейера.
Реализация извлечения эмбеддинга лиц на центральном процессоре выполняется очень медленно (30+ секунд на изображение). Чтобы справиться с проблемой, мы реализуем их с параллельным выполнением конвейера (что дает ~ 13 секунд на изображение), а затем объединяем их результаты для дальнейших задач кластеризации. Мы представляем tqdm вместе с PyPiper для обновления хода выполнения и изменения размера кадров, извлеченных из входного видео, для плавного выполнения конвейера.

Вход: видео.mp4
Выход:

Required Python3 modules:
os, cv2, numpy, tensorflow, json, re, shutil, time, pickle, pyPiper, tqdm, imutils, face_recognition, dlib, warnings, sklearn

Раздел фрагментов:
Что касается содержимого файла FaceClusteringLibrary.py , который содержит все определения классов, ниже приведены фрагменты и объяснение их работы.

Реализация класса ResizeUtils предоставляет функции rescale_by_height и rescale_by_width .
Rescale_by_width - это функция, которая принимает в качестве входных данных 'image' и 'target_width'. Он увеличивает / уменьшает размер изображения для ширины, чтобы соответствовать target_width . Высота рассчитывается автоматически, поэтому соотношение сторон остается неизменным. rescale_by_height тоже то же самое, но вместо ширины он нацелен на высоту.

'''
The ResizeUtils provides resizing function
to keep the aspect ratio intact
Credits: AndyP at StackOverflow'''
class ResizeUtils:
# Given a target height, adjust the image
# by calculating the width and resize
def rescale_by_height( self , image, target_height,
method = cv2.INTER_LANCZOS4):
# Rescale `image` to `target_height`
# (preserving aspect ratio)
w = int ( round (target_height * image.shape[ 1 ] / image.shape[ 0 ]))
return (cv2.resize(image, (w, target_height),
interpolation = method))
# Given a target width, adjust the image
# by calculating the height and resize
def rescale_by_width( self , image, target_width,
method = cv2.INTER_LANCZOS4):
# Rescale `image` to `target_width`
# (preserving aspect ratio)
h = int ( round (target_width * image.shape[ 0 ] / image.shape[ 1 ]))
return (cv2.resize(image, (target_width, h),
interpolation = method))

Ниже приводится определение класса FramesGenerator Этот класс предоставляет функциональные возможности для извлечения изображений jpg путем последовательного чтения видео. Если мы возьмем пример входного видеофайла, он может иметь частоту кадров ~ 30 кадров в секунду. Можно сделать вывод, что на 1 секунду видео будет 30 изображений. Даже для 2-минутного видео количество изображений для обработки будет 2 * 60 * 30 = 3600. Это слишком большое количество изображений для обработки и может занять несколько часов для полной конвейерной обработки.

Но есть еще один факт, что лица и люди могут не измениться за секунду. Таким образом, учитывая 2-минутное видео, создание 30 изображений за 1 секунду является громоздким и повторяющимся процессом. Вместо этого мы можем сделать только 1 снимок за 1 секунду. Реализация «FramesGenerator» выгружает только 1 изображение в секунду из видеоклипа.

Учитывая, что выгруженные изображения подлежат face_recognition/dlib для извлечения лиц, мы стараемся поддерживать пороговое значение высоты не более 500 и ширины до 700. Это ограничение накладывается функцией «AutoResize», которая далее вызывает rescale_by_height или rescale_by_width для уменьшения размера изображения в случае превышения ограничений, но при этом сохраняется соотношение сторон.

AutoResize к следующему фрагменту, функция AutoResize пытается наложить ограничение на заданный размер изображения. Если ширина больше 700, мы уменьшаем ее, чтобы сохранить ширину 700 и сохранить соотношение сторон. Другой установленный здесь предел: высота не должна превышать 500.

# The FramesGenerator extracts image
# frames from the given video file
# The image frames are resized for
# face_recognition / dlib processing
class FramesGenerator:
def __init__( self , VideoFootageSource):
self .VideoFootageSource = VideoFootageSource
# Resize the given input to fit in a specified
# size for face embeddings extraction
def AutoResize( self , frame):
resizeUtils = ResizeUtils()
height, width, _ = frame.shape
if height > 500 :
frame = resizeUtils.rescale_by_height(frame, 500 )
self .AutoResize(frame)
if width > 700 :
frame = resizeUtils.rescale_by_width(frame, 700 )
self .AutoResize(frame)
return frame

Ниже приведен фрагмент функции GenerateFrames Он запрашивает частоту кадров, чтобы решить, из какого количества кадров можно выгрузить одно изображение. Мы очищаем выходной каталог и начинаем повторять кадры. AutoResize размер изображения, если оно достигает предела, указанного в функции AutoResize.

# Extract 1 frame from each second from video footage
# and save the frames to a specific folder
def GenerateFrames( self , OutputDirectoryName):
cap = cv2.VideoCapture( self .VideoFootageSource)
_, frame = cap.read()
fps = cap.get(cv2.CAP_PROP_FPS)
TotalFrames = cap.get(cv2.CAP_PROP_FRAME_COUNT)
print ( "[INFO] Total Frames " , TotalFrames, " @ " , fps, " fps" )
print ( "[INFO] Calculating number of frames per second" )
CurrentDirectory = os.path.curdir
OutputDirectoryPath = os.path.join(
CurrentDirectory, OutputDirectoryName)
if os.path.exists(OutputDirectoryPath):
shutil.rmtree(OutputDirectoryPath)
time.sleep( 0.5 )
os.mkdir(OutputDirectoryPath)
CurrentFrame = 1
fpsCounter = 0
FrameWrittenCount = 1
while CurrentFrame < TotalFrames:
_, frame = cap.read()
if (frame is None ):
continue
if fpsCounter > fps:
fpsCounter = 0
frame = self .AutoResize(frame)
filename = "frame_" + str (FrameWrittenCount) + ".jpg"
cv2.imwrite(os.path.join(
OutputDirectoryPath, filename), frame)
FrameWrittenCount + = 1
fpsCounter + = 1
CurrentFrame + = 1
print ( '[INFO] Frames extracted' )

Ниже приведен фрагмент класса FramesProvider Он наследует «Узел», который можно использовать для построения конвейера обработки изображений. Реализуем функции «настройка» и «запуск». Любые аргументы, определенные в функции «setup», могут иметь параметры, которые конструктор будет ожидать в качестве параметров во время создания объекта. Здесь мы можем передать sourcePath параметр в FramesProvider объекта. Функция «настройка» запускается только один раз. Функция «run» запускается и продолжает передавать данные, вызывая emit для конвейера обработки, пока не будет вызвана функция close

Здесь, в «настройке», мы принимаем sourcePath в качестве аргумента и перебираем все файлы в заданном каталоге фреймов. Какое бы расширение файла ни было .jpg (которое будет сгенерировано классом FrameGenerator ), мы добавляем его в список «filesList».

Во время вызовов run все пути к изображениям jpg из «filesList» упаковываются с атрибутами, указывающими уникальный «id» и «imagePath» в качестве объекта, и отправляются в конвейер для обработки.

# Following are nodes for pipeline constructions.
# It will create and asynchronously execute threads
# for reading images, extracting facial features and
# storing them independently in different threads
# Keep emitting the filenames into
# the pipeline for processing
class FramesProvider(Node):
def setup( self , sourcePath):
self .sourcePath = sourcePath
self .filesList = []
for item in os.listdir( self .sourcePath):
_, fileExt = os.path.splitext(item)
if fileExt = = '.jpg' :
self .filesList.append(os.path.join(item))
self .TotalFilesCount = self .size = len ( self .filesList)
self .ProcessedFilesCount = self .pos = 0
# Emit each filename in the pipeline for parallel processing
def run( self , data):
if self .ProcessedFilesCount < self .TotalFilesCount:
self .emit({ 'id' : self .ProcessedFilesCount,
'imagePath' : os.path.join( self .sourcePath,
self .filesList[ self .ProcessedFilesCount])})
self .ProcessedFilesCount + = 1
self .pos = self .ProcessedFilesCount
else :
self .close()

Ниже приводится реализация класса FaceEncoder, который наследует «Node» и может быть передан в конвейер обработки изображений. В функции «setup» мы принимаем значение «detect_method» для вызова распознавателя лиц «face_recognition / dlib». Он может иметь детектор на основе «cnn» или «hog».
Функция «run» распаковывает входящие данные в «id» и «imagePath».

Затем он считывает изображение из «imagePath», запускает «face_location», определенный в библиотеке «face_recognition / dlib», чтобы вырезать выровненное изображение лица, которое является нашей областью интереса. Выровненное изображение лица - это прямоугольное обрезанное изображение, глаза и губы которого выровнены по определенному месту на изображении (Примечание: реализация может отличаться от других библиотек, например opencv).

Далее мы вызываем функцию «face_encodings», определенную в «face_recognition / dlib», чтобы извлечь вложения лиц из каждого блока. Это встраивание плавающих значений может помочь вам достичь точного местоположения функций на выровненном изображении лица.

Мы определяем переменную «d» как массив ящиков и соответствующих вложений. Теперь мы упаковываем «id» и массив вложений в качестве ключа «кодирования» в объект и отправляем его в конвейер обработки изображений.

# Encode the face embedding, reference path
# and location and emit to pipeline
class FaceEncoder(Node):
def setup( self , detection_method = 'cnn' ):
self .detection_method = detection_method
# detection_method can be cnn or hog
def run( self , data):
id = data[ 'id' ]
imagePath = data[ 'imagePath' ]
image = cv2.imread(imagePath)
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
boxes = face_recognition.face_locations(
rgb, model = self .detection_method)
encodings = face_recognition.face_encodings(rgb, boxes)
d = [{ "imagePath" : imagePath, "loc" : box, "encoding" : enc}
for (box, enc) in zip (boxes, encodings)]
self .emit({ 'id' : id , 'encodings' : d})

Ниже приводится реализация DatastoreManager которая снова наследуется от «узла» и может быть подключена к конвейеру обработки изображений. Цель этого класса - выгрузить массив «encodings» как файл pickle и использовать параметр «id» для уникального имени файла pickle. Мы хотим, чтобы конвейер работал многопоточным.
Чтобы использовать многопоточность для повышения производительности, нам нужно правильно разделить асинхронные задачи и попытаться избежать какой-либо необходимости в синхронизации. Итак, для максимальной производительности мы независимо разрешаем потокам в конвейере записывать данные в отдельный отдельный файл, не мешая другим операциям потока.

Если вы думаете, сколько времени это сэкономило на используемом оборудовании для разработки, без многопоточности, среднее время извлечения встраивания составило ~ 30 секунд. После многопоточного конвейера (с 4 потоками) он уменьшился до ~ 10 секунд, но за счет высокой загрузки ЦП.
Поскольку поток занимает около 10 секунд, частая запись на диск не происходит, и это не снижает нашу многопоточную производительность.

Другой случай, если вы думаете, почему используется pickle вместо альтернативы JSON? Правда в том, что JSON - лучшая альтернатива рассолу. Pickle очень небезопасен для хранения данных и обмена данными. Pickles можно злонамеренно модифицировать для встраивания исполняемых кодов в Python. Файлы JSON удобочитаемы и быстрее кодируются и декодируются. Единственное, в чем хорош pickle, - это безошибочный сброс объектов и содержимого Python в двоичные файлы.

Поскольку мы не планируем хранить и распространять файлы pickle, а для безошибочного выполнения мы используем pickle. В противном случае настоятельно рекомендуется использовать JSON и другие альтернативы.

# Recieve the face embeddings for clustering and
# id for naming the distinct filename
class DatastoreManager(Node):
def setup( self , encodingsOutputPath):
self .encodingsOutputPath = encodingsOutputPath
def run( self , data):
encodings = data[ 'encodings' ]
id = data[ 'id' ]
with open (os.path.join( self .encodingsOutputPath,
'encodings_' + str ( id ) + '.pickle' ), 'wb' ) as f:
f.write(pickle.dumps(encodings))

Ниже приведена реализация класса PickleListCollator . Он предназначен для чтения массивов объектов в нескольких файлах pickle, слияния в один массив и выгрузки объединенного массива в один файл pickle.

Здесь есть только одна функция GeneratePickle которая принимает outputFilepath который указывает единственный выходной файл pickle, который будет содержать объединенный массив.

# PicklesListCollator takes multiple pickle
# files as input and merges them together
# It is made specifically to support use-case
# of merging distinct pickle files into one
class PicklesListCollator:
def __init__( self , picklesInputDirectory):
self .picklesInputDirectory = picklesInputDirectory
# Here we will list down all the pickles
# files generated from multiple threads,
# read the list of results append them to a
# common list and create another pickle
# with combined list as content
def GeneratePickle( self , outputFilepath):
datastore = []
ListOfPickleFiles = []
for item in os.listdir( self .picklesInputDirectory):
_, fileExt = os.path.splitext(item)
if fileExt = = '.pickle' :
ListOfPickleFiles.append(os.path.join(
self .picklesInputDirectory, item))
for picklePath in ListOfPickleFiles:
with open (picklePath, "rb" ) as f:
data = pickle.loads(f.read())
datastore.extend(data)
with open (outputFilepath, 'wb' ) as f:
f.write(pickle.dumps(datastore))

Ниже представлена реализация класса FaceClusterUtility Определен конструктор, который принимает «EncodingFilePath» со значением в качестве пути к объединенному файлу pickle. Мы читаем массив из файла pickle и пытаемся кластеризовать их, используя реализацию «DBSCAN» в библиотеке «scikit». В отличие от k-средних, сканирование DBSCAN не требует количества кластеров. Количество кластеров зависит от параметра порога и рассчитывается автоматически.
Реализация DBSCAN предоставляется в «scikit» и также принимает количество потоков для вычислений.

Здесь у нас есть функция «Cluster», которая будет вызываться для чтения данных массива из файла pickle, запуска «DBSCAN», печати уникальных кластеров как уникальных граней и возврата меток. Метки представляют собой уникальные значения, представляющие категории, которые можно использовать для определения категории лица, присутствующего в массиве. (Содержимое массива поступает из файла рассола).

# Face clustering functionality
class FaceClusterUtility:
def __init__( self , EncodingFilePath):
self .EncodingFilePath = EncodingFilePath
# Credits: Arian's pyimagesearch for the clustering code
# Here we are using the sklearn.DBSCAN functioanlity
# cluster all the facial embeddings to get clusters
# representing distinct people
def Cluster( self ):
InputEncodingFile = self .EncodingFilePath
if not (os.path.isfile(InputEncodingFile) and
os.access(InputEncodingFile, os.R_OK)):
print ( 'The input encoding file, ' +
str (InputEncodingFile) +
' does not exists or unreadable' )
exit()
NumberOfParallelJobs = - 1
# load the serialized face encodings
# + bounding box locations from disk,
# then extract the set of encodings to
# so we can cluster on them
print ( "[INFO] Loading encodings" )
data = pickle.loads( open (InputEncodingFile, "rb" ).read())
data = np.array(data)
encodings = [d[ "encoding" ] for d in data]
# cluster the embeddings
print ( "[INFO] Clustering" )
clt = DBSCAN(eps = 0.5 , metric = "euclidean" ,
n_jobs = NumberOfParallelJobs)
clt.fit(encodings)
# determine the total number of
# unique faces found in the dataset
labelIDs = np.unique(clt.labels_)
numUniqueFaces = len (np.where(labelIDs > - 1 )[ 0 ])
print ( "[INFO] # unique faces: {}" . format (numUniqueFaces))
return clt.labels_

Ниже представлена реализация TqdmUpdate который наследуется от «tqdm». tqdm - это библиотека Python, которая визуализирует индикатор выполнения в интерфейсе консоли.
Переменные «n» и «total» распознаются «tqdm». Значения этих двух переменных используются для расчета достигнутого прогресса.
Параметры «done» и «total_size» в функции «update» предоставляют значения при привязке к событию обновления в конвейерной структуре «PyPiper». super().refresh() вызывает реализацию функции «обновления» в классе «tqdm», которая визуализирует и обновляет индикатор выполнения в консоли.

# Inherit class tqdm for visualization of progress
class TqdmUpdate(tqdm):
# This function will be passed as progress
# callback function. Setting the predefined
# variables for auto-updates in visualization
def update( self , done, total_size = None ):