最近在學習圖像的知識,使用到了圖像旋轉,所以自己學了一下圖像旋轉的原理,自己用python寫了一遍。
這里用到的知識有圖像旋轉和雙線性插值法,這兩篇是我參考的文章:圖像旋轉算法原理、圖像處理之雙線性插值法。
簡單介紹一下圖像旋轉的過程:1.首先將圖像坐標系轉換為數學坐標系。2.使用旋轉公式對坐標進行旋轉。3.將旋轉后的數學坐標系轉換為圖像坐標系。
其中x,y是轉換后的坐標,x0,y0是原始的坐標,θ是旋轉的角度;第一個矩陣是將圖像坐標系轉換為數學坐標系,W和H分別為圖像的寬高;第二個矩陣為旋轉公式;第三個公式是將數學坐標系轉換為圖像坐標系,W’和H'分別是新圖像的寬高。這里的轉換應該是使用了齊次坐標,至於為什么,我也不是很理解。
得到坐標的轉換公式如下(公式一)。
通過公式一,就可以從原始的圖像坐標獲得轉換之后的圖像坐標。
而另一個圖像轉換的過程是通過轉換后的坐標獲得原始圖像的坐標,圖像旋轉的過程:1.將圖像坐標轉換為數學坐標。2.使用圖像旋轉的逆公式。3.將數學坐標轉換為圖像坐標。
這個和上面的公式對比一下,就是第一個矩陣將W和H換成了W‘和H';上面那條公式第二個矩陣其實是順時針旋轉的,而這里的是逆時針旋轉的;第三條公式將W’和H'換成了W和H。
得到坐標的轉換公式如下。(公式二)
這里就可以通過轉換后的圖像坐標獲得對應的原始圖像的坐標。
有了公式,那只要解出未知數的值就可以套用了,其實主要還是求新圖像的寬高,因為圖像旋轉之后,需要一個更大的新圖像來裝原圖像。
旋轉的主要情況分為上面4種情況,旋轉了[0,90°),[90°,180°),[180°,270°),[270°,360°),黑色坐標軸為原始圖像坐標軸,紅色為旋轉后的圖像坐標軸。注意圖二和圖四的寬高相對於圖一和圖三的寬高是變了位置的。(ps:圖片是我最喜歡的球員,東契奇)
以第一張圖為例,新圖像的長和寬計算公式是。四個角度相當於θ-0×90°,θ-1×90°,θ-2×90°,θ-3×90°,計算結果可以用θ%90°來得到。但是因為圖二和圖四的寬高已經變換位置了,為了使用同一條計算新圖像寬高的公式,圖二圖四可以用(90°-(θ%90))得到另一個角的度數,來計算新圖像的寬高,只需要計算θ%90°的結果,如果是奇數(1or3)就求另一個角的讀書。(想不到其他簡便的方法了)
得到新圖像的寬高后,就可以寫代碼了,下面是代碼部分。
計算新圖像的寬高,因為numpy庫里面sin和cos的參數是弧度,所以需要進行角度轉弧度。
if int(angle / 90) % 2 == 0: reshape_angle = angle % 90 else: reshape_angle = 90 - (angle % 90) reshape_radian = math.radians(reshape_angle) # 角度轉弧度 # 三角函數計算出來的結果會有小數,所以做了向上取整的操作。 new_height = math.ceil(height * np.cos(reshape_radian) + width * np.sin(reshape_radian)) new_width = math.ceil(width * np.cos(reshape_radian) + height * np.sin(reshape_radian))
首先是使用公式一進行前向映射生成新的圖像。前向映射就是通過原圖像的坐標計算新圖像的坐標,再把對應的像素賦值過去。
radian = math.radians(angle) cos_radian = np.cos(radian) sin_radian = np.sin(radian) dx = 0.5 * new_width + 0.5 * height * sin_radian - 0.5 * width * cos_radian dy = 0.5 * new_height - 0.5 * width * sin_radian - 0.5 * height * cos_radian for y0 in range(height): for x0 in range(width): x = x0 * cos_radian - y0 * sin_radian + dx y = x0 * sin_radian + y0 * cos_radian + dy new_img[int(y) - 1, int(x) - 1] = img[int(y0), int(x0)] # 因為整體映射的結果會比偏移一個單位,所以這里x,y做減一操作。
前向映射效果圖
可以看到,新圖像里面很多像素都沒有填充到,因為前向映射算出來的結果有小數,不能一一映射到新圖像的每個坐標上。
下面是使用公式二,進行后向映射,后向映射就是通過新圖像的每個坐標點找到原始圖像中對應的坐標點,再把像素賦值上去。
dx_back = 0.5 * width - 0.5 * new_width * cos_radian - 0.5 * new_height * sin_radian dy_back = 0.5 * height + 0.5 * new_width * sin_radian - 0.5 * new_height * cos_radian for y in range(new_height): for x in range(new_width): x0 = x * cos_radian + y * sin_radian + dx_back y0 = y * cos_radian - x * sin_radian + dy_back if 0 < int(x0) <= width and 0 < int(y0) <= height: # 計算結果是這一范圍內的x0,y0才是原始圖像的坐標。 new_img[int(y), int(x)] = img[int(y0) - 1, int(x0) - 1] # 因為計算的結果會有偏移,所以這里做減一操作。
后向映射效果圖
可以看到使用后向映射,新圖像的每個坐標都有像素值。
最后使用填充像素的方法是后向映射+雙線性插值法,后向映射得到對應的坐標后,取得坐標最近的四個真實的坐標點,分別乘上不同的權重再求和,就得到了賦值的像素。具體的原理可以看文章開頭給的鏈接。代碼如下(channel是圖像的通道數,最下面完整的代碼會給出聲明的地方)。
if channel: fill_height = np.zeros((height, 2, channel), dtype=np.uint8) fill_width = np.zeros((2, width + 2, channel), dtype=np.uint8) else: fill_height = np.zeros((height, 2), dtype=np.uint8) fill_width = np.zeros((2, width + 2), dtype=np.uint8) img_copy = img.copy() # 因為雙線性插值需要得到x+1,y+1位置的像素,映射的結果如果在最邊緣的話會發生溢出,所以給圖像的右邊和下面再填充像素。 img_copy = np.concatenate((img_copy, fill_height), axis=1) img_copy = np.concatenate((img_copy, fill_width), axis=0) dx_back = 0.5 * width - 0.5 * new_width * cos_radian - 0.5 * new_height * sin_radian dy_back = 0.5 * height + 0.5 * new_width * sin_radian - 0.5 * new_height * cos_radian for y in range(new_height): for x in range(new_width): x0 = x * cos_radian + y * sin_radian + dx_back y0 = y * cos_radian - x * sin_radian + dy_back x_low, y_low = int(x0), int(y0) x_up, y_up = x_low + 1, y_low + 1 u, v = math.modf(x0)[0], math.modf(y0)[0] # 求x0和y0的小數部分 x1, y1 = x_low, y_low x2, y2 = x_up, y_low x3, y3 = x_low, y_up x4, y4 = x_up, y_up if 0 < int(x0) <= width and 0 < int(y0) <= height: pixel = (1 - u) * (1 - v) * img_copy[y1, x1] + (1 - u) * v * img_copy[y2, x2] + u * (1 - v) * img_copy[y3, x3] + u * v * img_copy[y4, x4] # 雙線性插值法,求像素值。 new_img[int(y), int(x)] = pixel
雙向性插值法效果圖
雙向性插值法效果個人感覺沒有單獨使用后向映射的效果好,不知道是不是我的代碼有問題,如果有寫法不對的話,可以指出,我再好好學習學習。
總結,這里只實現了順時針旋轉的效果,而逆時針旋轉的話,只需要將公式一和公式二中間的旋轉公式對換就可以了。另外這里也只是以圖像中心點為坐標原點進行旋轉的,圍繞任意坐標點進行旋轉的方法還沒吃透,希望有大神可以指點一下。然后這里的代碼都是沒有經過優化的,只是簡單實現效果而已,所以運行速度會比較慢。下面附上完整的代碼。
import cv2 import math import numpy as np path = r'E:\pic\p77.jpg' img = cv2.imread(path) height, width = img.shape[:2] if img.ndim == 3: channel = 3 else: channel = None angle = 30 if int(angle / 90) % 2 == 0: reshape_angle = angle % 90 else: reshape_angle = 90 - (angle % 90) reshape_radian = math.radians(reshape_angle) # 角度轉弧度 # 三角函數計算出來的結果會有小數,所以做了向上取整的操作。 new_height = math.ceil(height * np.cos(reshape_radian) + width * np.sin(reshape_radian)) new_width = math.ceil(width * np.cos(reshape_radian) + height * np.sin(reshape_radian)) if channel: new_img = np.zeros((new_height, new_width, channel), dtype=np.uint8) else: new_img = np.zeros((new_height, new_width), dtype=np.uint8) radian = math.radians(angle) cos_radian = np.cos(radian) sin_radian = np.sin(radian) dx = 0.5 * new_width + 0.5 * height * sin_radian - 0.5 * width * cos_radian dy = 0.5 * new_height - 0.5 * width * sin_radian - 0.5 * height * cos_radian # ---------------前向映射-------------------- # for y0 in range(height): # for x0 in range(width): # x = x0 * cos_radian - y0 * sin_radian + dx # y = x0 * sin_radian + y0 * cos_radian + dy # new_img[int(y) - 1, int(x) - 1] = img[int(y0), int(x0)] # 因為整體映射的結果會比偏移一個單位,所以這里x,y做減一操作。 # ---------------后向映射-------------------- dx_back = 0.5 * width - 0.5 * new_width * cos_radian - 0.5 * new_height * sin_radian dy_back = 0.5 * height + 0.5 * new_width * sin_radian - 0.5 * new_height * cos_radian # for y in range(new_height): # for x in range(new_width): # x0 = x * cos_radian + y * sin_radian + dx_back # y0 = y * cos_radian - x * sin_radian + dy_back # if 0 < int(x0) <= width and 0 < int(y0) <= height: # 計算結果是這一范圍內的x0,y0才是原始圖像的坐標。 # new_img[int(y), int(x)] = img[int(y0) - 1, int(x0) - 1] # 因為計算的結果會有偏移,所以這里做減一操作。 # ---------------雙線性插值-------------------- if channel: fill_height = np.zeros((height, 2, channel), dtype=np.uint8) fill_width = np.zeros((2, width + 2, channel), dtype=np.uint8) else: fill_height = np.zeros((height, 2), dtype=np.uint8) fill_width = np.zeros((2, width + 2), dtype=np.uint8) img_copy = img.copy() # 因為雙線性插值需要得到x+1,y+1位置的像素,映射的結果如果在最邊緣的話會發生溢出,所以給圖像的右邊和下面再填充像素。 img_copy = np.concatenate((img_copy, fill_height), axis=1) img_copy = np.concatenate((img_copy, fill_width), axis=0) for y in range(new_height): for x in range(new_width): x0 = x * cos_radian + y * sin_radian + dx_back y0 = y * cos_radian - x * sin_radian + dy_back x_low, y_low = int(x0), int(y0) x_up, y_up = x_low + 1, y_low + 1 u, v = math.modf(x0)[0], math.modf(y0)[0] # 求x0和y0的小數部分 x1, y1 = x_low, y_low x2, y2 = x_up, y_low x3, y3 = x_low, y_up x4, y4 = x_up, y_up if 0 < int(x0) <= width and 0 < int(y0) <= height: pixel = (1 - u) * (1 - v) * img_copy[y1, x1] + (1 - u) * v * img_copy[y2, x2] + u * (1 - v) * img_copy[y3, x3] + u * v * img_copy[y4, x4] # 雙線性插值法,求像素值。 new_img[int(y), int(x)] = pixel cv2.imshow('res', new_img) cv2.waitKey() cv2.destroyAllWindows()