前幾版How-Old發佈後,很多用戶反饋,在顯示結果的頁面中,用於標註前面人年齡的標籤,會遮擋住後面的人的臉。這是由於咱們最初採用固定偏移的方式來放置年齡標籤。算法
而怎麼樣讓標籤不遮擋住其餘人的臉,則成爲一個有趣的問題。最近咱們發佈了一次How-Old更新,正好用這篇文章,來記錄一下咱們對這一問題的實現。windows
先直觀的看一下新版本的改變(左舊 右新):數組
咱們來抽象一下這個問題。服務器
在服務器端識別出了照片中的臉後,會將識別數據傳回客戶端,其中包含了每一個臉的邊緣矩形的位置和大小信息(FaceRect)。app
而後咱們要爲每一個臉添加對應的標籤(LabelRect)。LabelRect和FaceRect兩兩不重合,LaebelRect自身兩兩不重合。(FaceRect自己是有可能重合的)佈局
而且咱們但願每一個標籤都儘可能離對應的臉比較近。測試
以上就是比較核心的問題描述。此外咱們在實現中還加入了一些小小的加強體驗的條件,在正文中會爲你們敘述。優化
咱們採用了平面分割標記的算法來佈置LabelRect。this
對於每一個Rect(包括LabelRect,FaceRect),咱們須要它的中心點RectCenter(x, y),咱們須要肯定的也正是每一個LabelRect的中心點。spa
簡單的分析一下,咱們發如今每一個Rect周圍必定的區域內,是不能佈置LabelCenter的,不然就會致使重合。
以下圖所示:
亮藍色是FaceRect,墨綠色是LabelRect,中間的綠點是LabelRect的中心。
粉紅色的半透明區域就是那些不能放置LabelCenter的。這個區域的大小也由LabelRect的大小肯定(此例中LabelRect的大小是咱們設定好的,每一個都同樣)。
粉紅區域是有FaceRect分別向左右各擴展LabelRect.Width/2,向上下各擴展LabelRect.Height/2肯定的。能夠看出只要在粉紅區域之外放置LabelRect,就必然不會致使LabelRect和FaceRect相交。
咱們簡單的把每一個粉紅區域叫作一個ForbidRect。
這樣咱們就只須要在ForbidRect的邊界上選出最合適的點做爲LabelCenter就好了(好比離FaceRect最近的點)。
但實際上上圖還有問題。還要保證LabelRect彼此不相交呢?
上圖應該是這樣:
爲了方便,咱們採用依次佈置LabelRect的方式,先佈置的一旦佈置好就再也不移動了,後佈置的受限於前面佈置的。(即不採用「在一個漏斗裏倒入小球,小球會彼此擠開」這種方式)
如今咱們提供一種逐步佈置的過程,直觀的理解一下:
最初從服務器傳回的FaceRect。
============================
得出最初的ForbidRect集。
============================
佈置第一個LabelRect。
============================
更新ForbidRect集。
============================
佈置第2個LabelRect。
============================
再更新ForbidRect集就達到了咱們以前那樣的結果。
(此過程舉例中先放哪一個後放哪一個,是隨便選的)。
那,咱們怎麼肯定該把LabelCenter放在哪呢?換言之,咱們怎麼出ForbidRect的邊界上選出那個合適的點呢?
當時咱們就想,怎麼在非離散的二維平面上作這個?
而後咱們採用了分割平面的方法,就像上圖那些重疊的半透明的粉紅色塊同樣,將平面分紅一塊塊的來遍歷。
Like this:
(不重要的色塊被淡化了。)
每一個forbidRect都會引入4個分割線,橫向倆,縱向倆。
同時每條分割線會包含引入這條線的ForbidRect編號,每條線都用一個二元組描述:
Tuple1= (offset, rect_id)。Offset是這條線在垂直方向上距原點的偏移量(就是「直線X=3」裏面的那個「3」),rect_id就是引入它的ForbidRect編號。
橫線,縱向分開統計。
舉例:假設左上角那個forbidRect編號是0,右下角那個是1。當前縱向的分割線二元組數組爲:L1 = {(1, 0), (5, 0), (4, 1), (8, 1)}
而後咱們爲了以防萬一要處理一下,就是把偏移量相同的線歸組(雖然不太可能有線重合,但這也是優化點之一,咱們能夠將forbidRect對齊到一些偏移量爲某整數倍的位置)。
歸組後的新二元組以下:
Tuple2=(offset,set<rect_id>)。二元組的第二個元素變成了forbidRect 編號的集合了。
此時咱們有兩個Tuple2數組了(橫向的,縱向的),咱們按照offset字段將它們排序(兩個方向的分開進行)。
舉例,排序後的縱向線的數組爲:L2 = {(1, {0}), (4, {1}), (5, {0}), (8, {1})}
這時咱們要遍歷一下排序後的數組,收集一些信息,經過相似棧的方式獲取每一個forbidRect覆蓋的分割線在分割線數組中的索引(從0開始)。由於分割線排好序了,咱們就記一個區間好了。
舉例:forbidRect 0 的「覆蓋線」的索引區間爲: [0, 2]。
可是咱們是爲了分割平面才引入的分割線,由於水平方向上索引爲2的線(第三條線)以後已經不是forbidRect 0 的範圍了,因此這個索引區間的意義其實是[0, 2)——再也不是分割線的索引,而是橫向上的小平面區域的索引。
同時,咱們還有一個映射M1:(index1, index2) -> isDirty。映射源是一個被橫縱線分割出的小矩形(Cell)的橫縱向索引,映射目標是一個boolean量,用來表示這個Cell是否屬於一個ForbidRect。
舉例:(0,0)->true, (1,0)->true, (2,0)->false. (2,2)->true.
作好這些準備後,就是咱們最後的佈局階段了。
在How-Old實際使用的算法中,
咱們依照距離全部faceRect重心(是「重心」)最小的順序爲FaceRect排序,也就是越靠近中心的越先處理。
對每一個faceRect,找到它的ForbidRect。經過ForbidRect在X Y方向上的「覆蓋Cell」索引區間,找出位於該Forbidrect邊界上的Cell。
舉例:ForbidRect 0 邊界上的Cell有:(0, -1) (1, -1) (-1, 0) (-1, 1) (2, 0) (2, 1) (0, 2) (1, 2)
就是圖中這四個黃色塊標示的8個Cell(最左邊和最上邊的就爲它們編號-1)。
===================================
其中有幾個Cell是Dirty的:
===================================
也就是說,咱們只要在這6條線段(藍色標出)上找LabelCenter就能夠了
===================================
咱們當前的策略是:先上,再左,再右,最後下方。
對每一個線段,判斷它的兩個頂點,是在FaceRect與線段垂直的軸線的一左一右?一上一下?仍是在同一側?——這樣就能判斷最優的點(距離最近)。每一個線段有一個最優解,再從中得出全局最優解。
(若是在上方就能得出這樣的解,直接就用它作全局解。否則依次繼續左、右、下方中找。下方的點,咱們不喜歡,設置一個值去抑制它成爲全局最優解)。
但,若是一個forbidRect四面受敵,一條這樣的邊界線段也沒有怎麼辦呢?
此時咱們經過一個forbidRect相交矩陣,廣度優先,遍歷每一個和它直接或間接相接的forbidRect,從這些ForbidRect的邊界線段上,找出最優的那個點,做爲LabelCenter。
以後咱們將這個LabelRect對應的ForbidRect加入ForbidRect集,並對下一個Face(按距重心排序地)進行一樣的過程。直到全部Face都處理完成。
這個算法的大體流程就是這樣,其中也還有一些地方值得繼續優化。固然咱們還對標籤大小,標籤偏移等屬性進行了微調。
但願這篇文章能拋磚引玉,若是你們有更好的算法或者想法,歡迎和咱們交流。也歡迎下載最新版的How-Old進行各類各樣圖片的測試。
最後 向量子力學致敬:)
並附上咱們的微軟顏齡的 應用下載地址:https://www.windowsphone.com/zh-cn/store/app/%E5%BE%AE%E8%BD%AF%E9%A2%9C%E9%BE%84/8f4e7547-7ecb-4736-8306-11b97ba293e1