Инвертирование вещественной индексной сетки

OpenCV remap() использует вещественную индексную сетку для выборки сетки значений из изображения с использованием билинейной интерполяции и возвращает сетку выборок в качестве нового изображения.

Чтобы быть точным, позвольте:

A = an image 
X = a grid of real-valued X coords into the image. 
Y = a grid of real-valued Y coords into the image.
B = remap(A, X, Y)

Тогда для всех координат пикселей i, j,

B[i, j] = A(X[i, j], Y[i, j]) 

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

Мой вопрос: учитывая индексную сетку X, Y, как я могу сгенерировать "обратную сетку" X^-1, Y^-1 такую, что:

X(X^-1[i, j], Y^-1[i, j]) = i
Y(X^-1[i, j], Y^-1[i, j]) = j

А также

X^-1(X[i, j], Y[i, j]) = i
Y^-1(X[i, j], Y[i, j]) = j

Для всех целочисленных пиксельных координат i, j?

FWIW, изображения и индексные карты X и Y имеют одинаковую форму. Однако в индексных картах X и Y нет априорной структуры. Например, они не обязательно являются аффинными или жесткими преобразованиями. Они могут даже быть необратимыми, например, если X, Y отображает несколько пикселей в A на одну и ту же точную координату пикселей в B. Я ищу идеи для метода, который найдет разумную обратную карту, если она существует.

Решение не обязательно должно быть основано на OpenCV, так как я не использую OpenCV, а другую библиотеку, которая имеет реализацию remap(). Хотя любые предложения приветствуются, я особенно заинтересован в том, чтобы "математически исправить", то есть, если моя карта M является полностью обратимой, метод должен найти идеальное обратное, с небольшим пределом точности машины.

Ответ 1

Ну, мне просто нужно было решить эту проблему инверсии переназначения, и я обрисую свое решение.

Учитывая X, Y для функции remap(), которая выполняет следующие действия:

B[i, j] = A(X[i, j], Y[i, j])   

Я вычислил Xinv, Yinv, которые могут быть использованы функцией remap() для инвертирования процесса:

A[x, y] = B(Xinv[x,y],Yinv[x,y])

Сначала я строю KD-дерево для набора 2D-точек {(X[i,j],Y[i,j]}, чтобы я мог эффективно найти N ближайших соседей к заданной точке (x,y).. Я использую евклидово расстояние для своей метрики расстояния, Я нашел отличную библиотеку заголовков C++ для KD-Trees на GitHub.

Затем я перебираю все значения (x,y) в сетке A и нахожу ближайших соседей N = 5 {(X[i_k,j_k],Y[i_k,j_k]) | k = 0 .. N-1} в моем наборе точек.

  • Если расстояние d_k == 0 для некоторого k, то Xinv[x,y] = i_k и Yinv[x,y] = j_k, в противном случае...

  • Используйте Обратное взвешивание расстояния (IDW) для вычисления интерполированного значения:

    • пусть весит w_k = 1 / pow(d_k, p) (я использую p = 2)
    • Xinv[x,y] = (sum_k w_k * i_k)/(sum_k w_k)
    • Yinv[x,y] = (sum_k w_k * j_k)/(sum_k w_k)

Обратите внимание, что если B является изображением W x H, то X и Y являются массивами W x H с плавающей точкой. Если A является изображением w x h, то Xinv и Yinv являются массивами w x h для поплавков. Важно, чтобы вы соответствовали размеру изображения и карты.

Работает как шарм! В своей первой версии я попробовал перебор поиска и даже не дождался его завершения. Я переключился на KD-Tree, затем начал получать разумные времена выполнения. Если бы у меня было время, я бы хотел добавить это в OpenCV.

Второе изображение ниже использует remap() для удаления искажения объектива с первого изображения. Третье изображение - результат инверсии процесса.

enter image description here enter image description here enter image description here

Ответ 2

Если ваша карта является производной от гомографии H вы можете инвертировать H и напрямую создавать обратные карты с помощью cv::initUndistortRectifyMap().

например, в Python:

import numpy as np.
map_size = () # fill in your map size
H_inv = np.linalg.inv(H)
map1, map2 = cv2.initUndistortRectifyMap(cameraMatrix=np.eye(3), distCoeffs=np.zeros(5), R=H_inv, newCameraMatrix=np.eye(3), size=map_size, m1type=cv2.CV_32FC1)

В документации OpenCV говорится о initUndistortRectifyMap():

Функция фактически строит карты для алгоритма обратного отображения, который используется remap(). То есть для каждого пикселя (u, v) в целевом изображении функция вычисляет соответствующие координаты в исходном изображении.

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

Вот простое решение Python, которое инвертирует карты, выполняя двухточечное отображение. Это, вероятно, оставит некоторые координаты неназначенными, в то время как другие будут обновлены несколько раз. Так что на карте могут быть дыры.

Вот небольшая программа на Python, демонстрирующая оба подхода:

import cv2
import numpy as np


def invert_maps(map_x, map_y):
    assert(map_x.shape == map_y.shape)
    rows = map_x.shape[0]
    cols = map_x.shape[1]
    m_x = np.ones(map_x.shape, dtype=map_x.dtype) * -1
    m_y = np.ones(map_y.shape, dtype=map_y.dtype) * -1
    for i in range(rows):
        for j in range(cols):
            i_ = round(map_y[i, j])
            j_ = round(map_x[i, j])
            if 0 <= i_ < rows and 0 <= j_ < cols:
                m_x[i_, j_] = j
                m_y[i_, j_] = i
    return m_x, m_y


def main():
    img = cv2.imread("pigeon.png", cv2.IMREAD_GRAYSCALE)

    # a simply rotation by 45 degrees
    H = np.array([np.sin(np.pi/4), -np.cos(np.pi/4), 0, np.cos(np.pi/4), np.sin(np.pi/4), 0, 0, 0, 1]).reshape((3,3))
    H_inv = np.linalg.inv(H)
    map_size = (img.shape[1], img.shape[0])

    map1, map2 = cv2.initUndistortRectifyMap(cameraMatrix=np.eye(3), distCoeffs=np.zeros(5), R=H, newCameraMatrix=np.eye(3), size=map_size, m1type=cv2.CV_32FC1)
    map1_inv, map2_inv = cv2.initUndistortRectifyMap(cameraMatrix=np.eye(3), distCoeffs=np.zeros(5), R=H_inv, newCameraMatrix=np.eye(3), size=map_size, m1type=cv2.CV_32FC1)
    map1_simple_inv, map2_simple_inv = invert_maps(map1, map2)

    img1 = cv2.remap(src=img, map1=map1, map2=map2, interpolation=cv2.INTER_LINEAR)
    img2 = cv2.remap(src=img1, map1=map1_inv, map2=map2_inv, interpolation=cv2.INTER_LINEAR)
    img3 = cv2.remap(src=img1, map1=map1_simple_inv, map2=map2_simple_inv,
                               interpolation=cv2.INTER_LINEAR)

    cv2.imshow("Original image", img)
    cv2.imshow("Mapped image", img1)
    cv2.imshow("Mapping forth and back with H_inv", img2)
    cv2.imshow("Mapping forth and back with invert_maps()", img3)
    cv2.waitKey(0)


if __name__ == '__main__':
    main()

Ответ 3

Нет стандартного способа сделать это с помощью OpenCV.

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

Прежде всего, вы должны создать переназначение карт с тем же размером, что и исходное изображение. Я создал карты с большими размерами для более простой интерполяции, а на последнем этапе обрезал их до нужного размера. Затем вы должны заполнить их значениями, существующими в предыдущих картах переопределения (не так сложно: просто перебирайте их и если координаты координат x и y лежат в пределах вашего изображения, возьмите их строку и столбец как новые y и x и поместите в старые x и y и строка новой карты). Это довольно простое решение, но оно дает довольно хороший результат. Для идеального вы должны интерполировать старые значения x и y на целочисленные значения с помощью метода интерполяции и соседних пикселей.

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

Вы столкнетесь с довольно сложной задачей: вы должны интерполировать пиксели в пустых областях. Другими словами, вы должны принимать расстояния до ближайших ненулевых координат пикселов и смешивать цвет (если вы переназначаете цвета) или координаты (если вы выполняете вычисления с полным отображением) в соответствии с этими расстояниями. На самом деле это также не так сложно для линейной интерполяции, и вы даже можете взглянуть на реализацию remap() в OpenCV github page. Для интерполяции NN это намного проще - просто возьмите цвет/координату ближайшего соседа.

И конечной задачей является экстраполяция областей вне границ области перепрограммированных пикселей. Также можно использовать алгоритм OpenCV в качестве ссылки.

Ответ 4

Вот реализация ответа @wcochran. Я пытался восстановить коррекцию объектива, вызванную функцией объектива.

mod = lensfunpy.Modifier(lens, cam.crop_factor, width, height)
mod.initialize(focal_length, aperture, distance)

undist_coords = mod.apply_geometry_distortion()

## the lens correction part
# im_undistorted = cv2.remap(im, undist_coords, None, cv2.INTER_CUBIC)

# im_undistorted = cv2.remap(im, undist_coords, None, cv2.INTER_LANCZOS4)
# cv2.imwrite(undistorted_image_path, im_undistorted)
undist_coords_f = undist_coords.reshape((-1, 2))
tree = KDTree(undist_coords_f)
def calc_val(point_pos):
    nearest_dist, nearest_ind = tree.query([point_pos], k=5)
    if nearest_dist[0][0] == 0:
        return undist_coords_f[nearest_ind[0][0]]
    # starts inverse distance weighting
    w = np.array([1.0 / pow(d, 2) for d in nearest_dist])
    sw = np.sum(w)
    # embed()
    x_arr = np.floor(nearest_ind[0] / 1080)
    y_arr = (nearest_ind[0] % 1080)
    xx = np.sum(w * x_arr) / sw
    yy = np.sum(w * y_arr) / sw
    return (xx, yy)

un_correction_x = np.zeros((720, 1080))
un_correction_y = np.zeros((720, 1080))

## reverse the lens correction
for i in range(720):
    print("row %d operating" % i)
    for j in range(1080):
        un_correction_x[i][j], un_correction_y[i][j] = calc_val((i, j))
        # print((i, j), calc_val((j, i)))

dstMap1, dstMap2 = cv2.convertMaps(un_correction_x.astype(np.float32), un_correction_y.astype(np.float32), cv2.CV_32FC2)
im_un_undistorted = cv2.remap(im_undistorted, dstMap1, dstMap2, cv2.INTER_LANCZOS4)

Ответ 5

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

Вот очень простая реализация в Python с использованием scipy.interpolate.griddata:

map_x, map_y = cv2.initUndistortRectifyMap(K, D, None, new_K, image_size, cv2.CV_32FC1)

points =  np.stack([map_x.flatten(), map_y.flatten()], axis=1)
grid = np.mgrid[:map_x.shape[0], :map_y.shape[1]]
values = grid.reshape(2, -1).T[..., ::-1] 

from scipy.interpolate import griddata
grid_y, grid_x = grid
map_back = griddata(points, values, (grid_x, grid_y), method='cubic').astype(map_undistort.dtype)

Если вы используете CV_32FC2 для карт, вы можете упростить построение точек:

map_undistort, _ = cv2.initUndistortRectifyMap(K, D, None, new_K, image_size, cv2.CV_32FC2)
points = map_undistort.reshape(-1, 2)

Ответ 6

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

Что бы я попробовал, это пороговое изображение, чтобы преобразовать его в двоичный, как в индексное изображение, так и в обычное изображение. Затем попытайтесь определить объекты. Большинство сопоставлений, по крайней мере, сохраняют связь и число Эйлера, в основном самый большой объект в индексе будет по-прежнему самым большим объектом в равнине.

Затем возьмите моменты для совпадающих изображений/индексированных пар и посмотрите, можете ли вы удалить перевод, вращение и масштабирование. Это дает вам несколько обратных карт, которые вы можете попытаться сшить вместе. (Жестко, если преобразование непросто, но общая проблема воссоздания только какого-либо преобразования не может быть решена).

Ответ 7

OP здесь. Кажется, я нашел ответ. Я не реализовал еще, и если кто-то приходит с менее неудобным раствором (или находит что-то неправильно с этим), я выберу свой ответ вместо этого.

Заявление о проблемах

Пусть A - исходное изображение, B - целевое изображение, а M - отображение из A-коордов в B-коорды, т.е.

B[k, l, :] == A(M[k, l, 0], M[k, l, 1], :) 
for all k, l in B coords.

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

B = A(M)

Мы хотим найти обратное отображение N, которое наилучшим образом отображает B обратно в A:

Find N s.t. A \approx B(N)

Проблема может быть указана без ссылки на A или B:

Find N = argmin_N || M(N) - I_n ||

... где ||*|| указывает норму Фробениуса, а I_n - тождественное отображение с теми же размерами, что и N, т.е. отображение, где:

I_n[i, j, :] == [i, j]
for all i, j

Наивное решение

Если значения M - это целые числа, а M - изоморфизм, то вы можете построить N непосредственно как:

N[M[k, l, 0], M[k, l, 1], :] = [k, l]
for all k, l

Или в наших упрощенных обозначениях:

N[M] = I_m

... где I_m - тождественное отображение с теми же размерами, что и M.

Есть две проблемы:

  • M не является изоморфизмом, поэтому приведенное выше будет оставлять "дырки" в N при N [i, j::] для любого [i, j] не среди значений в M.
  • Значения M - это координаты с плавающей запятой [i, j], а не целые координаты. Мы не можем просто присвоить значение билинейно-интерполированной величине N (i, j,:) для float-значных i, j. Для достижения эквивалентного эффекта мы должны вместо этого установить значения [i, j] четырех окружающих углов N [floor (i), floor (j),:], N [floor (i), ceil (j),:], N [ceil (i), floor (j),:], N [ceil (i), ceil (j),:] такие, что интерполированное значение N (i, j,:) равно искомому значению [k, l], для всех отображений пикселей [i, j] → [k, l] в M.

Решение

Построить пустой N как трехмерный тензор поплавков:

N = zeros(size=(A.shape[0], A.shape[1], 2))

Для каждой координаты [i, j] в A-координатном пространстве do:

  • Найдите сетку 2x2 A-координат в M, которая [i, j] находится внутри. Вычислите матрицу гомографий H, которая отображает эти A-координаты в соответствующие B-координаты (заданные индексами пикселя сетки 2x2).
  • Установите N [i, j,:] = matmul (H, [i, j])

Потенциально дорогостоящим шагом здесь будет поиск на шаге 1 для сетки 2x2 A-координат в M, которая окружает [i, j]. Поиск грубой силы сделает весь этот алгоритм O (n * m), где n - количество пикселей в A, а m - количество пикселей в B.

Чтобы уменьшить это до O (n), вместо этого можно было бы запустить алгоритм сканирующей линии в каждом четырехграннике A-координаты, чтобы идентифицировать все целочисленные значения [i, j], которые он содержит. Это можно было бы предварительно вычислить как хэш-карту, которая отображает целочисленные значения A-коордов [i, j] в верхний левый угол его окружных четырехсторонних B-коордов [k, l].