圖像旋轉后出現黑點 - (二) - 填坑


前接:圖像旋轉后出現黑點 - (一) - 入坑

這是填坑篇,之前寫的圖片旋轉程序把圖片變成了桌布,幾個世紀后,在一個月黑風高的夜晚,我靈光乍現,何不試試雙線性插值?

先上代碼和效果圖。

 1 # !/usr/bin/env python3
 2 # -*-coding:utf-8-*-
 3 """ 
 4 雙線性插值參考資料: 雙線性插值原理及Python實現 - Jinglever  https://www.jianshu.com/p/29e5c84ea539
 5 
 6 如果出現錯誤:...If you are on Ubuntu or Debian, install libgtk2.0-dev and pkg-config
 7 執行 pip3 install opencv-contrib-python
 8 """
 9 import numpy as np
10 # np.set_printoptions(suppress=True)    # 關閉科學計數法
11 import cv2
12 import os
13 
14 
15 # 旋轉矩陣R
16 ANGLE = 30  # (dim=°)
17 assert 0 < ANGLE < 90   # 目前限制這個旋轉范圍,原因是y1, y2, y3, y4上下關系根據角度變化
18 alpha = ANGLE/360*2*np.pi
19 R_rev = np.matrix([[np.cos(alpha), np.sin(alpha)],      # 逆向映射推導的旋轉矩陣
20                 [-np.sin(alpha), np.cos(alpha)]])
21 print(R_rev)
22 
23 # 重設圖片大小
24 WIDTH, HEIGHT = 640, 480
25 
26 img = cv2.imread("timg.jpg")
27 img = cv2.resize(img, (WIDTH, HEIGHT))
28 # img_gray = np.float32(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY))
29 img = np.float32(img)
30 print(img.shape)
31 
32 # 假設已經得到旋轉后的圖片,利用圖片邊框畫出圖片的矩形,在矩形內遍歷坐標就是圖片各個像素點的坐標
33 # 注意旋轉角度超過90度后邊框線的上下關系會發生變化,待改進……
34 x = np.arange(np.abs(WIDTH*np.cos(alpha)) + np.abs(HEIGHT*np.sin(alpha)), dtype=np.int32)
35 y1 = lambda x: (- x*np.tan(alpha)).astype(np.int32)
36 y2 = lambda x: (y1(x) + HEIGHT/np.cos(alpha)).astype(np.int32)
37 y3 = lambda x: (x/np.tan(alpha)).astype(np.int32)
38 y4 = lambda x: (y3(x) - WIDTH/np.sin(alpha)).astype(np.int32)
39 # 用矩形下面2條線(的最大值)確定y坐標最小值,上面2條線(的最小值)確定y坐標最大值
40 y_min = np.max(np.concatenate((y1(x).reshape(1, -1), y4(x).reshape(1, -1))), axis=0)
41 y_max = np.min(np.concatenate((y2(x).reshape(1, -1), y3(x).reshape(1, -1))), axis=0)
42 # 計算旋轉后圖片各像素點坐標
43 pre_index = [np.array((yi, xi)).reshape(-1, 1) for xi in x for yi in range(y_min[xi], y_max[xi]+1)]
44 
45 ori_index = np.array(list(map(R_rev.dot, pre_index))).reshape(-1, 2)     # 坐標變換到原圖
46 hs_p, ws_p = np.hsplit(ori_index, 2) # 分離y, x坐標
47 
48 ws_p = np.clip(ws_p, 0, WIDTH-1)  # 限制坐標最值防止越界
49 hs_p = np.clip(hs_p, 0, HEIGHT-1)
50 
51 ws_0 = np.clip(np.floor(ws_p), 0, WIDTH - 2).astype(np.int)     # 找出每個投影點在原圖的近鄰點坐標
52 hs_0 = np.clip(np.floor(hs_p), 0, HEIGHT - 2).astype(np.int)
53 ws_1 = ws_0 + 1
54 hs_1 = hs_0 + 1
55 
56 f_00 = img[hs_0, ws_0, :].T    # 四個臨近點的像素值
57 f_01 = img[hs_0, ws_1, :].T
58 f_10 = img[hs_1, ws_0, :].T
59 f_11 = img[hs_1, ws_1, :].T
60 
61 w_00 = ((hs_1 - hs_p) * (ws_1 - ws_p)).T    # 計算權重
62 w_01 = ((hs_1 - hs_p) * (ws_p - ws_0)).T
63 w_10 = ((hs_p - hs_0) * (ws_1 - ws_p)).T
64 w_11 = ((hs_p - hs_0) * (ws_p - ws_0)).T
65 
66 pixels = (f_00 * w_00).T + (f_01 * w_01).T + (f_10 * w_10).T + (f_11 * w_11).T  # 計算目標像素值
67 
68 y_new, x_new = np.hsplit(np.array(pre_index).reshape(-1, 2), 2) # # 分離y, x坐標
69 y_new = y_new - np.min(y_new)      # y坐標平移,防止圖片旋轉后被窗口切分
70 
71 h, w = np.max(y_new), np.max(x_new)    # 旋轉后畫布大小
72 # 像素映射 原始→新圖
73 new_img = np.zeros((h+1, w+1, img.shape[2]))    # (H, W, C)
74 new_img[y_new, x_new, :] = pixels   # 填充像素
75 
76 cv2.imwrite('./AffinedImg.jpg', new_img, [int(cv2.IMWRITE_JPEG_QUALITY),95])
77 # 顯示圖片
78 cv2.imshow('img', np.array(new_img, dtype=np.uint8))
79 cv2.waitKey(0)
80 cv2.destroyAllWindows()

原圖見入坑篇

下面是運行結果,這次我換成了彩色的:

 

雙線性插值常用於圖像的比例縮放,基本原理很容易搜索到,這里就不多說了,重點講一下怎么把它應用到圖像旋轉上來。

假設輸入圖片是 input image,輸出圖片是 output image,首先回顧一下雙線性插值的思路:坐標的變換是反着來的,從 output image 到 input image。即 output image 對應的整數坐標,縮放變換到 input image 后,變成浮點數坐標,然后取它4個角上的點,計算浮點數坐標的顏色,填充到 output image 對應的坐標那里。

再說回圖像旋轉,之前出現黑點就是因為圖像的變換是從 input image 到 output image,即每個 input image 的像素坐標用旋轉矩陣算到 output image 上,然后把浮點數直接量化成整數,這樣就引入了量化誤差,個別輸出的坐標就錯位了,導致有黑點(本來在黑點位置的像素因為坐標錯位到別的地方去了,黑點那里就沒有顏色數據了)。

應用雙線性插值的解決思路:先得到 output image 對應的整數坐標,變換到 input image 后,變成浮點數坐標,然后取它4個角上的點,計算浮點數坐標的顏色,填充到 output image 對應的坐標那里。(跟上面那句一樣)

那么,實現過程就分為以下幾步:

1. 獲取 output image 對應各個像素點坐標。

  1) 假設已經得到 output image,這張圖片是旋轉一定角度的,俗話說就是斜着的,但是坐標系是正着的,怎么得到像素坐標?

2. 坐標映射:使用反着轉的旋轉矩陣(R_rev)把 output image 的坐標轉到 input image 上,這個結果算出來是浮點數。

3. 雙線性插值:取浮點數坐標4個角上的點,計算浮點數坐標的顏色,然后填充回 output image。大功告成!

 

首先回答問題 1) :

我使用了一個很簡單的方法,就是靠圖像邊框作為邊界,框出圖像的矩形區域,遍歷里面的所有點。

$y_{1}=-x\cdot \tan \left ( \alpha  \right )$

$y_{2}=-x\cdot \tan \left ( \alpha  \right ) + \frac {HEIGHT}{\cos \left ( \alpha  \right )}$

$y_{3}=\frac{x}{\tan \left ( \alpha  \right )}$

$y_{4}=\frac{x}{\tan \left ( \alpha  \right )}-\frac{WIDTH}{\sin \left ( \alpha  \right )}$

上圖繪制了y1, y2, y3, y4四條直線,注意圖片顯示的坐標,y軸正方向朝下。如圖所示,y1, y2, y3, y4是圖片的邊框線,標號是我自己隨便標的,如果旋轉角度在90度內,邊框線的上下關系不變(y2, y3在上,y1, y4在下,注意y軸正方向朝下)。這也就是現在這個程序只能實現90度以內旋轉的原因,如果要繼續旋轉,例如旋轉120度時,就變成 y1, y3 在上,y2, y4 在下,需要修改程序。

 

然后是遍歷圖片坐標:

 

 

  如圖所示,從點 (0, 0) 開始,按照箭頭方向逐列遍歷圖片坐標,保存到 pre_index 中。 對應代碼:(理解注釋里的上下關系的時候,仍然要記得y軸正方向朝下!)

# 假設已經得到旋轉后的圖片,利用圖片邊框畫出圖片的矩形,在矩形內遍歷坐標就是圖片各個像素點的坐標
# 注意旋轉角度超過90度后邊框線的上下關系會發生變化,待改進……
x = np.arange(np.abs(WIDTH*np.cos(alpha)) + np.abs(HEIGHT*np.sin(alpha)), dtype=np.int32)
y1 = lambda x: (- x*np.tan(alpha)).astype(np.int32)
y2 = lambda x: (y1(x) + HEIGHT/np.cos(alpha)).astype(np.int32)
y3 = lambda x: (x/np.tan(alpha)).astype(np.int32)
y4 = lambda x: (y3(x) - WIDTH/np.sin(alpha)).astype(np.int32)
# 用矩形下面2條線(的最大值)確定y坐標最小值,上面2條線(的最小值)確定y坐標最大值
y_min = np.max(np.concatenate((y1(x).reshape(1, -1), y4(x).reshape(1, -1))), axis=0)
y_max = np.min(np.concatenate((y2(x).reshape(1, -1), y3(x).reshape(1, -1))), axis=0)
# 計算旋轉后圖片各像素點坐標
pre_index = [np.array((yi, xi)).reshape(-1, 1) for xi in x for yi in range(y_min[xi], y_max[xi]+1)]

到這里第1步就完成了。

然后是第2步,坐標映射。

# R = np.matrix([[np.cos(alpha), -np.sin(alpha)],
#                 [np.sin(alpha), np.cos(alpha)]])
R_rev = np.matrix([[np.cos(alpha), np.sin(alpha)],      # 逆向映射推導的旋轉矩陣
                [-np.sin(alpha), np.cos(alpha)]])

按照推導正向旋轉矩陣的方法反推逆向旋轉矩陣,就可以得到上面的結果。如果仍然難以理解,就當做反轉(alpha = -alpha)

ori_index = np.array(list(map(R_rev.dot, pre_index))).reshape(-1, 2)     # 坐標變換到原圖

ori_index 里的坐標全部是根據 pre_index 計算來的,並不是從原圖上面取點。這里計算出來的 ori_index 數據類型是浮點數。

然后是第3步,雙線性插值和像素填充。

從 ori_index 開始直到計算出來 pixels 就是雙線性插值的過程了,實現原理可以參考一下參考資料。

之后是像素填充:

y_new, x_new = np.hsplit(np.array(pre_index).reshape(-1, 2), 2) # # 分離y, x坐標
y_new = y_new - np.min(y_new)      # y坐標平移,防止圖片旋轉后被窗口切分

h, w = np.max(y_new), np.max(x_new)    # 旋轉后畫布大小
# 像素映射 原始→新圖
new_img = np.zeros((h+1, w+1, img.shape[2]))    # (H, W, C)
new_img[y_new, x_new, :] = pixels   # 填充像素

需要注意的是,旋轉后的圖片有一部分的坐標值是負值,實際顯示的時候如果輸入負坐標,圖片會被分開顯示,所以把旋轉后的圖片朝y軸正方向平移,移到所有點坐標值都大於0的地方。

現在 pixels 里已經計算出來旋轉后圖片所有點的像素值,像素點數據的排列方向和 pre_index 是相同的,所以直接把對應的點賦值就可以了。

最后的圖片就是 new_img,插值效果還是很不錯的。;-)

 

參考資料:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM