Вступление

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

Этого можно добиться, выполнив следующие задачи:

  • Выполните извлечение функции гистограммы ориентированных градиентов (HOG) на помеченном обучающем наборе изображений и обучите классификатор Linear SVM classifier.
  • Реализуйте технику скользящего окна и используйте обученный классификатор для поиска транспортных средств на изображениях.
  • Запустите конвейер для видеопотока и создайте тепловую карту повторяющихся обнаружений кадр за кадром, чтобы отбросить выбросы и следовать за обнаруженными транспортными средствами.
  • Оцените ограничивающую рамку для обнаруженных транспортных средств.

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

Вот ссылки на помеченные данные для примеров транспортных средств и не транспортных средств для обучения классификатора. Эти примеры изображений взяты из комбинации базы данных изображений транспортных средств GTI, набора тестов KITTI vision и примеров, извлеченных из самого видео проекта. Приглашаем вас воспользоваться преимуществами недавно выпущенного набора данных с меткой Udacity, чтобы пополнить свои данные обучения.

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

Подробности

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

Vehicle train images count: 8792
Non-vehicle train image count: 8968

Изображения транспортных средств и не транспортных средств в виде массива numpy загружаются в отдельный список с помощью функции ниже.

def get_dataset(rootdir):
    images = []
    for subdir, dirs, files in os.walk(rootdir):
        for file in files:
            if '.DS_Store' not in file:
                images.append(os.path.join(subdir, file))
                
    return list(map(lambda img: cv2.cvtColor(cv2.imread(img), cv2.COLOR_BGR2RGB), images))

Изображения транспортных средств и не транспортных средств:

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

Пространственное объединение

Хотя включение трех цветовых каналов изображения с полным разрешением может быть обременительным, вы можете выполнить пространственное объединение изображения и при этом сохранить достаточно информации, чтобы помочь в поиске транспортных средств.

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

Удобная функция для уменьшения разрешения изображения - cv2.resize () OpenCV. Если вы затем захотите преобразовать это в одномерный вектор признаков, можно использовать функцию numpy ravel ().

Цветовая гистограмма

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

Цветовая гистограмма - это просто гистограмма, которая показывает уровень цвета для каждого отдельного цветового канала RGB.

Если бы нам пришлось, мы могли бы различать два изображения только на основе различий в гистограммах. Как и ожидалось, изображение красной машины имеет большую интенсивность значений общих интервалов в R-гистограмме 1 (красный канал) по сравнению с R-гистограммой 2 синей машины. В отличие от этого синяя машина имеет большую интенсивность общих значений интервалов в B-гистограмме. 2 (синий канал), чем на гистограмме 1 красного автомобиля.

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

Гистограмма ориентированных градиентов (HOG)

Дескриптор функции - это представление изображения или фрагмента изображения, которое упрощает изображение, извлекая полезную информацию и отбрасывая постороннюю информацию.

Гистограмма ориентированных градиентов (HOG) - это дескриптор функции, используемый в компьютерном зрении и обработке изображений с целью обнаружения объектов. Методика подсчитывает появление градиентной ориентации в локализованных частях изображения.

В дескрипторе признаков HOG в качестве признаков используются распределения (гистограммы) направлений градиентов (ориентированные градиенты). Градиенты (производные по x и y) изображения полезны, потому что величина градиентов велика по краям и углам (области резкого изменения интенсивности), и мы знаем, что края и углы содержат гораздо больше информации о форме объекта, чем плоские области.

Затем нужно выбрать правильные параметры для обучения классификатора предсказанию изображения. Я определил класс параметров для определения этих параметров.

Вот выбранные параметры.

Parameters(
        color_space = 'YCrCb',
        spatial_size = (16, 16),
        orient = 8,
        pix_per_cell = 8,
        cell_per_block = 2,
        hog_channel = 'ALL',
        hist_bins = 32,
        scale = 1.5,
        spatial_feat=True, 
        hist_feat=True, 
        hog_feat=True
)

Извлечение признаков зависит от параметров. Я выполнил извлечение признаков с различными комбинациями параметров и обнаружил, что указанная выше комбинация параметров лучше всего подходит для извлечения признаков.

Классификатор

Машины опорных векторов (SVM) - это набор контролируемых методов обучения, используемых для классификации, регрессии и обнаружения выбросов. Я решил использовать LinearSVC в качестве классификатора этого проекта.

Вот код для обучения классификатора:

sample_size = 8750
cars = vehicles[0:sample_size]
notcars = non_vehicles[0:sample_size]
car_features = list(map(lambda img: extract_features(img, params), cars))
notcar_features = list(map(lambda img: extract_features(img, params), notcars))

X = np.vstack((car_features, notcar_features)).astype(np.float64)                        
# Fit a per-column scaler
X_scaler = StandardScaler().fit(X)
# Apply the scaler to X
scaled_X = X_scaler.transform(X)
# Define the labels vector
y = np.hstack((np.ones(len(car_features)), np.zeros(len(notcar_features))))

# Split up data into randomized training and test sets
rand_state = np.random.randint(0, 100)
X_train, X_test, y_train, y_test = train_test_split(
    scaled_X, y, test_size=0.2, random_state=rand_state)
print('Using:',params.orient,'orientations',params.pix_per_cell,
    'pixels per cell and', params.cell_per_block,'cells per block')
print('Feature vector length:', len(X_train[0]))
# Use a linear SVC 
svc = LinearSVC()
# Check the training time for the SVC
t=time.time()
svc.fit(X_train, y_train)
t2 = time.time()
print(round(t2-t, 2), 'Seconds to train SVC...')
# Check the score of the SVC
print('Test Accuracy of SVC = ', round(svc.score(X_test, y_test), 4))

Изображения автомобилей и нор-каров загружаются и извлекаются с помощью описанных выше методов извлечения элементов. Эти значения хранятся в car_features и notcar_features. Мы предполагаем, что вывод carfeatures будет равен единице, а notcar_features равен нулю. Значение «y» заполняется на основе этого предположения. «X» - это комбинация списка car_features и notcar_features.

StandardScaler предполагает, что данные обычно распределяются внутри каждой функции, и будет масштабировать их таким образом, чтобы теперь распределение было сосредоточено вокруг 0 ​​со стандартным отклонением 1. Значения «x» преобразуются с помощью функции и получают результат scaled_X.

Есть несколько библиотек, помогающих разделить набор данных. Функция «train_test_split» от «sklearn» - одна из них, которая помогает разделить набор данных на обучение тестовых данных для классификатора.

Наконец, LinearSVC определяется и передает x_train и y_train для обучения классификатора. По завершении тестовый набор данных используется для проверки точности классификатора.

Точность классификатора составляет около 99 процентов на тестовой части набора данных.

Using: 8 orientations 8 pixels per cell and 2 cells per block
Feature vector length: 5568
3.04 Seconds to train SVC...
Test Accuracy of SVC =  0.9911

Раздвижное окно

В контексте компьютерного зрения (как следует из названия) скользящее окно - это прямоугольная область фиксированной ширины и высоты, которая «скользит» по изображению. Для каждого из этих окон мы обычно берем область окна и применяем классификатор изображений, чтобы определить, есть ли в окне интересующий нас объект.

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

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

Вот код тепловой карты и порогового значения:

def add_heat(heatmap, bbox_list):
    # Iterate through list of bboxes
    for box in bbox_list:
        # Add += 1 for all pixels inside each bbox
        # Assuming each "box" takes the form ((x1, y1), (x2, y2))
        heatmap[box[0][1]:box[1][1], box[0][0]:box[1][0]] += 1
    # Return updated heatmap
    return heatmap# Iterate through list of bboxes
    
def apply_threshold(heatmap, threshold):
    # Zero out pixels below the threshold
    heatmap[heatmap <= threshold] = 0
    # Return thresholded map
    return heatmap
    
    
def heat_threshold(img, threshold, svc, X_scaler, windows_list, params):
    heat = np.zeros_like(img[:,:,0]).astype(np.float)
    # Add heat to each box in box list
    heat = add_heat(heat,windows_list)
    # Apply threshold to help remove false positives
    heat = apply_threshold(heat,threshold)
    # Visualize the heatmap when displaying    
    heatmap = np.clip(heat, 0, 255)
    # Find final boxes from heatmap using label function
    labels = label(heatmap)
    draw_img = draw_labeled_bboxes(np.copy(img), labels)
    
    return draw_img, heatmap

Подвыборка свиней - более эффективный метод для подхода со скользящим окном. Код должен только один раз извлечь черты «свиньи», а затем его можно выполнить субдискретизацию, чтобы получить все его перекрывающиеся окна. Каждое окно определяется коэффициентом масштабирования, где масштаб 1 приведет к окну размером 8 x 8 ячеек, тогда как перекрытие каждого окна определяется расстоянием между ячейками. Это означает, что Cell_per_step = 2 приведет к перекрытию окна поиска на 75%. Эту же функцию можно запускать несколько раз для разных значений шкалы для создания окон поиска с несколькими масштабами. Подвыборка свиней помогает сократить время вычислений для поиска функций HOG и, таким образом, обеспечивает более высокую пропускную способность.

Я решил выбрать положение окна поиска от 350px до 656px, а значение cells_per_step уменьшено до единицы, чтобы получить более точный результат.

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

Вот код для поиска автомобилей с использованием окна подвыборки свиней:

def find_cars_hog_sub(img, ystart, ystop, svc, scaler, params, cells_per_step = 1):
    draw_img = np.copy(img)
    cspace = params.color_space
    
    img_tosearch = img[ystart:ystop,:,:]
    if cspace != 'RGB':
        if cspace == 'HSV':
            ctrans_tosearch = cv2.cvtColor(img_tosearch, cv2.COLOR_RGB2HSV)
        elif cspace == 'LUV':
            ctrans_tosearch = cv2.cvtColor(img_tosearch, cv2.COLOR_RGB2LUV)
        elif cspace == 'HLS':
            ctrans_tosearch = cv2.cvtColor(img_tosearch, cv2.COLOR_RGB2HLS)
        elif cspace == 'YUV':
            ctrans_tosearch = cv2.cvtColor(img_tosearch, cv2.COLOR_RGB2YUV)
        elif cspace == 'YCrCb':
            ctrans_tosearch = cv2.cvtColor(img_tosearch, cv2.COLOR_RGB2YCrCb)
    else: ctrans_tosearch = np.copy(img_tosearch)  
    
    if params.scale != 1:
        imshape = ctrans_tosearch.shape
        ctrans_tosearch = cv2.resize(ctrans_tosearch, (np.int(imshape[1]/params.scale), np.int(imshape[0]/params.scale)))
        
    ch1 = ctrans_tosearch[:,:,0]
    ch2 = ctrans_tosearch[:,:,1]
    ch3 = ctrans_tosearch[:,:,2]
    # Define blocks and steps as above
    nxblocks = (ch1.shape[1] // params.pix_per_cell) - params.cell_per_block + 1
    nyblocks = (ch1.shape[0] // params.pix_per_cell) - params.cell_per_block + 1 
    nfeat_per_block = params.orient*params.cell_per_block**2
    
    # 64 was the orginal sampling rate, with 8 cells and 8 pix per cell
    window = 64
    nblocks_per_window = (window // params.pix_per_cell) - params.cell_per_block + 1
    nxsteps = (nxblocks - nblocks_per_window) // cells_per_step
    nysteps = (nyblocks - nblocks_per_window) // cells_per_step
    
    # Compute individual channel HOG features for the entire image
    hog1 = get_hog_features(ch1, params.orient, params.pix_per_cell, params.cell_per_block, feature_vec=False)
    hog2 = get_hog_features(ch2, params.orient, params.pix_per_cell, params.cell_per_block, feature_vec=False)
    hog3 = get_hog_features(ch3, params.orient, params.pix_per_cell, params.cell_per_block, feature_vec=False)
    car_windows = []
    for xb in range(nxsteps):
        for yb in range(nysteps):
            ypos = yb*cells_per_step
            xpos = xb*cells_per_step
            # Extract HOG for this patch
            hog_feat1 = hog1[ypos:ypos+nblocks_per_window, xpos:xpos+nblocks_per_window].ravel() 
            hog_feat2 = hog2[ypos:ypos+nblocks_per_window, xpos:xpos+nblocks_per_window].ravel() 
            hog_feat3 = hog3[ypos:ypos+nblocks_per_window, xpos:xpos+nblocks_per_window].ravel() 
            hog_features = np.hstack((hog_feat1, hog_feat2, hog_feat3))
            xleft = xpos*params.pix_per_cell
            ytop = ypos*params.pix_per_cell
            # Extract the image patch
            subimg = cv2.resize(ctrans_tosearch[ytop:ytop+window, xleft:xleft+window], (64,64))
          
            # Get color features
            spatial_features = bin_spatial(subimg, size=params.spatial_size)
            hist_features = color_hist(subimg, nbins=params.hist_bins, bins_range=params.hist_range)
            # Scale features and make a prediction
            test_features = X_scaler.transform(np.hstack((spatial_features, hist_features, hog_features)).reshape(1, -1))    
            test_prediction = svc.predict(test_features)
            
            if test_prediction == 1:
                xbox_left = np.int(xleft*params.scale )
                ytop_draw = np.int(ytop*params.scale )
                win_draw = np.int(window*params.scale )
                cv2.rectangle(draw_img,(xbox_left, ytop_draw+ystart),(xbox_left+win_draw,ytop_draw+win_draw+ystart),(0,0,255),6) 
                car_windows.append(((xbox_left, ytop_draw+ystart),(xbox_left+win_draw,ytop_draw+win_draw+ystart)))
                
    return car_windows

Видео о конвейере

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

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

Вот код конвейера:

def pipeline(img):
    ystart = 350
    ystop = 656
    threshold = 1
    car_windows = find_cars_hog_sub(img, ystart, ystop, svc, X_scaler, params)
    draw_img, heat_map = heat_threshold(img, threshold, svc, X_scaler, car_windows, params)
    
    return draw_img

Видео о конвейере:

Дополнительный вызов

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

Видео:

Если вы хотите подробно ознакомиться с кодом, посетите репозиторий github.