鉴于图像和一组标签附着在图像上的特定点上,我正在寻找一种算法,将标签布置到图像的两侧,并带有一定的约束(每侧的标签数量大致相同,标签大致等距,将标签连接到各自的点,没有线交叉。
现在,通过按Y坐标(他们所指的点)排序标签,as in this example(仅限概念验证,请忽略),通常可以非常天真地找到近似解决方案实际数据的准确性或其他方面!)。
现在为了满足不交叉的条件,我想到了一些想法:
在我开始其中之一之前,我会欢迎其他人的意见:有其他人遇到过类似问题并有任何信息可以报告上述任何方法的成功/失败,或者他们是否有一个更好/更简单的解决方案,我没有发生?感谢您的投入!
答案 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相关的一些记忆。实际上有很多共同的要求,包括:
因此,可以将初始任务转换为类似于PCB布线的任务。
有很多可用信息,但我建议您查看Algorithmic studies on PCB routing by Tan Yan。
它提供了许多细节和许多提示。
适应当前任务
这个想法是将图像和标签上的标记视为两组引脚,并使用转义路由来解决任务。通常,PCB区域表示为引脚阵列。可以通过可能的优化对图像进行相同的操作:
因此,任务可以简化为“在未使用引脚的情况下路由”
最终结果可能非常接近所要求的风格:
Algorithmic studies on PCB routing by Tan Yan是一个继续的好地方。
附加说明
为了突出相似性,我改变了绘画的风格。
做一些反向转换应该不是一个大问题,保持良好的外观和可读性。
无论如何,简单的擅长(例如我)可以花几分钟时间创造更好的东西(或者不同的东西):
至于我,曲线看起来不像一个完整的解决方案,至少在这个阶段。无论如何,我只是试图证明存在增强空间,因此PCB布线方法可以作为一种选择。
答案 2 :(得分:8)
一种选择是将其变成整数编程问题。
假设您在图表外部分布了n points
和n corresponding labels
。
可能的行数为n^2
,如果我们查看所有可能的交叉点,则交叉点少于n^4
(如果显示所有可能的行)。
在我们的整数规划问题中,我们添加了以下约束:
(决定是否开启一条线(即显示在屏幕上))
对于图中的每个点,只有一条可能的n行 连接到它将被打开。
对于每个标签,只有一条可能的n条线连接到它 要开启。
仅对每对交叉线段line1和line2 可以打开这些行中的零个或一个。
可选地,我们可以最小化所有接通线路的总距离。这增强了美感。
当所有这些约束同时存在时,我们有一个解决方案:
下面的代码为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
让我们确定一些重要的问题:
顺便说一下,混乱真的很混乱。我们喜欢秩序和可预测性。无需向初始图像引入额外的信息噪声。
图形消息的可读性由内容及其表示决定。消息的可读性涉及读者理解文本和图片样式的能力。由于额外的“嘈杂”方法,你有这个有趣的算法任务。消除混乱 - 找到更好的解决方案:)
请注意,这只是PoC。这个想法是只使用带有清晰标记的水平线。标签放置是直截了当且具有确定性。可以提出几个类似的想法。
通过这种方法,您可以轻松平衡左右标签,避免线条之间的小垂直间隙,为标签提供最佳垂直密度等。
修改强>
好的,让我们看看初始流程的外观。
用户故事:作为用户,我希望对重要图像进行注释,以简化理解并提高其解释价值。
重要假设:
因此,最好的解决方案是注释,但没有注释。 (我真的建议花一些时间阅读the theory of inventive problem solving)。
基本上,用户看到初始图片应该没有障碍,但注释应该在需要时就在那里。这可能有点令人困惑,对不起。
你认为交叉点问题是下图中唯一的问题吗?
请注意,开发方法背后的实际目标是提供两个信息流(图像和注释),并帮助用户尽快理解所有内容。顺便说一下,视力记忆也很重要。
人类视觉背后的原因:
你想打破这些机制中的至少一个吗?我希望你不要。因为它会使实际结果不是非常用户友好。
那么什么会分散我的注意力?
为什么我的建议应该考虑?
其他一些评论:
分组可以用来处理Robert King提到的复杂案例:
或者我可以想象一下,某个点位于它的默认位置上方。但只有一秒钟,因为我不想打破主要的处理流程并影响其他标记。
感谢您的阅读。
答案 4 :(得分:4)
您可以找到图表的中心,然后从中心径向向外的点绘制线条。你可以进行交叉的唯一方法是,如果两个点位于同一条光线上,在这种情况下,你只需将其中一条线移动一点,另一条移动另一条线,如下所示:
仅显示实际零件:
如果有两个或多个点与中心共线,您可以将线稍微移到侧面:
虽然这不会产生非常好的多段线,但它非常清楚地标出了图表。此外,为了使其更具吸引力,最好为中心选择一个实际上是对象中心的点,而不仅仅是点集的中心。
答案 5 :(得分:3)
我会在你的原型中添加一件事 - 可能在此之后可以接受:
遍历每个交叉点并交换标签,重复直到有交叉点。
这个过程是有限的,因为状态数是有限的,每个交换减少了所有行长度的总和 - 所以没有循环是可能的。
答案 6 :(得分:1)
此问题可以转换为图形布局。
我建议你看看,例如Graphviz library。我没有做过任何实验,但相信通过表示要标记的点和标签本身作为节点而引线作为边缘,您将获得良好的结果。
您必须表示标签不应该作为“虚拟”节点不要重叠的区域。
Graphvis有bindings for many languages。
即使Graphviz没有足够的灵活性来完全按照您的需要进行操作,该页面的“理论”部分也提供了可用于您的问题的能量最小化和弹簧算法的参考。关于图形布局的文献是巨大的。