27

给定一张图像和一组附加到图像上特定点的标签,我正在寻找一种算法来将标签布置在图像的两侧,并具有一定的约束(每侧的标签数量大致相同,标签大致等距,将标签连接到它们各自的点的线,没有线交叉)。

现在,通过按 Y 坐标(它们所指的点)对标签进行排序,通常可以非常天真地找到近似解,如本例所示(仅用于概念证明,请忽略实际数据的准确性或其他情况!)。

现在为了满足没有交叉的条件,我想到了一些想法:

  • 使用遗传算法找到没有交叉的标签排序;
  • 使用另一种方法(例如动态规划算法)来搜索这样的排序;
  • 使用上述算法之一,允许间距和排序的变化,以找到最小化交叉数量和均匀间距变化的解决方案;
  • 也许有一些标准我可以用来在特定标准内对标签的所有可能排序进行粗略搜索(如果两个标签的距离大于 X,则不要重新排序);
  • 如果所有其他方法都失败了,只需尝试数百万个随机排序/间距偏移量,并采用能够提供最小交叉/间距变化的那个。(优点:编程简单,可能会找到一个足够好的解决方案;轻微的缺点,虽然不是一个显示停止器:可能无法在应用程序期间动态运行它以允许用户更改图像的布局/大小。 )

在我开始其中一个之前,我只想欢迎其他人的意见:是否有其他人遇到过类似的问题,并且有任何信息可以报告上述任何方法的成功/失败,或者他们是否有更好的/我没有想到的更简单的解决方案?感谢您的输入!

4

7 回答 7

14

Lucas Bradsheet 的荣誉论文Labeling Maps using Multi-Objective Evolutionary Algorithms对此进行了很好的讨论。

首先,本文为许多标签质量指标创建了可用的指标。

例如,清晰度(站点和标签之间的映射有多明显):clearance(s)=r s +r s 1/r t
其中 r s是站点与其标签之间的距离,r t是 a 之间的距离网站和最接近的其他标签)。

它还具有用于标签、站点和边界之间的冲突以及测量标签的密度和对称性的有用指标。然后,Bradsheet 使用多目标遗传算法生成可行解的“帕累托前沿”。它还包括有关他如何使个体变异的信息,以及有关提高算法速度的一些注释。

里面有很多细节,应该提供一些很好的思考。

于 2012-11-08T15:52:07.183 回答
9

让我们暂时忘记信息设计。这个任务回忆了一些与PCB 布线算法相关的记忆。实际上有很多共同的要求,包括:

  1. 交叉口优化
  2. 尺寸优化
  3. 差距优化

因此,可以将初始任务变成类似于 PCB 布线的任务。

有很多可用的信息,但我建议查看Tan Yan 的关于 PCB 布线的算法研究

它提供了很多细节和几十个提示。

适应当前任务

这个想法是将图像和标签上的标记视为两组引脚,并使用转义路由来解决任务。通常 PCB 区域表示为一个引脚阵列。可以对图像进行相同的优化,并进行可能的优化:

  1. 避免低对比度区域
  2. 避免使用文本框(如果有)
  3. ETC

因此,可以将任务简化为“在未使用引脚的情况下进行路由”

在此处输入图像描述

最终结果可以非常接近请求的样式:

在此处输入图像描述

Tan Yan 对 PCB 布线的算法研究是一个继续的好地方。

补充笔记

为了突出相似性,我稍微改变了绘画的风格。

在此处输入图像描述

做一些反向转换应该不是什么大问题,保持良好的外观和可读性。

在此处输入图像描述

无论如何,简单的专家(例如我)可以花几分钟时间发明更好的东西(或不同的东西):

在此处输入图像描述

至于我,曲线看起来不像是一个完整的解决方案,至少在这个阶段是这样。无论如何,我只是试图表明存在改进的空间,因此可以考虑将 PCB 布线方法作为一种选择。

于 2012-11-17T14:13:03.413 回答
8

一种选择是将其转换为整数规划问题。

假设您拥有n pointsn corresponding labels分布在图表的外部。

可能的线数是n^2,如果我们查看所有可能的交叉点,则少于n^4交叉点(如果显示了所有可能的线)。

在我们的整数规划问题中,我们添加了以下约束:

(决定线路是否开启(即显示到屏幕上))

  1. 对于图表上的每个点,连接到它的可能的 n 条线中只有一条要打开。

  2. 对于每个标签,连接到它的可能的 n 条线路中只有一条要打开。

  3. 对于每一对相交的线段line1和line2,这些线中只有0条或其中一条可以被打开。

  4. 可选地,我们可以最小化所有接通线路的总距离。这增强了美感。

当所有这些约束同时成立时,我们有一个解决方案:

在此处输入图像描述

下面的代码为 24 个随机点生成了上图。

一旦你开始获得超过 15 分左右,程序的运行时间就会开始变慢。

我使用了PULP包和它的默认求解器。我使用 PyGame 进行显示。

这是代码:

__author__ = 'Robert'

import pygame
pygame.font.init()
import pulp
from random import randint

class Line():
    def __init__(self, p1, p2):
        self.p1 = p1
        self.p2 = p2
        self.length = (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2

    def intersect(self, line2):
        #Copied some equations for wikipedia. Not sure if this is the best way to check intersection.
        x1, y1 = self.p1
        x2, y2 = self.p2
        x3, y3 = line2.p1
        x4, y4 = line2.p2
        xtop = (x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4)
        xbottom = (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4)
        ytop = (x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4)
        ybottom = xbottom
        if xbottom == 0:
            #lines are parallel. Can only intersect if they are the same line. I'm not checking that however,
            #which means there could be a rare bug that occurs if more than 3 points line up.
            if self.p1 in (line2.p1, line2.p2) or self.p2 in (line2.p1, line2.p2):
                return True
            return False
        x = float(xtop) / xbottom
        y = float(ytop) / ybottom
        if min(x1, x2) <= x <= max(x1, x2) and min(x3, x4) <= x <= max(x3, x4):
            if min(y1, y2) <= y <= max(y1, y2) and min(y3, y4) <= y <= max(y3, y4):
                return True
        return False

def solver(lines):
    #returns best line matching
    lines = list(lines)
    prob = pulp.LpProblem("diagram labelling finder", pulp.LpMinimize)

    label_points = {} #a point at each label
    points = {} #points on the image
    line_variables = {}
    variable_to_line = {}

    for line in lines:
        point, label_point = line.p1, line.p2
        if label_point not in label_points:
            label_points[label_point] = []
        if point not in points:
            points[point] = []
        line_on = pulp.LpVariable("point{0}-point{1}".format(point, label_point),
            lowBound=0, upBound=1, cat=pulp.LpInteger) #variable controls if line used or not
        label_points[label_point].append(line_on)
        points[point].append(line_on)
        line_variables[line] = line_on
        variable_to_line[line_on] = line

    for lines_to_point in points.itervalues():
        prob += sum(lines_to_point) == 1 #1 label to each point..

    for lines_to_label in label_points.itervalues():
        prob += sum(lines_to_label) == 1 #1 point for each label.

    for line1 in lines:
        for line2 in lines:
            if line1 > line2 and line1.intersect(line2):
                line1_on = line_variables[line1]
                line2_on = line_variables[line2]
                prob += line1_on + line2_on  <= 1 #only switch one on.

    #minimize length of switched on lines:
    prob += sum(i.length * line_variables[i] for i in lines)

    prob.solve()
    print prob.solutionTime
    print pulp.LpStatus[prob.status] #should say "Optimal"
    print len(prob.variables())

    for line_on, line in variable_to_line.iteritems():
        if line_on.varValue > 0:
            yield line #yield the lines that are switched on

class Diagram():
    def __init__(self, num_points=20, width=700, height=800, offset=150):
        assert(num_points % 2 == 0) #if even then labels align nicer (-:
        self.background_colour = (255,255,255)
        self.width, self.height = width, height
        self.screen = pygame.display.set_mode((width, height))
        pygame.display.set_caption('Diagram Labeling')
        self.screen.fill(self.background_colour)
        self.offset = offset
        self.points = list(self.get_points(num_points))
        self.num_points = num_points
        self.font_size = min((self.height - 2 * self.offset)//num_points, self.offset//4)

    def get_points(self, n):
        for i in range(n):
            x = randint(self.offset, self.width - self.offset)
            y = randint(self.offset, self.height - self.offset)
            yield (x, y)

    def display_outline(self):
        w, h = self.width, self.height
        o = self.offset
        outline1 = [(o, o), (w - o, o), (w - o, h - o), (o, h - o)]
        pygame.draw.lines(self.screen, (0, 100, 100), True, outline1, 1)
        o = self.offset - self.offset//4
        outline2 = [(o, o), (w - o, o), (w - o, h - o), (o, h - o)]
        pygame.draw.lines(self.screen, (0, 200, 0), True, outline2, 1)

    def display_points(self, color=(100, 100, 0), radius=3):
        for point in self.points:
            pygame.draw.circle(self.screen, color, point, radius, 2)

    def get_label_heights(self):
        for i in range((self.num_points + 1)//2):
            yield self.offset + 2 * i * self.font_size

    def get_label_endpoints(self):
        for y in self.get_label_heights():
            yield (self.offset, y)
            yield (self.width - self.offset, y)

    def get_all_lines(self):
        for point in self.points:
            for end_point in self.get_label_endpoints():
                yield Line(point, end_point)


    def display_label_lines(self, lines):
        for line in lines:
            pygame.draw.line(self.screen, (255, 0, 0), line.p1, line.p2, 1)

    def display_labels(self):
        myfont = pygame.font.SysFont("Comic Sans MS", self.font_size)
        label = myfont.render("label", True, (155, 155, 155))
        for y in self.get_label_heights():
            self.screen.blit(label, (self.offset//4 - 10, y - self.font_size//2))
            pygame.draw.line(self.screen, (255, 0, 0), (self.offset -  self.offset//4, y), (self.offset, y), 1)
        for y in self.get_label_heights():
            self.screen.blit(label, (self.width - 2*self.offset//3, y - self.font_size//2))
            pygame.draw.line(self.screen, (255, 0, 0), (self.width - self.offset + self.offset//4, y), (self.width - self.offset, y), 1)

    def display(self):
        self.display_points()
        self.display_outline()
        self.display_labels()
        #self.display_label_lines(self.get_all_lines())
        self.display_label_lines(solver(self.get_all_lines()))

diagram = Diagram()
diagram.display()

pygame.display.flip()
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
于 2012-11-14T08:05:13.567 回答
8

我认为这个问题的实际解决方案是在稍微不同的层上。完全忽略信息设计开始解决算法问题似乎不是正确的想法。这里有一个有趣的例子

让我们确定一些重要的问题:

  1. 如何最好地查看数据?
  2. 会迷惑人吗?
  3. 它可读吗?
  4. 它真的有助于更好地理解图片吗?

顺便说一句,混乱真的很混乱。我们喜欢秩序和可预测性。无需向初始图像引入额外的信息噪声。

在此处输入图像描述

图形消息的可读性由内容及其呈现方式决定。信息的可读性涉及读者理解文本和图片风格的能力。由于额外的“嘈杂”方法,您拥有有趣的算法任务。消除混乱 - 找到更好的解决方案:)

在此处输入图像描述

请注意,这只是一个PoC。这个想法是只使用带有清晰标记的水平线。标签放置简单且确定。可以提出几个类似的想法。

在此处输入图像描述

使用这种方法,您可以轻松平衡左右标签,避免线条之间的小的垂直间隙,为标签提供最佳垂直密度等。

编辑

好的,让我们看看初始过程的外观。

用户故事:作为用户,我希望对重要的图像进行注释,以简化理解并增加其解释价值。

重要假设:

  1. 初始图像是用户的主要对象
  2. 可读性是必须的

因此,最好的解决方案是有注释但没有注释。(我真的建议花一些时间阅读有关创造性解决问题的理论)。

基本上,用户看到初始图片应该没有障碍,但注释应该在需要时就在那儿。可能会有些混乱,对此感到抱歉。

你认为路口问题是下图背后的唯一问题吗?

在此处输入图像描述

请注意,开发方法背后的实际目标是提供两个信息流(图像和注释)并帮助用户尽快理解所有内容。顺便说一句,视觉记忆也很重要。

人类视觉的背后是什么:

  1. 选择性注意
  2. 熟悉度检测
  3. 模式检测

你想至少打破其中一种机制吗?我希望你不要。因为它会使实际结果不是很人性化。

那么什么可以分散我的注意力呢?

  1. 奇怪的线条随机分布在图像上(随机的几何对象非常分散注意力)
  2. 注释位置和样式不统一
  3. 由于图像和注释层的最终合并而产生的奇怪的复杂模式

为什么要考虑我的建议?

  1. 它有简单的图案,所以图案检测会让用户停止注意注释,而是看到图片
  2. 它具有统一的设计,因此熟悉度检测也可以工作
  3. 它不会像其他解决方案那样影响初始图像,因为线条的宽度最小。
  4. 线条是水平的,不使用抗锯齿,因此可以节省更多信息并提供干净的结果
  5. 最后,它确实大大简化了路由算法。

一些额外的评论:

  1. 不要使用随机点来测试你的算法,使用简单但重要的案例。您会看到自动化解决方案有时可能会严重失败。
  2. 我不建议按原样使用我提出的方法。有很多可能的增强功能。
  3. 我真正建议的是上一层并在元级别上进行多次迭代。

分组可用于处理复杂的情况,Robert King 提到:

在此处输入图像描述

或者我可以想象一下,某个点位于其默认位置的略上方。但只是一秒钟,因为我不想破坏主要的处理流程并影响其他标记。

在此处输入图像描述

感谢您的阅读。

于 2012-11-16T13:09:39.197 回答
5

您可以找到图表的中心,然后从中心径向向外的点绘制线。唯一可以交叉的方法是如果两个点位于同一条射线上,在这种情况下,您只需将其中一条线向一侧移动一点,另一条线向另一侧移动一点,如下所示:

中心线

仅显示实际零件:

全部做完

如果有两个或更多点与中心共线,您可以将线稍微移到一边:

在共线性的情况下

虽然这不会产生非常好的多段线事物,但它非常清楚地标记了图表。此外,为了使其更具视觉吸引力,最好为实际上是对象中心的中心选择一个点,而不仅仅是点集的中心。

于 2012-11-16T16:36:15.010 回答
3

我会在您的原型中再添加一件事 - 在此之后可能会被接受:

遍历每个交叉点并交换标签,重复直到有交叉点。

这个过程是有限的,因为状态的数量是有限的,并且每次交换都会减少所有行长度的总和 - 所以不可能有循环。

于 2012-11-13T08:25:50.457 回答
1

这个问题可以转换为图形布局。

我建议您查看例如Graphviz 库。我没有做过任何实验,但相信通过将要标记的点和标签本身表示为节点,将引线表示为边,你会得到很好的结果。

您必须将标签不应作为“虚拟”节点的区域表示为不重叠。

Graphvis 具有多种语言的绑定

即使 Graphviz 没有足够的灵活性来完全满足您的需求,该页面的“理论”部分也提供了可应用于您的问题的能量最小化和弹簧算法的参考。关于图形布局的文献是巨大的。

于 2012-11-17T15:32:31.567 回答