OpenCV\'s remap() uses a real-valued index grid to sample a grid of values from an image using bilinear interpolation, and returns the grid of samples as a new image.
<OP here. I think I've found an answer. I haven't implemented it yet, and if someone comes up with a less fiddly solution (or finds something wrong with this one), I'll choose their answer instead.
Let A be the source image, B be the destination image, and M be the mapping from A's coords to B's coords, i.e.:
B[k, l, :] == A(M[k, l, 0], M[k, l, 1], :)
for all k, l in B's coords.
...where square braces indicate array lookup with integer indices, and circular braces indicate bilinear interpolation lookup with floating-point indices. We restate the above using the more economical notation:
B = A(M)
We wish to find an inverse mapping N that maps B back to A as best as is possible:
Find N s.t. A \approx B(N)
The problem can be stated without reference to A or B:
Find N = argmin_N || M(N) - I_n ||
...where ||*||
indicates the Frobenius norm, and I_n
is the identity map with the same dimensions as N, i.e. a map where:
I_n[i, j, :] == [i, j]
for all i, j
If M's values are all integers, and M is an isomorphism, then you can construct N directly as:
N[M[k, l, 0], M[k, l, 1], :] = [k, l]
for all k, l
Or in our simplified notation:
N[M] = I_m
...where I_m is the identity map with the same dimensions as M.
There are two problems:
Construct empty N as a 3D tensor of floats:
N = zeros(size=(A.shape[0], A.shape[1], 2))
For each coordinate [i, j] in A's coordinate space, do:
The potentially expensive step here would be the search in step 1 for the 2x2 grid of A-coordinates in M that encircles [i, j]. A brute-force search would make this whole algorithm O(n*m) where n is the number of pixels in A, and m the number of pixels in B.
To reduce this to O(n), one could instead run a scanline algorithm within each A-coordinate quadrilateral to identify all the integer-valued coordinates [i, j] it contains. This could be precomputed as a hashmap that maps integer-valued A coords [i, j] to the upper-left corner of its encircling quadrilateral's B coords [k, l].
If you map is derived from a homography H
you could invert H
and directly create the inverse maps with cv::initUndistortRectifyMap()
.
e.g. in 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)
The OpenCV documentation states about initUndistortRectifyMap()
:
The function actually builds the maps for the inverse mapping algorithm that is used by
remap()
. That is, for each pixel (u, v) in the destination image, the function computes the corresponding coordinates in the source image.
In the case you have just given the maps, you have to do it by yourself. Hoewever, interpolation of the new maps' coordinates is not trivial, because the support region for one pixel could be very large.
Here is a simple Python solution which inverts the maps by doing point-to-point mapping. This will probably leave some coordinates unassigned, while others will be updated several times. So there may be holes in the map.
Here is a small Python program demonstrating both approaches:
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()
Here's an implementation of @wcochran 's answer. I was trying to recover a lens correction resulted by lensfunpy.
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)
Well I just had to solve this remap inversion problem myself and I'll outline my solution.
Given X
, Y
for the remap()
function that does the following:
B[i, j] = A(X[i, j], Y[i, j])
I computed Xinv
, Yinv
that can be used by the remap()
function to invert the process:
A[x, y] = B(Xinv[x,y],Yinv[x,y])
First I build a KD-Tree for the 2D point set {(X[i,j],Y[i,j]}
so I can efficiently find the N
nearest neighbors to a given point (x,y).
I use Euclidian distance for my distance metric. I found a great C++ header lib for KD-Trees on GitHub.
Then I loop thru all the (x,y)
values in A
's grid and find the N = 5
nearest neighbors {(X[i_k,j_k],Y[i_k,j_k]) | k = 0 .. N-1}
in my point set.
If distance d_k == 0
for some k
then Xinv[x,y] = i_k
and Yinv[x,y] = j_k
, otherwise...
Use Inverse Distance Weighting (IDW) to compute an interpolated value:
w_k = 1 / pow(d_k, p)
(I use 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)
Note that if B
is a W x H
image then X
and Y
are W x H
arrays of floats. If A
is a w x h
image then Xinv
and Yinv
are w x h
arrays for floats. It is important that you are consistent with image and map sizing.
Works like a charm! My first version I tried brute forcing the search and I never even waited for it to finish. I switched to a KD-Tree then I started to get reasonable run times. I f I ever get time I would like to add this to OpenCV.
The second image below is use remap()
to remove the lens distortion from the first image. The third image is a result of inverting the process.
There is no any standard way to do it with OpenCV.
If you are looking for a complete ready-to-use solution, I am not sure that I can help, but I can at least describe a method that I used some years ago to do this task.
First of all, you should create remapping maps with the same dimension as your source image. I created maps with larger dimensions for simpler interpolation, and at final step cropped them to proper size. Then you should fill them with values existing in previous remapping maps (not so difficult: just iterate over them and if maps coordinates x and y lays in limits of your image, take their row and column as new y and x, and place into old x and y column and row of the new map). It is rather simple solution,but it gives rather good result. For perfect one you should interpolate old x and y to integer values using your interpolation method and neighbour pixels.
After this you should either actually remap pixel colors manually, or completely fill your remapping map with pixel coordinates and use version from OpenCV.
You will meet rather challenging task: you should interpolate pixels in empty areas. In other words, you should take distances to closest non-zero pixel coordinates and mix color (if you remap colors) or coordinates (if you proceed with full maps computation) fractions according to these distances. Actually it is also not so difficult for linear interpolation, and you can even look into remap()
implementation in OpenCV github page. For NN interpolation it will me much simpler - just take color/coordinate of nearest neighbour.
And a final task is extrapolation of areas out of borders of remapped pixels area. Also algorithm from OpenCV can be used as a reference.
From what I understand you have an original image, and a transformed image, and you wish to recover the nature of the transform that has been applied without knowing it, but assuming it is something sensible, like a rotation or a fish-eye distort.
What I would try is thresholding the image to convert it to binary, in both the index image and the plain image. Then try to identify objects. Most mappings will at least retain connectivity and Euler number, mostly the largest object in the index will still be the largest object in the plain.
Then take moments for your matched image / indexed pairs and see if you can remove translation, rotation and scaling. That gives you several reverse maps, which you can then try to stitch together. (Hard if the transform is not simple, but the general problem of reconstituting just any transformation cannot be solved).