2 # -*- coding: utf-8 -*-
5 # © 2008 Joachim Breitner <mail@joachim-breitner.de>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # * Faster geometric algorithms (nearest point, containing facet)
23 # * Faster drawing (copying pixbufs to the X server)
25 # * Better interpolation
40 selection_distance = 10
46 # vor_inge = (370,107)
47 # klspieli = (396, 32)
49 # polizeimensch = (360,225)
51 # preiss_oben = (402,324)
72 # (gack,polizeimensch),
73 # (polizeimensch,mitte),
75 # (vor_inge,klspieli),
78 # (polizeimensch,doelker),
79 # (doelker,preiss_oben),
80 # (preiss_oben, tunnel),
84 def dist2((x1,y1),(x2,y2)):
85 return ((x1-x2)**2 + (y1-y2)**2)
86 def dist((x1,y1),(x2,y2)):
87 return math.sqrt((x1-x2)**2 + (y1-y2)**2)
88 def find_footpoint((p1,p2),(x,y)):
91 u = float((x-x1)*(x2-x1) + (y - y1)*(y2 - y1)) / float((x1-x2)**2 + (y1-y2)**2)
94 return (int(round(x1 + u*(x2-x1))),
95 int(round(y1 + u*(y2-y1))))
96 def convex(r, (r1,g1,b1), (r2,g2,b2)):
97 return ((1-r) * r1 + r * r2,
108 return (self.vertices, self.edges, self.start)
111 (self.vertices, self.edges, self.start) = dump
113 def nearest_point(self, p):
115 return min(self.vertices, key=lambda v: dist(p,v))
120 for (p1,p2) in self.edges:
121 if p1 == p or p2 == p:
125 def delete_vertex(self,p):
127 self.vertices.remove(p)
129 def add_vertex(self, p):
130 assert not p in self.vertices
131 assert type(p[0]) == int and type(p[1]) == int
132 if not self.vertices:
134 self.vertices.append(p)
136 def has_edge(self,(p1,p2)):
137 return (p1,p2) in self.edges or (p2,p1) in self.edges
139 def toggle_edge(self,(p1,p2)):
140 assert p1 in self.vertices and p2 in self.vertices
141 if (p1,p2) in self.edges:
142 self.edges.remove((p1,p2))
143 elif (p2,p1) in self.edges:
144 self.edges.remove((p2,p1))
146 self.edges.append((p1,p2))
155 self.point_selected = None
156 self.hover_point = None
158 self.image = gtk.DrawingArea()
159 self.image.set_size_request(self.width, self.height)
160 self.image.add_events(gtk.gdk.BUTTON_PRESS_MASK |
161 gtk.gdk.BUTTON_RELEASE_MASK |
162 gtk.gdk.EXPOSURE_MASK |
163 gtk.gdk.POINTER_MOTION_MASK)
164 self.image.connect("expose_event",self.do_expose_event_orig)
165 self.image.connect("button_press_event",self.do_button_press_event)
166 self.image.connect("motion_notify_event",self.do_motion_notify_event)
168 self.moved = gtk.DrawingArea()
169 self.moved.set_size_request(self.width, self.height)
170 self.moved.add_events(gtk.gdk.BUTTON_PRESS_MASK |
171 gtk.gdk.BUTTON_RELEASE_MASK |
172 gtk.gdk.EXPOSURE_MASK |
173 gtk.gdk.POINTER_MOTION_MASK)
174 self.moved.connect("expose_event",self.do_expose_event_moved)
177 hbox1.add(self.image)
178 hbox1.add(self.moved)
180 self.graph_edit = gtk.CheckButton('Edit graph')
181 edit_help = gtk.Button('Edit help')
182 edit_help.connect("clicked", self.show_edit_help)
183 vbox_edit = gtk.VBox()
184 vbox_edit.add(self.graph_edit)
185 vbox_edit.add(edit_help)
187 do_open = gtk.Button('Open Image')
188 do_save = gtk.Button('Save graph')
189 do_open.connect("clicked",self.do_open_dialog)
190 do_save.connect("clicked",self.do_save_dialog)
191 do_distance = gtk.Button('Calc Dist.')
192 do_heightmap = gtk.Button('Calc Heightmap.')
193 do_morph = gtk.Button('Calc Morph.')
194 do_all = gtk.Button('Calc All.')
195 do_distance.connect("clicked",self.do_recalc,self.recalc_distance)
196 do_heightmap.connect("clicked",self.do_recalc,self.recalc_heightmap)
197 do_morph.connect("clicked",self.do_recalc,self.recalc_morph)
198 do_all.connect("clicked",self.do_recalc,self.recalc_all)
199 self.do_buttons = [do_open, do_save, do_distance,
200 do_heightmap, do_all, do_morph]
202 self.zoom = gtk.SpinButton()
203 self.zoom.set_range(1,10)
204 self.zoom.set_digits(1)
205 self.zoom.set_increments(1,1)
206 self.zoom.set_value(1)
207 hbox_zoom = gtk.HBox()
208 hbox_zoom.add(gtk.Label('Zoom:'))
209 hbox_zoom.add(self.zoom)
211 self.interpolator = gtk.combo_box_new_text()
212 self.interpolators = {
213 'Stripes': self.interpolate_stripes,
214 'Blocks': self.interpolate_blocks,
215 'None': self.interpolate_none,
217 keys = self.interpolators.keys()
220 self.interpolator.append_text(k)
221 self.interpolator.set_active(keys.index('None'))
222 hbox_interpolator = gtk.HBox()
223 hbox_interpolator.add(gtk.Label('Interpolation:'))
224 hbox_interpolator.add(self.interpolator)
226 vbox_morph = gtk.VBox()
227 vbox_morph.add(hbox_interpolator)
228 vbox_morph.add(hbox_zoom)
230 self.penalty = gtk.SpinButton()
231 self.penalty.set_range(1,10)
232 self.penalty.set_increments(1,1)
233 self.penalty.set_value(2)
234 hbox_penalty = gtk.HBox()
235 hbox_penalty.add(gtk.Label('Offroad penalty:'))
236 hbox_penalty.add(self.penalty)
238 vbox_dist = gtk.VBox()
239 vbox_dist.add(hbox_penalty)
241 self.show_heigthmap = gtk.CheckButton('Show heightmap')
242 self.show_heigthmap.props.active = True
243 self.show_heigthmap.connect('toggled', self.queue_draw)
244 vbox_heightmap = gtk.VBox()
245 vbox_heightmap.add(self.show_heigthmap)
248 hbox2.pack_start(do_open, expand=False)
249 hbox2.pack_start(do_save, expand=False)
250 hbox2.pack_start(vbox_edit, expand=False)
251 hbox2.pack_start(vbox_dist, expand=False)
252 hbox2.pack_start(do_distance, expand=False)
253 hbox2.pack_start(vbox_heightmap, expand=False)
254 hbox2.pack_start(do_heightmap, expand=False)
255 hbox2.pack_start(vbox_morph, expand=False)
256 hbox2.pack_start(do_morph, expand=False)
257 hbox2.pack_start(do_all, expand=False)
259 self.status = gtk.Label("Status")
261 self.progress = gtk.ProgressBar()
262 self.reset_progress()
265 hbox3.add(self.status)
266 hbox3.add(self.progress)
268 self.slider = gtk.HScale()
269 self.slider.set_range(0,self.width+self.height)
270 self.slider.connect("change_value",self.do_change_value)
273 vbox.pack_start(hbox1, expand=True,fill=True)
274 vbox.pack_start(self.slider, expand=False)
275 vbox.pack_start(hbox2, expand=False)
276 vbox.pack_start(hbox3, expand=False)
278 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
279 self.window.add(vbox)
285 self.pixbuf_heightmap = None
286 self.pixbuf_moved = None
287 self.moved_zoom = None
288 if os.path.exists('Ehbühl.jpg'):
289 self.load_files('Ehbühl.jpg')
291 self.do_open_dialog(None)
293 self.window.show_all()
295 def do_expose_event_orig(self, widget, event):
296 gc = widget.window.new_gc()
297 gc.set_clip_rectangle(event.area)
300 widget.window.draw_pixbuf(gc, self.pixbuf, 0,0,0,0,-1,-1)
301 if self.pixbuf_heightmap and self.show_heigthmap.props.active:
302 widget.window.draw_pixbuf(gc, self.pixbuf_heightmap, 0,0,0,0,-1,-1)
305 and not self.graph_edit.props.active
306 and not self.progress.props.sensitive):
307 pb = self.equilines()
308 widget.window.draw_pixbuf(gc, pb, 0,0,0,0,-1,-1)
311 cr = widget.window.cairo_create()
312 cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
315 cr.set_source_rgba(0,0.8,0,0.8)
316 for (s,t) in self.graph.edges:
320 for x,y in self.graph.vertices:
321 cr.arc(x,y,2,0, 2 * math.pi)
324 x,y = self.graph.start
325 cr.arc(x,y,5,0, 2 * math.pi)
328 if self.graph_edit.props.active and self.point_selected:
329 x,y = self.point_selected
330 cr.set_source_rgba(0,0,1,1)
331 cr.arc(x,y,5,0, 2 * math.pi)
334 if self.graph_edit.props.active and self.hover_point:
335 x,y = self.hover_point
336 cr.set_source_rgba(0.5,0.5,1,1)
337 cr.arc(x,y,5,0, 2 * math.pi)
340 if self.point_selected:
341 if self.graph.has_edge((self.point_selected, self.hover_point)):
342 cr.set_source_rgba(1,0.5,0.5,1)
344 cr.set_source_rgba(0.5,0.5,1,1)
345 cr.move_to(*self.point_selected)
346 cr.line_to(*self.hover_point)
349 def do_expose_event_moved(self, widget, event):
350 gc = widget.window.new_gc()
351 gc.set_clip_rectangle(event.area)
353 if self.pixbuf_moved:
354 widget.window.draw_pixbuf(gc, self.pixbuf_moved, 0,0,0,0,-1,-1)
356 cr = widget.window.cairo_create()
357 cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
363 cr.set_source_rgba(0,1,1,0.5)
364 cr.arc(self.width/2, self.height/2, self.selected_d / z, 0, 2 * math.pi)
367 def do_button_press_event(self, widget, event):
368 if self.graph_edit.props.active:
369 p = (int(round(event.x)),int(round(event.y)))
370 n = self.graph.nearest_point(p)
372 if event.button == 1: #left click
373 if dist(n,p) > selection_distance:
374 self.graph.add_vertex(p)
375 self.point_selected = p
377 if self.point_selected == n:
378 self.point_selected = None
380 self.point_selected = n
382 elif event.button == 2: #middle click
383 if dist(n,p) > selection_distance:
388 elif event.button == 3: #right click
389 if self.point_selected:
390 if dist(n,p) > selection_distance:
391 self.graph.add_vertex(p)
392 self.graph.toggle_edge((self.point_selected, p))
393 self.point_selected = p
395 if n == self.point_selected:
396 if self.graph.alone(n):
397 self.graph.delete_vertex(n)
398 self.point_selected = None
400 self.graph.toggle_edge((self.point_selected, n))
403 def do_recalc(self, widget, func):
409 pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.width, self.height)
410 el = pb.get_pixels_array()
411 my_d = self.selected_d
412 (s_x,s_y) = self.graph.start
413 for x in range(max(0,int(s_x - my_d)), min(int(s_x + my_d), self.width)):
414 for y in range(max(0,int(s_y - my_d)), min(int(s_y + my_d), self.height)):
415 if my_d - 5 <= self.d[x,y] <= my_d + 5:
417 if my_d - 3 <= self.d[x,y] <= my_d + 3:
419 if my_d - 1 <= self.d[x,y] <= my_d + 1:
421 el[y,x,:]= (0,255,255,a)
424 def do_motion_notify_event(self, widget, event):
425 if 0<=event.x<self.width and 0<=event.y<self.height:
426 p = (int(round(event.x)),int(round(event.y)))
428 self.selected_d = self.d[p]
429 self.status.set_text("(%d,%d): %d" % (event.x, event.y, self.selected_d))
430 self.slider.set_value(self.selected_d)
432 if not self.graph_edit.props.active and not self.progress.props.sensitive:
435 if self.graph_edit.props.active:
436 n = self.graph.nearest_point(p)
437 if dist(n,p) < selection_distance:
440 self.hover_point = None
443 def do_change_value(self, widget, scroll, value):
445 if value != self.selected_d:
446 self.selected_d = value
447 self.status.set_text("Selected: %d" % (self.selected_d))
451 def show_edit_help(self,widget):
452 help = gtk.MessageDialog(parent = self.window,
453 type = gtk.MESSAGE_INFO,
454 buttons = gtk.BUTTONS_OK)
455 help.props.text = '''Graph editing:
456 Left click to select/unselect a vertex.
457 Left click any where else to add a new vertex.
458 Middle click to select center vertex.
459 Right click on the selected vertex to delete it, if it has no edges anymore.
460 Right click on another vertex to add or remove the edge.
461 Right click anywhere ot adda vertex and an edge in one go.'''
465 def queue_draw(self, widget=None):
466 self.image.queue_draw()
467 self.moved.queue_draw()
469 def update_gui(self, pulse=False):
471 if now - self.last_update > 0.1:
473 self.progress.pulse()
474 while gtk.events_pending():
475 gtk.main_iteration(False)
476 self.last_update = now
478 def prepare_progress(self):
479 self.progress.props.sensitive = True
480 self.progress.set_text('')
481 self.progress.set_fraction(0)
483 for button in self.do_buttons:
484 button.props.sensitive = False
486 def reset_progress(self):
487 self.progress.props.sensitive = False
488 self.progress.set_text('...idle...')
489 self.progress.set_fraction(0)
491 for button in self.do_buttons:
492 button.props.sensitive = True
495 def recalc_distance(self):
496 far = self.penalty.get_value_as_int() * (self.width + self.height)
498 self.prepare_progress()
499 self.progress.set_text('Preparing array')
501 d = Numeric.zeros((self.width,self.height), 'i')
502 for x in range(self.width):
503 self.update_gui(True)
504 for y in range(self.height):
507 d[self.graph.start] = 0
508 todo = [self.graph.start]
510 # unoptimized djikstra
511 self.progress.set_text('Djikstra')
515 s = min(todo, key=lambda e: d[e])
517 for t in ([t for (s2,t) in self.graph.edges if s2 == s ] +
518 [t for (t,s2) in self.graph.edges if s2 == s ]):
519 if d[s] + dist(s,t) < d[t]:
520 d[t] = d[s] + dist(s,t)
522 self.update_gui(True)
524 self.progress.set_text('Off-Graph')
525 for x in range(self.width):
526 self.progress.set_fraction(float(x)/float(self.width))
528 for y in range(self.height):
532 #(p1,p2) = min(graph, key = lambda e: dist2(find_footpoint(e,p),p))
533 #footpoint = find_footpoint((p1,p2),p)
534 #if d[footpoint] == far:
535 # d[footpoint] = min(d[p1] + dist(p1,footpoint),
536 # d[p2] + dist(p2,footpoint))
537 #d[p] = d[footpoint] + dist(p, footpoint)
540 #d[x,y] = min(map (lambda p1: d[p1] + 5*dist(p,p1), points))
543 for (p1,p2) in self.graph.edges:
544 f = find_footpoint((p1,p2),p)
546 d[f] = min(d[p1] + dist(p1,f), d[p2] + dist(p2,f))
547 d[p] = min(d[p], d[f] + self.penalty.get_value_as_int() * dist(f,p))
549 #self.progress.set_text('Dumping data')
550 #self.progress.set_fraction(0)
552 #pickle.dump(d, file('distance_map.data','w'))
556 self.reset_progress()
558 def recalc_heightmap(self):
560 self.recalc_distance()
563 self.prepare_progress()
564 self.progress.set_text('Recreating heightmap')
565 self.pixbuf_heightmap = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.width, self.height)
566 i = self.pixbuf_heightmap.get_pixels_array()
567 #i = Numeric.zeros((self.height,self.width,4), 'b')
568 for x in range(self.width):
569 self.progress.set_fraction(float(x)/float(self.width))
571 for y in range(self.height):
572 a = 255 - min(d[x,y]//3,255)
573 i[y,x,:]= (255,0,0,a)
575 #self.progress.set_text('Writing height data')
577 #pickle.dump(i, file('height_map.data','w'))
579 self.reset_progress()
581 def recalc_morph(self):
583 self.recalc_distance()
586 z = self.zoom.get_value()
588 self.prepare_progress()
589 self.progress.set_text('Calculating transformation')
590 f = Numeric.zeros((self.width,self.height,2),'i')
591 (cx,cy) = (self.width/2, self.height/2)
592 (sx,sy) = self.graph.start
593 for x in range(self.width):
594 self.progress.set_fraction(float(x)/float(self.width))
596 for y in range(self.height):
598 size = dist(p,self.graph.start) * z
600 npx = int(round(cx + (x-sx)*d[p]/size))
601 npy = int(round(cy + (y-sy)*d[p]/size))
602 if 0<= npx < self.width and 0<= npy < self.height:
607 #self.progress.set_text('Writing transformation data')
609 #pickle.dump(f,file('function.data','w'))
613 self.reset_progress()
615 self.prepare_progress()
616 self.progress.set_text('Calculating Morphed Image')
617 m = Numeric.zeros((self.height,self.width,3),'b')
618 o = self.pixbuf.get_pixels_array()
620 self.interpolate(o,m,f)
622 #self.progress.set_text('Writing morphed image')
624 #pickle.dump(m,file('output.data','w'))
629 self.pixbuf_moved = gtk.gdk.pixbuf_new_from_array(m, gtk.gdk.COLORSPACE_RGB, 8)
630 self.reset_progress()
632 def interpolate(self, o, m, f):
633 choice = self.interpolator.get_active_text()
634 self.interpolators[choice](o, m, f)
636 def interpolate_blocks(self, o, m, f):
637 for x in range(self.width):
638 self.progress.set_fraction(float(x)/float(self.width))
641 for y in range(self.height):
644 #print "Pixel data found directly"
648 for tx in range(x-i,x+i+1):
649 if 0 <= tx < self.width:
650 for ty in range(y-i,y+i+1):
651 if 0 <= ty < self.height:
652 if f[tx, ty] != (0,0):
655 #print "Neighboring pixels asked (%d)" % i
660 # print "Could not find pixel to take color from."
661 # m[y,x] = (255,255,0)
663 def interpolate_stripes(self, o, m, f):
664 for x in range(self.width):
665 self.progress.set_fraction(float(x)/float(self.width))
669 for y in range(self.height):
673 for my in range(prev+1,y):
675 float(my-prev)/float(y-prev),
680 def interpolate_none(self, o, m, f):
681 for x in range(self.width):
682 self.progress.set_fraction(float(x)/float(self.width))
685 for y in range(self.height):
689 def recalc_all(self):
690 self.recalc_distance()
691 self.recalc_heightmap()
694 def do_open_dialog(self, widget):
695 dialog = gtk.FileChooserDialog(title = "Open Image",
696 parent = self.window,
697 action = gtk.FILE_CHOOSER_ACTION_OPEN,
698 buttons = (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
699 gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
702 filename = dialog.get_filename()
704 self.load_files(filename)
707 def do_save_dialog(self, widget):
708 assert self.filename, "No file open at the moment"
710 help = gtk.MessageDialog(parent = self.window,
711 type = gtk.MESSAGE_INFO,
712 buttons = gtk.BUTTONS_OK)
713 help.props.text = 'Graph and calculated data saved'
717 def load_files(self, filename):
718 self.pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
719 self.width = self.pixbuf.props.width
720 self.height = self.pixbuf.props.height
721 self.filename = filename
725 self.pixbuf_heightmap = None
726 self.pixbuf_moved = None
727 self.moved_zoom = None
729 if os.path.exists(filename+'.graph'):
730 data = pickle.load(file(filename + '.graph'))
731 self.graph.load(data)
733 if os.path.exists(filename+'.data'):
734 data = pickle.load(file(filename + '.data'))
738 if 'i' in data and data['i']:
739 self.pixbuf_heightmap = gtk.gdk.pixbuf_new_from_array(data['i'],
740 gtk.gdk.COLORSPACE_RGB, 8)
741 if 'm' in data and data['m']:
743 self.pixbuf_moved = gtk.gdk.pixbuf_new_from_array(self.m,
744 gtk.gdk.COLORSPACE_RGB, 8)
746 self.moved_zoom = data['mz']
747 if 'penalty' in data:
748 self.penalty.set_value(data['penalty'])
750 def save_files(self):
751 assert self.filename, "No file open at the moment"
753 pickle.dump(self.graph.dump(), file(self.filename + '.graph','w'))
757 if self.pixbuf_heightmap:
758 data['i'] = self.pixbuf_heightmap.get_pixels_array()
759 if self.pixbuf_moved:
760 # Re-extracting pixel array from RGB without alpha
761 # and re-inserting causes strange shift, so let’s
762 # remember the array directly
764 data['mz'] = self.moved_zoom
765 data['penalty'] = self.penalty.get_value_as_int()
767 pickle.dump(data, file(self.filename + '.data','w'))
773 if __name__ == "__main__":
777 # vim:ts=4:sw=4:sts=4:et