用于在图像上布置标签的建议算法/方法

时间:2012-11-08 00:24:26

标签: algorithm sorting dynamic-programming genetic-algorithm backtracking

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

现在,通过按Y坐标(他们所指的点)排序标签,as in this example(仅限概念验证,请忽略),通常可以非常天真地找到近似解决方案实际数据的准确性或其他方面!)。

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

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

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

7 个答案:

答案 0 :(得分:13)

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

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

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

它还为标签,网站和边框之间的冲突以及测量标签的密度和对称性提供了有用的指标。 Bradsheet然后使用多目标遗传算法生成“Pareto frontier”可行解。它还包括有关他如何改变个体的信息,以及关于提高算法速度的一些注意事项。

其中有很多细节,它应该提供一些很好的思考。

答案 1 :(得分:9)

让我们暂时忘记信息设计。此任务回顾了与PCB routing algorithms相关的一些记忆。实际上有很多共同的要求,包括:

  1. 交叉点优化
  2. 尺寸优化
  3. 差距优化
  4. 因此,可以将初始任务转换为类似于PCB布线的任务。

    有很多可用信息,但我建议您查看Algorithmic studies on PCB routing by Tan Yan

    它提供了许多细节和许多提示。

    适应当前任务

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

    1. 避免低对比度区域
    2. 避免使用文本框
    3. 因此,任务可以简化为“在未使用引脚的情况下路由”

      enter image description here

      最终结果可能非常接近所要求的风格:

      enter image description here

      Algorithmic studies on PCB routing by Tan Yan是一个继续的好地方。

      附加说明

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

      enter image description here

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

      enter image description here

      无论如何,简单的擅长(例如我)可以花几分钟时间创造更好的东西(或者不同的东西):

      enter image description here

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

答案 2 :(得分:8)

一种选择是将其变成整数编程问题。

假设您在图表外部分布了n pointsn corresponding labels

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

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

(决定是否开启一条线(即显示在屏幕上))

  1. 对于图中的每个点,只有一条可能的n行 连接到它将被打开。

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

  3. 仅对每对交叉线段line1和line2     可以打开这些行中的零个或一个。

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

  5. 当所有这些约束同时存在时,我们有一个解决方案:

    enter image description here

    下面的代码为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
    

答案 3 :(得分:8)

我认为这个问题的实际解决方案是在略有不同的层上。开始解决完全忽略Information design的算法问题似乎不太合适。有一个有趣的例子here

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

  1. 如何最好地查看数据?
  2. 会让人感到困惑吗?
  3. 可读吗?
  4. 这有助于更好地理解图片吗?
  5. 顺便说一下,混乱真的很混乱。我们喜欢秩序和可预测性。无需向初始图像引入额外的信息噪声。

    enter image description here

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

    enter image description here

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

    enter image description here

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

    修改

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

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

    重要假设:

    1. 初始图像是用户的主要对象
    2. 可读性是必须的
    3. 因此,最好的解决方案是注释,但没有注释。 (我真的建议花一些时间阅读the theory of inventive problem solving)。

      基本上,用户看到初始图片应该没有障碍,但注释应该在需要时就在那里。这可能有点令人困惑,对不起。

      你认为交叉点问题是下图中唯一的问题吗?

      enter image description here

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

      人类视觉背后的原因:

      1. 选择性关注
      2. 熟悉度检测
      3. 模式检测
      4. 你想打破这些机制中的至少一个吗?我希望你不要。因为它会使实际结果不是非常用户友好。

        那么什么会分散我的注意力?

        1. 随机分布在图像上的奇怪线条(随机几何物体非常分散注意力)
        2. 不统一的注释放置和样式
        3. 由于图像和注释图层的最终合并而导致奇怪的复杂图案
        4. 为什么我的建议应该考虑?

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

            1. 不要使用随机点来测试您的算法,使用简单但重要的案例。您会发现自动化解决方案有时可能会失败。
            2. 我不建议使用我提出的方法。有很多可能的增强功能。
            3. 我真正建议的是升级并在元级别上进行多次迭代。
            4. 分组可以用来处理Robert King提到的复杂案例:

              enter image description here

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

              enter image description here

              感谢您的阅读。

答案 4 :(得分:4)

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

Centerlines

仅显示实际零件:

All Done

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

In case of colinearity

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

答案 5 :(得分:3)

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

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

这个过程是有限的,因为状态数是有限的,每个交换减少了所有行长度的总和 - 所以没有循环是可能的。

答案 6 :(得分:1)

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

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

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

Graphvis有bindings for many languages

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