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--讓圖片比例隨心縮放
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/14263.html
標籤:其他
