В данной статье будет рассказываться о применении библиотеки машинного зрения (openCV) для удаления эффекта радиального искажения (дисторсии) с фото и видео. Данный эффект также известен как эффект рыбьего глаза (fisheye) или distortion. Решение написать данную статью было принято после нескольких дней поиска информации в интернете. Не смотря на то, что есть гайды на английском языке, они не объясняют как правильно установить openCV, чтобы все работало. В статье присутствует готовый код.


Сразу привожу фото итогового результата. Слева оригинальное фото, справа — обработанное:


before after



Сборка и установка openCV


Первое, что нужно сделать, это грамотно установить библиотеку openCV. Для этого скачиваем из официального репозитория два проекта — openCV и opencv_contrib.


git clone https://github.com/opencv/opencv.git
git clone https://github.com/opencv/opencv_contrib.git

Пока загружается openCV, устанавливаем видеокодек ffmpeg:


sudo apt-get install ffmpeg

Заходим в папку openCV, создаем подпапку buid и заходим в нее. Вся работа по сборке и установке библиотеки openCV будет производиться из этой директории.


cd opencv
mkdir build
cd build/

Для сборки библиотеки выполняем следующие команды:


cmake .. -DOPENCV_EXTRA_MODULES_PATH=/путь к папке opencv_contrib/modules/ /путь к папке opencv/
make -j5
sudo make install

У меня сборка заняла около полутора часов, установка — несколько минут. Обратите внимание: если у вас возникла ошибка при сборке(выполнение команды cmake), для нового запуска необходимо удалить файл CMakeCache.txt. После установки можем проверить все ли правильно получилось. Для этого можно вызвать рабочую среду python и импортировать библиотеку openCV. Если никаких ошибок не произошло, то вы все сделали правильно. Вторая строчка покажет, какая версия у вас установлена. На момент написания статьи я пользовался 3 версией библиотеки.


import cv2
print ("OpenCV version : {0}".format(cv2.__version__))

Калибровка камеры


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


Определение поправочных коэффициентов
from __future__ import print_function
import numpy as np
import cv2
from common import splitfn
import os

if __name__ == '__main__':
    import sys
    import getopt
    from glob import glob

    args, img_mask = getopt.getopt(sys.argv[1:], '', ['debug=', 'square_size='])
    args = dict(args)
    args.setdefault('--debug', '/рабочая директория/')
    args.setdefault('--square_size', 1.0)
    if not img_mask:
        img_mask = '/папка с изображениями/*.png'
    else:
        img_mask = img_mask[0]

    img_names = glob(img_mask)
    debug_dir = args.get('--debug')
    if not os.path.isdir(debug_dir):
        os.mkdir(debug_dir)
    square_size = float(args.get('--square_size'))

    pattern_size = (9, 6)
    pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32)
    pattern_points[:, :2] = np.indices(pattern_size).T.reshape(-1, 2)
    pattern_points *= square_size

    obj_points = []
    img_points = []
    h, w = 0, 0
    img_names_undistort = []
    for fn in img_names:
        print('processing %s... ' % fn, end='')
        img = cv2.imread(fn, 0)
        if img is None:
            print("Failed to load", fn)
            continue

        h, w = img.shape[:2]
        found, corners = cv2.findChessboardCorners(img, pattern_size)
        if found:
            term = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.1)
            cv2.cornerSubPix(img, corners, (5, 5), (-1, -1), term)

        if not found:
            print('chessboard not found')
            continue

        img_points.append(corners.reshape(-1, 2))
        obj_points.append(pattern_points)

        print('ok')

    rms, camera_matrix, dist_coefs, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, (w, h), None, None)

    print("\nRMS:", rms)
    print("camera matrix:\n", camera_matrix)
    print("distortion coefficients: ", dist_coefs.ravel())

    cv2.destroyAllWindows()

В результате выполнения данного скрипта в консоли появится сообщение об обработанных фото и отобразятся два важных параметра — camera matrix и distortion coefficients. Это и есть те калибровочные коэффициенты, которые нам нужны.


coef

Обработка фото и видео


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


Скрипт для обработки фото
from __future__ import print_function
import numpy as np
import cv2
import glob
from matplotlib import pyplot as plt
from common import splitfn
import os

img_names_undistort = [img for img in glob.glob("/путь до папки с фотографиями/*.png")]
new_path = "/путь для сохранения обработанных изображений/"

camera_matrix = np.array([[1.26125746e+03, 0.00000000e+00, 9.40592038e+02],
                          [0.00000000e+00, 1.21705719e+03, 5.96848905e+02],
                          [0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]);
dist_coefs = np.array([-0.49181345,  0.25848255, -0.01067125, -0.00127517, -0.01900726]);

i = 0

#for img_found in img_names_undistort:
while i < len(img_names_undistort):
        img = cv2.imread(img_names_undistort[i])
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

        h,  w = img.shape[:2]
        newcameramtx, roi = cv2.getOptimalNewCameraMatrix(camera_matrix, dist_coefs, (w, h), 1, (w, h))

        dst = cv2.undistort(img, camera_matrix, dist_coefs, None, newcameramtx)

    dst = cv2.cvtColor(dst, cv2.COLOR_BGR2RGB)

        # crop and save the image
        x, y, w, h = roi
        dst = dst[y:y+h-50, x+70:x+w-20]

    name = img_names_undistort[i].split("/")
    name = name[6].split(".")
    name = name[0]
    full_name = new_path + name + '.jpg'

        #outfile = img_names_undistort + '_undistorte.png'
        print('Undistorted image written to: %s' % full_name)
        cv2.imwrite(full_name, dst)
    i = i + 1

Скрипт для обработки видео
from __future__ import print_function
import numpy as np
import cv2
import glob
from matplotlib import pyplot as plt
from common import splitfn
import os

FILENAME_IN = "videoin.mp4"
FILENAME_OUT = "videoout.mp4"
CODEC = 'mp4v' 

camera_matrix = np.array([[1.26125746e+03, 0.00000000e+00, 9.40592038e+02],
                          [0.00000000e+00, 1.21705719e+03, 5.96848905e+02],
                          [0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]);
dist_coefs = np.array([-3.18345478e+01, 7.26874187e+02, -1.20480816e-01, 9.43789095e-02, 5.28916586e-01]);

print ("OpenCV version : {0}".format(cv2.__version__))
print((cv2.__version__).split('.'))
# Load video
video = cv2.VideoCapture(FILENAME_IN)

fourcc = cv2.VideoWriter_fourcc(*list(CODEC))

fps = video.get(cv2.CAP_PROP_FPS)

frame_count = video.get(cv2.CAP_PROP_FRAME_COUNT)

size = (int(video.get(cv2.CAP_PROP_FRAME_WIDTH)),
        int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)))
sizew = (1676, 846)
writer = cv2.VideoWriter(FILENAME_OUT, fourcc, 25, sizew)

newcameramtx, roi = cv2.getOptimalNewCameraMatrix(camera_matrix, dist_coefs, (size[0], size[1]), 1, (size[0], size[1]))
x, y, w, h = roi
M = cv2.getRotationMatrix2D((size[0]/2,size[1]/2),5,1)

while video.grab() is True:
    print("On frame %i of %i."%(video.get(cv2.CAP_PROP_POS_FRAMES), frame_count))

    frame = video.retrieve()[1]
    frame = cv2.undistort(frame, camera_matrix, dist_coefs, None, newcameramtx)
    frame = frame[y:y+h-50, x+70:x+w-20]

    writer.write(frame)
video.release()
writer.release()

Комментарии (19)


  1. HerrDonUlt
    28.10.2017 22:24

    pip install opencv-python


    1. Zenker
      29.10.2017 05:07

      Ставить opencv из пипа чревато, там кросплатформенная сборка, в которой не работает, например, cv2.VideoCapture(), причём без всяких сообщений об ошибках, что может подарить незабываемые часы отладки. Могу предположить, что это не единственный такой сюрприз. Поэтому ставить надо, по возможности, из репозитория своего дистрибутива. Собирать самому есть смысл разве что если нужна свежая версия или есть требования к производительности. Вот тут есть немного про оптимизацию под raspberry: www.pyimagesearch.com/2017/10/09/optimizing-opencv-on-the-raspberry-pi


      1. veveve
        29.10.2017 14:05

        там кросплатформенная сборка, в которой не работает, например, cv2.VideoCapture()

        Тоже недавно сталкивался. А вы не в курсе, почему именно не работает? Чем эта сборка отличается?


        1. Zenker
          29.10.2017 16:57

          А это так и задумано, у них на гитхабе об этом прямо написано:

          MacOS and Linux packages do not support video related functionality (not compiled with FFmpeg).


  1. SyavaSyava
    29.10.2017 00:41

    Стесняюсь спросить, а где эта чудесная программа взяла детали, недостающие на фото слева, которые потом добавила на фото справа?
    Если что – речь про коричневый фон вокруг мишени слева в средней части. На левом фото его точно нет…
    Про всё остальное ничего сказать не могу – не понял совершенно ничего… Но это не ваша вина )))


    1. Cram
      29.10.2017 10:04
      +2

      Скорее всего исходное изображение больше, а те которые тут вставлены просто вырезаны.


      1. DenisN03 Автор
        30.10.2017 06:57

        Изображение слева обрезал, чтобы сделать одинаковый размер фотографий.


  1. iroln
    29.10.2017 02:13

    В статье ни слова о теории, об используемом алгоритме коррекции дисторсии.


    А зачем contrib понадобился? Калибровка камеры входит в официальный OpenCV.


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

    Всё тут:
    https://docs.opencv.org/3.1.0/dc/dbb/tutorial_py_calibration.html


    Устанавлиать opencv очень просто:


    pip install opencv-python

    или если нужен contrib


    pip install opencv-contrib-python

    import getopt

    Серьёзо? argparse надо использовать.


    И да, в статье слишком много слова "данный". Ужасное слово. :)


  1. TheShock
    29.10.2017 08:40
    +1

    Сразу привожу фото итогового результата. Слева оригинальное фото, справа — обработанное:

    На хабре принято выкладывать фото на web.habrastorage.org, а не во вконтакте (он у многих заблочен)


    1. DenisN03 Автор
      30.10.2017 06:40
      -1

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


      1. TheShock
        30.10.2017 13:18
        -1

        Что значит "в следующий раз"?? У вас кнопка редактирования отвалилась? Сейчас исправьте, а не оставляйте халтуру навсегда.


        1. DenisN03 Автор
          30.10.2017 18:32

          Перезалил на web.habrastorage.org :)


          1. TheShock
            30.10.2017 20:19

            Супер! Спасибо


  1. kraidiky
    29.10.2017 15:36

    Очень интересно, теперь бы прибавить к этому какой-нибудь путный алгоритм стабилизации изображений и мой склад видео с экшенкамеры перестанет лежать мёртвым грузом. :)


  1. muxa_ru
    29.10.2017 21:15

    Битрейт конечного видео берётся из исходного или нужно указывать отдельно?


    1. DenisN03 Автор
      30.10.2017 06:53
      +1

      Битрейт определяется значением:

      fps = video.get(cv2.CAP_PROP_FPS)


      1. muxa_ru
        30.10.2017 16:37

        а точно это битрейт, а не FPS ?


  1. muxa_ru
    29.10.2017 23:05
    +1

    А есть такой же, но с перламутровыми пуговицами выравнивающий фотографию изогнутых книжных страниц?


    1. Psychopompe
      29.10.2017 23:13
      +1

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