По данным Национального совета безопасности (NSC), каждый год вождение в сонном состоянии является причиной около 100 000 аварий, 71 000 травм и 1550 смертельных исходов. Кроме того, исследование, проведенное Фондом безопасности дорожного движения AAA, показало, что сонливость была фактором, способствующим до 9,5% всех аварий и 10,8% аварий, которые включали срабатывание подушек безопасности, травмы или значительный материальный ущерб.

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

Моя цель — создать одну модель глубокого обучения, которая предсказывает, зевает человек или нет, и другую, которая предсказывает, закрыт или открыт глаз на основе набора данных изображения. В сочетании эти элементы могут помочь сформировать более надежную модель обнаружения сонливости.

Полный код на моем Github.

Обзор данных

Набор данных получен от Kaggle. Отмечено, что я не уверен в том, как изначально были получены данные, но я вручную просматривал изображения каждого класса. Он уже разделен между тестовым и обучающим набором. Классы Closed, Open, yawn и no_yawn. Изначально в наборе данных было 2900 изображений. Каждый каталог имеет четыре класса:

  • Closed — изображение закрытого глаза крупным планом.
  • Открыть — изображение открытого глаза крупным планом.
  • Зевает — водители в машине зевают.
  • No_yawn — водители в машине не зевают

Набор «Закрытый поезд» содержит 627 изображений и 109 изображений в тестовом наборе. Набор Open train содержит 617 изображений и 109 изображений в тестовом наборе. Набор поездов для зевоты содержит 637 изображений и 106 изображений в тестовом наборе. Набор поездов no_yawn содержит 627 изображений и 109 изображений в тестовом наборе.

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

ЭДА

Загрузить каталог

#train directory
traindir = r'/content/drive/MyDrive/ColabNotebooks/DrowsinessDetection/Data/dataset_new/train'

#test directory
testdir = r'/content/drive/MyDrive/ColabNotebooks/DrowsinessDetection/Data/dataset_new/test'

Получить список изображений в каждом подкаталоге

#Train Set
train_closed_img = [fn for fn in os.listdir(f'{traindir}/Closed') if fn.endswith('.jpg')]
train_open_img = [fn for fn in os.listdir(f'{traindir}/Open') if fn.endswith('.jpg')]
train_yawn_img = [fn for fn in os.listdir(f'{traindir}/yawn') if fn.endswith('.jpg')]
train_noyawn_img = [fn for fn in os.listdir(f'{traindir}/no_yawn') if fn.endswith('.jpg')]
#Test set
test_closed_img = [fn for fn in os.listdir(f'{testdir}/Closed') if fn.endswith('.jpg')]
test_open_img = [fn for fn in os.listdir(f'{testdir}/Open') if fn.endswith('.jpg')]
test_yawn_img = [fn for fn in os.listdir(f'{testdir}/yawn') if fn.endswith('.jpg')]
test_noyawn_img = [fn for fn in os.listdir(f'{testdir}/no_yawn') if fn.endswith('.jpg')]

А. Случайные изображения

import random

def getRandImg(directory, subfolder):
  path = directory + subfolder
  file_path_type = os.listdir(path) 

  fig = plt.figure(figsize = (8,6))
  for i in range(3):
    images = random.choice(file_path_type)

    ax = fig.add_subplot(1, 3, i+1)

    imgs = cv.imread(path + '/' + images)[:,:,::-1]
    plt.imshow(imgs)
    plt.axis('off')
    plt.title(images)
  plt.show()

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

Б. Размер класса

train_number_classes = {'Open': len(os.listdir(traindir + '/Open')),
'Closed': len(os.listdir(traindir + '/Closed')),
'yawn': len(os.listdir(traindir + '/yawn')),
'no_yawn': len(os.listdir(traindir + '/no_yawn'))}

train_number_classes = dict(train_number_classes)

fig, ax = plt.subplots(figsize=(10,5))
plt.bar(train_number_classes.keys(),train_number_classes.values());

plt.title("Number of Images by Class: Train Set");
plt.xlabel('Class Name');
plt.ylabel('# Images');

Выше показано количество изображений в каждом классе тренировочного набора. Это показывает, что классы имеют одинаковое количество изображений. В открытом 617 изображений. Закрытое имеет 627 изображений. No_yawn содержит 627 изображений. Yawn содержит 637 изображений.

С. Размер изображения

# Check for image size
train_set = {'Open': traindir + '/Open/',
'Closed': traindir + '/Closed/',
'yawn': traindir + '/yawn/',
'no_yawn': traindir + '/no_yawn/'}
#create function to get dimenstions for RBG
def get_dims(file):
#Returns dimenstions for an RBG image
  im = Image.open(file)
  arr = np.array(im)
  h,w,d = arr.shape
  return h,w

def loop_dir(directories):

  for n,d in directories.items():
    filepath = d
    filelist = [filepath + f for f in os.listdir(filepath)]
    dims = db.from_sequence(filelist).map(get_dims)
    with ProgressBar():
      dims = dims.compute()
      dim_df = pd.DataFrame(dims, columns=['height', 'width'])
      sizes = dim_df.groupby(['height', 'width']).size().reset_index().rename(columns={0:'count'})
    
      sizes.plot.scatter(x='width', y='height');
      plt.title('Image Sizes (pixels) | {}'.format(n))

#Get size of images
loop_dir(train_set)

Выше приведены размеры изображений каждого класса тренировочного набора. Классы yawn и no_yawn имеют одинаковый размер изображения. Открытые и закрытые классы имеют различные размеры изображений. Эти классы должны быть изменены для модели.

Д. RGB набора поездов

На диаграммах ниже показаны средние значения красного, зеленого и синего каналов для каждого изображения в каждом классе. Для этого раздела я следовал руководству по этой статье.

Красный

Выше показано распределение средних красных каналов каждого класса. Между классами yawn и no_yawn класс без зевоты имеет немного более высокое среднее значение красного по сравнению с классом yawn. Открытый класс имеет более высокое распределение средних красных каналов по сравнению с закрытым классом.

Зеленый

Выше показано распределение средних зеленых каналов каждого класса. Между классами yawn и no_yawn класс yawn имеет немного более высокое среднее значение зеленого по сравнению с классом no yawn. В классе зевоты есть полоса, которая представляет более высокую частоту по сравнению с другими полосами для средних зеленых каналов изображений. Закрытый класс и открытый класс имеют схожие распределения среднего значения зеленых каналов.

Синий

Между классами yawn и no_yawn класс yawn имеет несколько более высокое распределение средних значений синего канала по сравнению с классом no yawn. Закрытый и открытый классы имеют схожие распределения средних значений синего.

С. Среднее изображение для каждого класса

def find_mean_img(full_mat, title, size = (64, 64)):
    # calculate the average
    mean_img = np.mean(full_mat, axis = 0)
    # reshape it back to a matrix
    mean_img = mean_img.reshape(size)
    plt.imshow(mean_img, vmin=0, vmax=255, cmap='Greys_r')
    plt.title(f'Average {title}')
    plt.axis('off')
    plt.show()
    #plot_rgb(mean_img)
    return mean_img

train_closed_mean = find_mean_img(tr_closed_img, 'Closed')
train_open_mean = find_mean_img(tr_open_img, 'Open')
train_yawn_mean = find_mean_img(tr_yawn_img, 'yawn')
train_noyawn_mean = find_mean_img(tr_noyawn_img, 'no_yawn')

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

Д. Контраст

def findContrast(mean1, mean2, title):
  contrast_mean = mean1 - mean2
  plt.imshow(contrast_mean, cmap='bwr')
  plt.title(title)
  plt.axis('off')
  plt.show()

#Between Closed and Open of training set
findContrast(train_open_mean, train_closed_mean, 'Difference Between Closed & Open Average: Train set')

#Between Yawn and no yawn of training set
findContrast(train_noyawn_mean, train_yawn_mean, 'Difference Between Yawn & No Yawn Average: Train set')

Вышеприведенное показывает разницу между открытыми и закрытыми классами, а также классами с зевотой и без зевоты в тренировочном наборе. Синим цветом показаны области, которые отличаются в зависимости от среднего изображения. Я вижу, в чем разница между закрытыми и открытыми классами, так как открытый глаз и радужка особенно выделяются. Классы yawn и no_yawn, по-видимому, в основном фокусируются на фоне обычных изображений.

Е. Собственные изображения

Ниже приведены собственные изображения каждого класса в обучающем наборе. Эти собственные образы дают нам представление о том, какие различные важные аспекты изображения составляют изображение в целом. Они могут указывать на потенциально важные функции.

from sklearn.decomposition import PCA
from math import ceil

def eigenimages(full_mat, title, n_comp = 0.7, size = (64, 64)):
    # fit PCA to describe n_comp * variability in the class
    pca = PCA(n_components = n_comp, whiten = True)
    pca.fit(full_mat)
    print('Number of PC: ', pca.n_components_)
    return pca
  
def plot_pca(pca, size = (64, 64)):
    # plot eigenimages in a grid
    n = pca.n_components_
    fig = plt.figure(figsize=(8, 8))
    r = int(n**.5)
    c = ceil(n/ r)
    for i in range(n):
        ax = fig.add_subplot(r, c, i + 1, xticks = [], yticks = [])
        ax.imshow(pca.components_[i].reshape(size), 
                  cmap='Greys_r')
    plt.axis('off')
    plt.show()
plot_pca(eigenimages(tr_closed_img, 'Closed'))
plot_pca(eigenimages(tr_open_img, 'Open'))

Закрыто

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

Открыть

Для открытого класса первое изображение фокусируется на роговице и радужной оболочке глаза. Второе изображение фокусируется на изгибе глаза слева направо. Третье изображение фокусируется на структуре бровей и на том, как глаз сидит под бровью. Четвертое изображение похоже, как и третье изображение из закрытого класса, направлено на отделение области, где находится глаз, от фона. Пятое изображение, кажется, фокусируется на тени носа и структурах вокруг глаза.

Зевок

Для классов зевоты и отсутствия зевоты труднее различать изображения. Было 17 собственных векторов для зевоты и 17 для отсутствия зевоты. В каждом случае я выбрал несколько из этих особенностей, которые казались более узнаваемыми в том, что они представляли.

Для класса зевоты первое изображение фокусируется на заднем стекле, а не на переднем плане автомобиля. Второе изображение фокусируется на лице водителя, сидящего в машине. Третье изображение фокусируется на элементах внутри автомобиля. Четвертое изображение фокусируется на позиционировании тела водителя. Из них кажется вероятным, что второй собственный вектор, представленный здесь, будет ближе всего к полезной функции для предсказания зевоты.

Не зевать

Для класса без зевоты первое изображение фокусируется на внутренней части автомобиля. Второе изображение фокусируется на лице водителя. Третье изображение фокусируется на окне автомобиля. На четвертом изображении в фокусе находится тело водителя.

Предварительная обработка изображения

Размер изменен

def resize(path, new_folder):
  
  for img in os.listdir(path):
    resized_img = Image.open(path + '/' + img)
    resized_img = resized_img.convert('RGB')
    #resized_img.thumbnail((150, 150))
    resized_img = resized_img.resize((300,300), Image.LANCZOS)
    
    #save in same folder
    #resized_img.save(path + '/' + img)

    #save in new folder
    resized_img.save(new_folder + '/' + img)
# path of folder
train_closed_folder = "/content/drive/MyDrive/ColabNotebooks/DrowsinessDetection/Data/dataset_new/train/resized_closed"
# call function
resize(traindir + '/Closed', train_closed_folder)

# path of folder
train_open_folder = "/content/drive/MyDrive/ColabNotebooks/DrowsinessDetection/Data/dataset_new/train/resized_open"
# call function
resize(traindir + '/Open', train_open_folder)

Как было обнаружено ранее, классы Closed и Open имеют разные размеры, поэтому их необходимо изменить. Я помещаю измененные изображения в их собственную папку. Измененные изображения имеют размер 300 на 300 пикселей.

Оттенки серого

def grayscale(directory, new_folder):
  for img in os.listdir(directory):
    image = cv.imread(directory + '/' + img)
    gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)

    #save to new folder
    cv.imwrite(new_folder + '/' + img, gray)
#Have folder for the grayscale images
folder = "/content/drive/MyDrive/ColabNotebooks/DrowsinessDetection/Data/dataset_new/train/gray_noyawn"

#Call grayscale function
grayscale(traindir + '/no_yawn', folder)

Я оттенил изображения каждого набора классов и поместил их в отдельную папку с помощью OpenCV. Ниже приведен пример изображения в градациях серого.

Моделирование

Я начал с того, что разделил модели на классы «зевота/без зевоты» и закрытые/открытые классы. Для классификатора зевоты я выбрал «зевок» в качестве положительного класса, так как с точки зрения бизнеса нас больше заботит память и точность предсказания зевоты, чем отсутствие зевоты, поскольку важно убедиться, что мы знаем, когда водитель надевает себя. или другие группы риска. По той же причине я выбрал «закрытый» как положительный класс для обучения закрытого/открытого классификатора. Для обеих моделей я создал Dummy Classifier, Sequential и предварительно обученную модель MobileNetV2. Классы yawn и no_yawn имеют несколько больше изображений в тренировочном наборе по сравнению с классами Closed и Open. Благодаря этому я смог стандартизировать классы Closed и Open для модели, но не классы yawn и no_yawn. Модель закрытых и открытых классов имела эпоху 15 с размером пакета 32. Модель классов yawn и no_yawn имела эпоху 10 с размером пакета 32. Ниже приведены результаты модели для классов с использованием обучающего набора и проверки. набор.

Ниже приведены показатели модели.

Фиктивный классификатор просто предсказывает наиболее распространенную метку.

Учитывая, что набор данных сбалансирован, это приводит к тому, что классы Open и Closed имеют точность 0,50 для обучающего набора. Итак, мы видим, что наши обученные модели значительно превосходят его.

Ниже приведен график, показывающий потери и точность модели для классов Open и Closed для последовательной модели.

Ниже приведен график, показывающий потери и точность модели для классов Open и Closed для предварительно обученной модели с использованием MobileNetV2.

Последовательная модель работала лучше, чем модель MobileNetV2 для классов Open и Closed.

Ниже приведены показатели модели.

Опять же, учитывая, что классы сбалансированы, фиктивный классификатор имеет оценку 0,50 для обучающей выборки, которую обе модели значительно превосходят.

Ниже приведен график, показывающий потери и точность модели для классов yawn и no_yawn для последовательной модели.

Ниже приведен график, показывающий потери и точность модели для классов yawn и no_yawn для предварительно обученной модели с использованием MobileNetV2.

Модель MobileNetV2 работала лучше, чем последовательная модель для классов theyawn и no_yawn.

Перемещение порога

Эти модели могут использоваться автомобильными компаниями для предупреждения водителей, если они слишком много зевают или закрывают глаза. Если модели предсказывают ложное срабатывание, водитель будет предупрежден о том, что он дремлет, когда это не так. Это может вызвать раздражение водителя и нежелание покупать автомобиль с этой функцией.

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

Чтобы улучшить эту модель, мы можем оптимизировать показатель отзыва, потому что важны как точность, так и отзыв. Мы можем улучшить модель, изменив порог. Ниже приведены текущие показатели, основанные на пороге 0,50 на тестовом наборе.

Для модели yawn и no_yawn с MobileNetV2 ниже представлена ​​матрица путаницы с текущим порогом 0,50 и матрица путаницы с оптимальной оценкой отзыва. Оптимизированная оценка отзыва будет означать, что порог будет равен 0,00. Однако модель с порогом 0 была бы ненадежной моделью, потому что все не может быть помечено как зевота. Я сделал порог 0,15.

Ниже приведена таблица, показывающая метрики модели между пороговыми значениями 0,50 и 0,15 для прогнозов набора тестов для yawn/no_yawn.

Как видите, этой конкретной модели не требовалось изменение порога, потому что для тестового набора он уже был равен 1,0, а все остальные метрики выше пороговой отметки 0,50.

Ниже представлена ​​матрица путаницы модели при пороговых значениях 0,50 и 0,15. Как видите, изменений не было.

Ниже приведена диаграмма точного отзыва и порогового значения, показывающая пороговое значение по умолчанию и оптимизированное пороговое значение с метриками модели.

Ниже приведена таблица, показывающая метрики модели между пороговыми значениями 0,50 и 0,15 для прогнозов тестового набора для закрытых/открытых.

Как вы можете видеть, этой конкретной модели не требовалось изменение порога, потому что это приводило к небольшому уменьшению отзыва. Точность увеличилась до 1, хотя основное внимание уделяется отзыву.

Ниже представлена ​​матрица путаницы модели при пороговых значениях 0,50 и 0,15. Как видите, изменений не было.

Ниже приведена диаграмма точного отзыва и порогового значения, показывающая пороговое значение по умолчанию и оптимизированное пороговое значение с метриками модели.

Заключение

Для открытых и закрытых классов я бы использовал последовательную модель, потому что тестовый набор работал немного лучше, чем обучающий набор. Учебный набор MobileNetV2 работал немного лучше, чем набор для тестирования. Для классов yawn и no_yawn я бы использовал модель MobileNetV2, потому что тестовая выборка работала немного лучше, чем обучающая. Последовательный обучающий набор показал себя немного лучше, чем тестовый.

В целом обе модели работают хорошо. Следующим шагом является экспериментирование с моделями в среде с более высокой вычислительной мощностью. С Google Colab, если я добавлю MaxPooling2D или Dropout к последовательной модели или модели MobileNetV2, у него закончится память. Следующим шагом после этого я хотел бы преобразовать модель в обнаружение сонливости в реальном времени, которое автомобильные компании могут использовать, чтобы сделать дороги более безопасными.

Полный код на моем Github.