图像修图之放大眼睛
放大眼睛分为两步:检测眼睛、放大眼睛。检测眼睛就不写了,那个属于目标检测的范畴。
放大眼睛的思路是源于 Interactive Image Warping 这篇文章。
思路如这张图所示,圆心是眼睛,放大就是从 A 点放大到 B 点。
这里就要谈一个注意点,我第一次实现看了这个图那么简单,很快实现了但结果却很不好,原来有一个注意点:半径上的点放大比例是不一样的,离圆心较近的点放大会强一点,离圆心较远的点放大会弱一点,直到最后到了边缘。
放大的公式为,其中 a 用于控制放大程度,而 rmax 则是规定放大的范围:
上面的公式可能还是抽象,直接看如何调整横坐标的代码:
\[
dis^2 = (x - x_0)^2 + (y - y_0)^2
\]
\[
k = 1 - (1 - \frac{dis^2}{{rmax}^2}) * a
\]
\[
newx = x_0 + (x - x_0) * k
\]
最后,有经验的人肯定知道,做图像变换往往是遍历目标图片,去推测当前的点是原图的哪个点;而不是遍历原始图片,去推测这个点会变成目标图片上哪个点。
所以回看这个公式,我们要把 x 和 y 当成目标图片的像素点,k 就是缩放比例,根据 k 就能计算出当前像素点从原图哪个点变换来的。所以如果要放大眼睛,那么 k 其实应该是小于 1,是放大比例的倒数,所以 a 要小于 1 去保证 k 小于 1。
此时 rmax 的意思就是我的眼睛最终变换后的半径,而不是变换前的半径。推测出当前像素属于原图哪个点时,如果是小数,要用双线性插值去计算。
代码:
import cv2
import numpy as np
import math
src = cv2.imread('./src.jpg')
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY).astype(float)
def binterp(img, x, y):
x0, x1 = int(x), int(x) + 1
y0, y1 = int(y), int(y) + 1
q00, q01, q10, q11 = img[x0, y0], img[x0, y1], img[x1, y0], img[x1, y1]
result = 0
result += q00 * (x1 - x) * (y1 - y)
result += q10 * (x - x0) * (y1 - y)
result += q01 * (x1 - x) * (y - y0)
result += q11 * (x - x0) * (y - y0)
return result
eyes = [(321, 548)]
radius = 30
scale = 0.2
now = src.copy().astype(float)
for eye in eyes:
xlo, xhi = int(eye[0]-radius), int(eye[0]+radius)
ylo, yhi = int(eye[1]-radius), int(eye[1]+radius)
for i in range(xlo, xhi):
for j in range(ylo, yhi):
dx = (i - eye[0])
dy = (j - eye[1])
if dx*dx + dy*dy < radius*radius:
k = 1 - (1 - (dx*dx + dy*dy) / (radius*radius)) * scale
srcx = eye[0] + dx * k
srcy = eye[1] + dy * k
# bilinear interpolation
now[i, j] = binterp(src, srcx, srcy)
now = now.clip(0, 255).astype(np.uint8)
cv2.imwrite('./test.png', now)
题外话
当然如果觉得这样别扭,非要输入眼睛半径和放大比例,可以换算:
# 原来的眼睛半径,和放大的比例
oldr = 20
scale = 1.2
# newr 表示眼睛最后变换后的半径,也就是最后计算到这个边界就结束
newr = scale * oldr
rmax = newr
# 后面就一样了,根据目标推原图的位置,还是要给一个参数 a,表示放大的变化强度