微软颜龄 维护小记——布局的小智慧

简介

前几版How-Old发布后,不少用户反馈,在显示结果的页面中,用于标注前面人年龄的标签,会遮挡住后面的人的脸。这是因为我们最初采用固定偏移的方式来放置年龄标签。

而怎么样让标签不遮挡住其他人的脸,则成为一个有趣的问题。最近我们发布了一次How-Old更新,正好用这篇文章,来记录一下我们对这一问题的实现。

先直观的看一下新版本的改变(左旧 右新):

问题

我们来抽象一下这个问题。

在服务器端识别出了照片中的脸后,会将识别数据传回客户端,其中包含了每个脸的边缘矩形的位置和大小信息(FaceRect)。

然后我们要为每个脸添加对应的标签(LabelRect)。LabelRect和FaceRect两两不重合,LaebelRect自身两两不重合。(FaceRect本身是有可能重合的)

并且我们希望每个标签都尽量离对应的脸比较近。

以上就是比较核心的问题描述。此外我们在实现中还加入了一些小小的增强体验的条件,在正文中会为大家叙述。

算法

我们采用了平面分割标记的算法来布置LabelRect。

对于每个Rect(包括LabelRect,FaceRect),我们需要它的中心点RectCenter(x, y),我们需要确定的也正是每个LabelRect的中心点。

简单的分析一下,我们发现在每个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.

做好这些准备后,就是我们最后的布局阶段了。

我们依照距离所有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进行各种各样图片的测试。

最后 向量子力学致敬:)

时间: 2024-10-02 17:56:19

微软颜龄 维护小记——布局的小智慧的相关文章

target-densitydpi=device-dpi会使其他ui插件布局变小

target-densitydpi=device-dpi会使其他ui插件布局变小 东哥说:不用rem了,把meta改成这样<meta name="viewport" content="width=720, user-scalable=no, target-densitydpi=device-dpi">就可以直接上px... 这句target-densitydpi=device-dpi是什么意思呢? target-densitydpi这个私有属性,它表示目

小智慧96

能让内心保持宁静的人,才是最有力量的人."神静而心和,心和而形全:神躁则心荡,心荡则形伤."一个人心浮气躁时,方寸已乱,必然会导致举止失常,进退无据,会失去正确的判断力.反之,心静神定,泰然自若,你便听不到外界的喧嚣和嘈杂,为人处世就不会失于轻率.每临大事有静气,方为大家风范. [成就事业的七大法则]1.一定要自己喜欢并擅长的事情:2.保持学习心态:3.创新求变:4.认准了,就去做:不跟风,不动摇:5.少许诺,多兑现:6.帮助别人,成就自己:7.把事情做到极致.有些道理不仅适用于职场,

小智慧90

1.迷茫时看的8句话:①先处理心情再处理事情:②最困难的时候就是最接近成功的时候:③不为模糊不清的未来担忧只为清清楚楚的现在努力:④宽容他人对你的冒犯:⑤不要无缘无故的妒忌:⑥只为成功找方法,不为失败找借口:⑦不要看我失去什么,只看我还拥有什么:⑧用最放松的心态对待一切艰难. 小智慧90,布布扣,bubuko.com

小智慧,大作为——Bloom Filter

知道杀毒软件的病毒库如何存储的吗,杀毒软件又是如何识别一个病毒的呢,这或许是一个很深奥的问题.但是今天学习的了一个新东西--Bloom Filter,这个设计简单的小东西却可以很好的解决这一问题,并在很多常见的地方被使用,真是小智慧大作为.所以在这里分享一下Bloom Filter这个神奇的东东. 一.Bloom Filter是什么? Bloom Filter是具有很好的时间和空间效率的二进制向量数据结构,它使用较少的空间来存储海量的数据对象,并可以快速查询. 二.Bloom Filter的组成

15.Android中LinearLayout布局一些小记录

在App中,我们经常看到布局中会有分割线,直接上代码: 1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height=

用constraints布局的小技巧

constraints最大的好处便是屏幕适配 一般情况下导航栏都是44加上状态栏的20,这样可以让我们按照比例来布局,就不必固定或者等于高宽度来布局控件以消除警告, 选中两个控件然后 101/43代表的就是乘法系数的比例值,可以通过上面的first和second直接得出想要的结果,这样在不同屏幕时都会按照比例进行元素的布局且没有constraints警告了

【刷题小记48】小明的调查作业

描述 小明的老师布置了一份调查作业,小明想在学校中请一些同学一起做一项问卷调查,聪明的小明为了实验的客观性,想利用自己的计算机知识帮助自己.他先用计算机生成了N个1到1000之间的随机整数(0<N≤1000),对于其中重复的数字,只保留一个,把其余相同的数去掉,不同的数对应着不同的学生的学号.然后再把这些数从小到大排序,按照排好的顺序去找同学做调查.请你协助明明完成"去重"与"排序"的工作. 输入 输入有2行,第1行为1个正整数,表示所生成的随机数的个数: N

绿箭借助酷客多快速布局微信小程序

近日,酷客多小程序(http://www.kukeduo.cn)客户"绿箭商城"上线,酷客多小程序将携手绿箭打造新零售O2O商城系统,协助其向"互联网+"新零售转型,为打通线上线下双渠道引擎又做出了一伟大创举! 绿箭公司是国际糖果业界的领导者之一和全球最大的口香糖生产及销售商.绿箭已占据中国口香糖市场60%的市场份额,虽然在中国同行业竞争者不少,但他们的市场份额,销售额等都无法与绿箭相提并论. 此次绿箭借力微信小程序,秉持着小程序"用完即走"的理

圣杯布局:小细节

圣杯布局的思路整理: 两侧定宽,中间自适应 1.一个大的container,包裹左中右三个div 2.中间的main要自适应,设定其宽度为100%:左右定宽. 3.为三位选手都设定浮动,左侧div设定左外边距为-100%(相对于container,会把自己拉到上一行),右侧设定左外边距为负的自身宽度 4.父元素container设定padding,来为左右div留出位置. 5.左右选手使用相对定位并设定left值,这样在页面缩放/中间部分文字超出时不会遮挡内容. 可能会有新手跟我出现一样的问题: