opencv人臉檢測,旋轉處理


年會簽到,拍自己的大頭照,有的人可能會拍成橫向的,需要旋轉,用人臉檢測並修正它(圖片)。

1. 無腦檢測步驟為:

1. opencv 讀取圖片,灰度轉換
2. 使用CascadeClassifier()通過訓練數據訓練分類器
3. detectMultiScale()檢測人臉

訓練數據集下最基本的人臉haarcascade_frontalface_default.xml

2. 開始檢測

1) 斜臉檢測失敗
用了一張逃避可恥但有用劇照,不知是gakki臉斜還是不清晰的緣故,face_cascade.detectMultiScale無結果。

網上找了一張人臉圖發現可以有結果[[436 142 604 604]]

2) 旋轉圖片,被裁剪
測試圖片旋轉90°,180°,270°時,發現有個問題。下面為旋轉180°,90°的效果:90°(以及270°)的圖片大小沒變,導致橫向過長,縱向太窄,圖片被裁減。


粗暴的嘗試,直接用:

if angle in (90,270):
 w,h = h,w

轉換寬高后,輸出90°為:

出現了奇怪的邊框。

再來看一個45°的輸出,也同樣被裁剪

直到我找到了 Rotate images (correctly) with OpenCV and Python 這篇文章,寫的太好了,用的動圖也恰到好處。

rotating oblong pills using the OpenCV’s standard cv2.getRotationMatrix2D and cv2.warpAffine functions caused me some problems that weren’t immediately obvious.

作者檢測原型葯片旋轉沒關系,用橢圓的葯片就有被裁剪的問題,我上面的那些就是被裁剪了。

使用作者給的函數實驗:

def rotate_bound(image, angle):
    # grab the dimensions of the image and then determine the
    # center
    (h, w) = image.shape[:2]
    (cX, cY) = (w // 2, h // 2)
 
    # grab the rotation matrix (applying the negative of the
    # angle to rotate clockwise), then grab the sine and cosine
    # (i.e., the rotation components of the matrix)
    M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
    cos = np.abs(M[0, 0])
    sin = np.abs(M[0, 1])
 
    # compute the new bounding dimensions of the image
    nW = int((h * sin) + (w * cos))
    nH = int((h * cos) + (w * sin))
 
    # adjust the rotation matrix to take into account translation
    M[0, 2] += (nW / 2) - cX
    M[1, 2] += (nH / 2) - cY
 
    # perform the actual rotation and return the image
    return cv2.warpAffine(image, M, (nW, nH))

-45°旋轉

-90°旋轉

完成。

4) 自動旋轉圖片

作者旋轉的很好看,所以也模仿了一把,將自己展示圖片的函數改為:「 獲取圖片寬高,然后動態改變寬高顯示圖片,ms參數為停留的毫秒數(寬高除以3是因為圖片太大了,展示不好看)」

def show(img, ms=0):
 """ 顯示 """
 cv2.imshow('show', img)
 h, w = img.shape[:2]
 cv2.resizeWindow("show", w//3, h//3)
 cv2.waitKey(ms)

用自己之前不正確的旋轉函數rotate_img_old測試,有裁剪

for angle in range(0,360,10):
 show(rotate_img_bad(img_bgr, angle), 200)

用正確的測試:裴斐科特

for angle in range(0,360,10):
 show(rotate_img(img_bgr, angle), 200)

3) 再試gakki圖

range(0,360,45)以45°為步長檢測,發現-315°(即45°)有結果:

檢測到兩處人臉:修改配置 每次縮減比例:scaleFactor=1.2,檢測多次: minNeighbors=10

# 探測圖片中的人臉
faces = face_cascade.detectMultiScale(
 img_gray,
 scaleFactor=1.2,  # 每次縮減比例
 minNeighbors=10,  # 檢測多次
 flags=cv2.CASCADE_SCALE_IMAGE,
 minSize=(50,50)
)

之后檢測,只有一處。

4. 生成頭像

最后,想截取有效的最大頭像,

90°旋轉裁剪

在這次真實的年會場合實際使用的時候,不能轉45°去裁剪顯示,斜着頭拍照的可不想轉成正經的證件照,

所以只用轉90°然后裁剪其中最大正方形,取高和寬中小的一個。較簡單,藍色為實際裁剪區域。

斜的圖則不可這樣,會裁剪黑色的區域,(避免臉在圖中過小,取了檢測到的臉寬*2.5長度

代碼段

if angle%90 == 0:
    # 90° 裁剪圖片
    logging.debug("90° 裁剪圖片")
    logging.debug(f"{ix}, {iy}, {x}, {y}, {w}, {h}")

    length2 = min(ix, iy)

    # 如果人臉太小,放大區域但又不超過圖片長度
    length = int(w*2.5)
    length = min(length2, length)
    logging.debug(f"length: {length2} {length}")

    ow = length-w
    ow1 = ow//2
    oh = length-h
    oh1 = oh//2

    y1, y2 = y-oh1, y+h+oh1
    x1, x2 = x-ow1, x+w+ow1

    # 檢測圖片溢出
    logging.debug(f"{y1}, {y2}, {x1}, {x2}")
    if y1 < 0:
    logging.debug('裁剪:1 頂部溢出')
    y1 = 0
    y2 = length
    if y2 > iy:
    logging.debug('裁剪:2 底部溢出')
    y2 = iy
    y1 = iy-length
    if x1 < 0:
    logging.debug('裁剪:3 左側溢出')
    x1 = 0
    x2 = length
    if x2 > ix:
    logging.debug('裁剪:4 右側溢出')
    x2 = ix
    x1 = ix-length

    # 裁剪標記
    cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)

斜矩形裁剪探索

那么gakki那張斜的如何裁剪呢,斜矩形之外有黑色背景。舉例:應該取到綠色矩形的擴大版,但又沒有取到黑色背景。比如這樣的藍色區域

轉換為數學方法為:todo, 畫在本子上的,最后再補

編寫數學函數:

else:
    # 非90°裁剪圖片
    logging.debug(f"{angle}°裁剪圖片")
    # 1. 求A點坐標
    origin_h, origin_w = self.img_bgr.shape[:2]

    # 旋轉角度取的ab ad邊不同
    if angle%180<90:
        ab = origin_w
        ad = origin_h
    else:
        ab = origin_h
        ad = origin_w
    op = ix
    ap = math.cos(math.radians(angle)) * ab
    oa = op-ap
    A = Point(oa, 0)
    logging.debug(f"ab={ab}, ad={ad}, op={op}, ap={ap}, oa={oa}, {A}")
    # 2. 人臉中心Z坐標
    face_rect = Rectangle(Point(x, y), w, h)
    z = face_rect.center_p

    logging.debug(f"{face_rect} center point is {z}")
    # 3. Z到AB、AD距離
    k = math.tan(math.radians(angle)) # tan(α)
    k2 = -1/k # 垂直
    logging.info(f"k1 = {k}, k2 = {k2}")
    z_ab_len = abs(k*z.x-z.y-oa*k)/math.sqrt(k**2+1)
    z_ad_len = abs(k2*z.x-z.y-oa*k2)/math.sqrt(k2**2+1)
    logging.debug(f"z-ab len is {z_ab_len}, z-ad len is {z_ad_len}")
    # 4. 距離四邊最小距離
    h1 = z_ab_len
    h2 = z_ad_len
    h3 = ad-h1
    h4 = ab-h2
    min_len = min(h1, h2, h3, h4)
    logging.debug(f"face around len is {h1} {h2} {h3} {h4}, min:{int(min_len)}")

    # 5. 圓形標注
    #cv2.line(img, z.r_tuple(), (50, 100), (0,255,0))
    for r in (h1, h2, h3, h4):
        r = int(r)
        if int(min_len) == r:
            cv2.circle(img, z.r_tuple(), r, (255, 0, 0), 3)
        else:
            cv2.circle(img, z.r_tuple(), r, (0, 0, 255), 2)

來測試一波,最后的圓形標注只是為了輔助驗證。

其實是有問題的,看效果,最小的藍色圓形標注位置不對,超出原圖片了,而且觀察其他三個圓也不在圖片邊上。

if angle%180<90:
    ab = origin_w
    ad = origin_h
else:
    ab = origin_h
    ad = origin_w

這塊處理不同角度下取的矩形ABCD邊不同,數學角度看是沒有錯的,但是Opencv坐標圓心在左上角,所以得將數學圖形畫為:todo

上面賦值代碼只需改為

if angle%180<90:
    ab = origin_h
    ad = origin_w
else:
    ab = origin_w
    ad = origin_h

藍色圈已經是我們要求的矩形頭像外接圓了,其他三個圓輸出也是貼緊各自的邊框,完美。

在藍圈中畫黑色正方形:

是想要的效果,最后裁剪即可。

多角度修復

測試初始角度為橫向或者側向時,需修復角度:

# 最后取的圖片和角度無關,只取銳角
angle=angle%90

測試三張不同角度照片,和一張不需旋轉的圖:

結果為:

過程:

參考


免責聲明!

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



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