Image Retargeting
圖像縮略圖、圖像重定向
前言
這篇文章主要對比DL出現之前的幾種上古算法,為了作為DL方法的引子而存在,順便博客也該更新點新內容上來了,這篇博文就是介紹了我最近在玩什么。
本文方法
傳統的方法主要有三種:Resize
(拉伸、收縮
)、Crop
(裁剪
)和Seam Carving
(接縫裁剪
)。
其中接縫裁剪這個算法挺好玩的,論文參見 Seam Carving,截止本篇博文,被引用次數是1914次,可以說是很經典的文章了。
該論文實現的效果圖:
本文用到的python庫
三種算法的對比由python
實現,python版本為python3.8
,對應下列依賴庫版本為conda
直接安裝,不同版本請注意自己改動部分接口。
opencv 用於圖像處理
scipy 用於圖像卷積
notebook 提供環境
matplotlib 用於圖像顯示
tqdm 用於進度顯示(可不用 主要是因為SC算法太慢了 會讓人覺得程序卡了
numpy 用於輔助opencv
具體引用代碼如下:
import cv2
import matplotlib.pyplot as plt
import numpy as np
from scipy.ndimage.filters import convolve
from tqdm import trange
圖像的讀入
都有opencv了,還用問么?
img = cv2.imread('test1.jpg')
imshow(img)
img.shape
圖像的顯示
其中imshow()函數是自己定義的,用於顯示處理結果和處理過程的中間圖像,這樣就方便在notebook中查看了,需要注意的是opencv存儲圖像的格式和PIL不太一樣,為bgr,需要轉換。
def imshow(img):
if (len(img.shape) == 2) :
plt.imshow(img)
plt.show()
return
b,g,r = cv2.split(img)
img_rgb = cv2.merge([r,g,b])
plt.imshow(img_rgb)
plt.show()
方法一:裁剪(Crop)
裁剪配合numpy的花式索引
(別笑,這是正式名稱)即可實現,本質上就是對數組的划分。
假如限定屏幕寬度為900像素(因為一般用在手機、iPad等終端上,所以不限制高度),Resize的結果如下:
左側裁剪:
width = 900
height = img.shape[0]
crop = img[:height, :width]
imshow(crop)
居中裁剪:
width = 900
height = img.shape[0]
crop = img[:height, (img.shape[1] - width) // 2 : (img.shape[1] + width) // 2]
imshow(crop)
可以看出,裁剪方法完全沒有考慮圖像的細節,簡單的裁剪帶來內容的嚴重丟失,優點是速度極快,幾乎不消耗資源。
方法二:縮放(Resize)
縮放也是使用opencv內置函數實現。
opencv提供了五種Resize方法:
INTER_NEAREST - 最鄰近插值
INTER_LINEAR - 雙線性插值 默認
INTER_AREA - resampling using pixel area relation.
INTER_CUBIC - 4x4像素鄰域內的雙立方插值
INTER_LANCZOS4 - 8x8像素鄰域內的Lanczos插值
width = 900
height = 600
resize = cv2.resize(img, (width,height))
imshow(resize)
可以看出,縮放方法造成了圖像的失真,而且是嚴重失真,其優點也是速度極快,幾乎不消耗資源。
方法三:接縫裁剪(Seam Carving)
這是本文重點介紹的算法,主要思想是圖像總有一些不重要的列,將其刪除比刪除隨機的列或者重新填充要更保留圖像的細節部分,同時確保圖像整體不嚴重失真(這里的列不是數組意義上的列,是圖像中八聯通
的一條線,即一條接縫
)。
步驟一:獲取圖像的能量圖:
能量圖就是圖像的邊緣啦,相當於圖像的細節,這里使用偷懶的卷積實現。
卷積核是這兩個:
def cal_energy(img):
filter_du = np.array([
[1.0, 2.0, 1.0],
[0.0, 0.0, 0.0],
[-1.0, -2.0, -1.0],
])
filter_du = np.stack([filter_du] * 3, axis=2)
filter_dv = np.array([
[1.0, 0.0, -1.0],
[2.0, 0.0, -2.0],
[1.0, 0.0, -1.0],
])
filter_dv = np.stack([filter_dv] * 3, axis=2)
img = img.astype('float32')
convolved = np.absolute(convolve(img, filter_du)) + np.absolute(convolve(img, filter_dv))
energy_map = convolved.sum(axis=2)
return energy_map
energy_map = cal_energy(img)
print(energy_map.shape)
imshow(energy_map)
卷積核是兩個,分別從行和列上進行卷積操作。
這里是用了偷懶的卷積操作,對圖像所有像素點做卷積運算,相當於如下C艹代碼:
Mat compute_score_matrix(Mat energy_matrix)
{
Mat score_matrix = Mat::zeros(energy_matrix.size(), CV_32F);
score_matrix.row(0) = energy_matrix.row(0);
for (int i = 1; i < score_matrix.rows; i++)
{
for (int j = 0; j < score_matrix.cols; j++)
{
float min_score = 0;
// Handle the edge cases
if (j - 1 < 0)
{
std::vector<float> scores(2);
scores[0] = score_matrix.at<float>(i - 1, j);
scores[1] = score_matrix.at<float>(i - 1, j + 1);
min_score = *std::min_element(std::begin(scores), std::end(scores));
}
else if (j + 1 >= score_matrix.cols)
{
std::vector<float> scores(2);
scores[0] = score_matrix.at<float>(i - 1, j - 1);
scores[1] = score_matrix.at<float>(i - 1, j);
min_score = *std::min_element(std::begin(scores), std::end(scores));
}
else
{
std::vector<float> scores(3);
scores[0] = score_matrix.at<float>(i - 1, j - 1);
scores[1] = score_matrix.at<float>(i - 1, j);
scores[2] = score_matrix.at<float>(i - 1, j + 1);
min_score = *std::min_element(std::begin(scores), std::end(scores));
}
score_matrix.at<float>(i, j) = energy_matrix.at<float>(i, j) + min_score;
}
}
return score_matrix;
}
卷積之后的圖像即為願圖像的能量圖,代表了圖像的細節部分,即更鋒利的邊緣,該算法認為平坦的部分能量更低,自己實驗一下就能明白,一方面有效保留了圖像中的細節部分,另一方面可能造成算法錯誤的刪除了圖像的重要部分,如雪白平坦的胸部等。
步驟二:獲取圖像接縫
圖像的接縫就是一個八聯通的線,每行有且只能選取一個像素,這里使用動態規划,回溯法求解,dp轉移方程如下:
M(i, j) = e(i, j) + min{M(i - 1, j - 1), M(i - 1, j), M(i - 1, j + 1)}
def minimum_seam(img):
r, c, _ = img.shape
energy_map = cal_energy(img)
M = energy_map.copy()
backtrack = np.zeros_like(M, dtype=np.int)
for i in range(1, r):
for j in range(c):
if j == 0:
idx = np.argmin(M[i - 1, j:j + 2])
backtrack[i, j] = idx + j
min_energy = M[i - 1, idx + j]
else:
idx = np.argmin(M[i - 1, j - 1:j + 2])
backtrack[i, j] = idx + j - 1
min_energy = M[i - 1, idx + j - 1]
M[i, j] += min_energy
return M, backtrack
M, backtrack = minimum_seam(img)
imshow(M)
圖像的接縫由dp求出,可以看出這個算法是十分慢的,同時因為損失最小的接縫被刪掉后,該接縫涉及到的左右兩側的損失不能直接復用,必須重新計算,進一步減慢了算法的執行速度。
步驟三:裁剪一列
接縫都求出來了,很明顯裁剪的那一列就應該是損失最小的接縫,刪除方法使用numpy的黑科技argmin()。
def carve_column(img):
r, c, _ = img.shape
M, backtrack = minimum_seam(img)
mask = np.ones((r, c), dtype=np.bool)
j = np.argmin(M[-1])
for i in reversed(range(r)):
mask[i, j] = False
j = backtrack[i, j]
mask = np.stack([mask] * 3, axis=2)
img = img[mask].reshape((r, c - 1, 3))
return img
for i in trange(100):
one = carve_column(img)
imshow(one)
這里模擬刪除圖像中100列之后的情況。
最終步驟:按需裁剪圖像
這里把函數參數改為縮放倍數,其實也可以寫為刪除列數,都一樣,符合人類直覺即可。
def crop_c(img, scale_c):
r, c, _ = img.shape
new_c = int(scale_c * c)
for i in trange(c - new_c):
img = carve_column(img)
return img
crop = crop_c(img, 0.8)
imshow(crop)
注意這張圖沒使用原尺寸進行運算,6小時實在難等。
6小時之后更新的圖片,縮小了20%。
可以看到,原圖像在被接縫裁剪后,保留了本身的細節,未引入大面積失真,缺點是慢!慢!慢!測試圖像是一個4K的圖像,運算刪除一列需要30s,刪除20%的列就是768列,總計用時6小時!這樣處理圖片的速度估計沒人可以接受吧。
拓展:裁剪圖像的行
很明確了,翻轉一下行不就變成列了,復用一下就ok。
def crop_r(img, scale_r):
img = np.rot90(img, 1, (0, 1))
img = crop_c(img, scale_r)
img = np.rot90(img, 3, (0, 1))
return img
crop = crop_r(img, 0.8)
imshow(crop)
圖像效果,運行了三個小時。
拓展:目標移除
理解了原算法之后這就很容易理解了,將能量圖中需要重點保留的東西能量加高,需要刪除的東西能量減低,利用蒙版(mask)即可快速實現目標移除的效果,這里直接貼原論文的效果圖嘍。
后言
根據保密協定,DL部分代碼暫不貼出,我才不會說我還沒看懂呢(
引用
Image-Processing-OpenCV
Implementing Seam Carving with Python
Seam carving--讓圖片比例隨心縮放