Advertisement
Guest User

Untitled

a guest
Apr 24th, 2019
74
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.32 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. # Advanced zoom for images of various types from small to huge up to several GB
  3. import math
  4. import warnings
  5. import tkinter as tk
  6.  
  7. from tkinter import ttk
  8. from PIL import Image, ImageTk
  9.  
  10. class AutoScrollbar(ttk.Scrollbar):
  11. """ A scrollbar that hides itself if it's not needed. Works only for grid geometry manager """
  12. def set(self, lo, hi):
  13. if float(lo) <= 0.0 and float(hi) >= 1.0:
  14. self.grid_remove()
  15. else:
  16. self.grid()
  17. ttk.Scrollbar.set(self, lo, hi)
  18.  
  19. def pack(self, **kw):
  20. raise tk.TclError('Cannot use pack with the widget ' + self.__class__.__name__)
  21.  
  22. def place(self, **kw):
  23. raise tk.TclError('Cannot use place with the widget ' + self.__class__.__name__)
  24.  
  25. class CanvasImage:
  26. """ Display and zoom image """
  27. def __init__(self, placeholder, path):
  28. """ Initialize the ImageFrame """
  29. self.imscale = 1.0 # scale for the canvas image zoom, public for outer classes
  30. self.__delta = 1.3 # zoom magnitude
  31. self.__filter = Image.ANTIALIAS # could be: NEAREST, BILINEAR, BICUBIC and ANTIALIAS
  32. self.__previous_state = 0 # previous state of the keyboard
  33. self.path = path # path to the image, should be public for outer classes
  34. # Create ImageFrame in placeholder widget
  35. self.__imframe = ttk.Frame(placeholder) # placeholder of the ImageFrame object
  36. # Vertical and horizontal scrollbars for canvas
  37. hbar = AutoScrollbar(self.__imframe, orient='horizontal')
  38. vbar = AutoScrollbar(self.__imframe, orient='vertical')
  39. hbar.grid(row=1, column=0, sticky='we')
  40. vbar.grid(row=0, column=1, sticky='ns')
  41. # Create canvas and bind it with scrollbars. Public for outer classes
  42. self.canvas = tk.Canvas(self.__imframe, highlightthickness=0,
  43. xscrollcommand=hbar.set, yscrollcommand=vbar.set)
  44. self.canvas.grid(row=0, column=0, sticky='nswe')
  45. self.canvas.update() # wait till canvas is created
  46. hbar.configure(command=self.__scroll_x) # bind scrollbars to the canvas
  47. vbar.configure(command=self.__scroll_y)
  48. # Bind events to the Canvas
  49. self.canvas.bind('<Configure>', lambda event: self.__show_image()) # canvas is resized
  50. self.canvas.bind('<ButtonPress-1>', self.__move_from) # remember canvas position
  51. self.canvas.bind('<B1-Motion>', self.__move_to) # move canvas to the new position
  52. self.canvas.bind('<MouseWheel>', self.__wheel) # zoom for Windows and MacOS, but not Linux
  53. self.canvas.bind('<Button-5>', self.__wheel) # zoom for Linux, wheel scroll down
  54. self.canvas.bind('<Button-4>', self.__wheel) # zoom for Linux, wheel scroll up
  55. # Handle keystrokes in idle mode, because program slows down on a weak computers,
  56. # when too many key stroke events in the same time
  57. self.canvas.bind('<Key>', lambda event: self.canvas.after_idle(self.__keystroke, event))
  58. # Decide if this image huge or not
  59. self.__huge = False # huge or not
  60. self.__huge_size = 14000 # define size of the huge image
  61. self.__band_width = 1024 # width of the tile band
  62. Image.MAX_IMAGE_PIXELS = 1000000000 # suppress DecompressionBombError for the big image
  63. with warnings.catch_warnings(): # suppress DecompressionBombWarning
  64. warnings.simplefilter('ignore')
  65. self.__image = Image.open(self.path) # open image, but down't load it
  66. self.imwidth, self.imheight = self.__image.size # public for outer classes
  67. if self.imwidth * self.imheight > self.__huge_size * self.__huge_size and \
  68. self.__image.tile[0][0] == 'raw': # only raw images could be tiled
  69. self.__huge = True # image is huge
  70. self.__offset = self.__image.tile[0][2] # initial tile offset
  71. self.__tile = [self.__image.tile[0][0], # it have to be 'raw'
  72. [0, 0, self.imwidth, 0], # tile extent (a rectangle)
  73. self.__offset,
  74. self.__image.tile[0][3]] # list of arguments to the decoder
  75. self.__min_side = min(self.imwidth, self.imheight) # get the smaller image side
  76. # Create image pyramid
  77. self.__pyramid = [self.smaller()] if self.__huge else [Image.open(self.path)]
  78. # Set ratio coefficient for image pyramid
  79. self.__ratio = max(self.imwidth, self.imheight) / self.__huge_size if self.__huge else 1.0
  80. self.__curr_img = 0 # current image from the pyramid
  81. self.__scale = self.imscale * self.__ratio # image pyramide scale
  82. self.__reduction = 2 # reduction degree of image pyramid
  83. w, h = self.__pyramid[-1].size
  84. while w > 512 and h > 512: # top pyramid image is around 512 pixels in size
  85. w /= self.__reduction # divide on reduction degree
  86. h /= self.__reduction # divide on reduction degree
  87. self.__pyramid.append(self.__pyramid[-1].resize((int(w), int(h)), self.__filter))
  88. # Put image into container rectangle and use it to set proper coordinates to the image
  89. self.container = self.canvas.create_rectangle((0, 0, self.imwidth, self.imheight), width=0)
  90. self.__show_image() # show image on the canvas
  91. self.canvas.focus_set() # set focus on the canvas
  92.  
  93. def smaller(self):
  94. """ Resize image proportionally and return smaller image """
  95. w1, h1 = float(self.imwidth), float(self.imheight)
  96. w2, h2 = float(self.__huge_size), float(self.__huge_size)
  97. aspect_ratio1 = w1 / h1
  98. aspect_ratio2 = w2 / h2 # it equals to 1.0
  99. if aspect_ratio1 == aspect_ratio2:
  100. image = Image.new('RGB', (int(w2), int(h2)))
  101. k = h2 / h1 # compression ratio
  102. w = int(w2) # band length
  103. elif aspect_ratio1 > aspect_ratio2:
  104. image = Image.new('RGB', (int(w2), int(w2 / aspect_ratio1)))
  105. k = h2 / w1 # compression ratio
  106. w = int(w2) # band length
  107. else: # aspect_ratio1 < aspect_ration2
  108. image = Image.new('RGB', (int(h2 * aspect_ratio1), int(h2)))
  109. k = h2 / h1 # compression ratio
  110. w = int(h2 * aspect_ratio1) # band length
  111. i, j, n = 0, 1, round(0.5 + self.imheight / self.__band_width)
  112. while i < self.imheight:
  113. print('\rOpening image: {j} from {n}'.format(j=j, n=n), end=='')
  114. band = min(self.__band_width, self.imheight - i) # width of the tile band
  115. self.__tile[1][3] = band # set band width
  116. self.__tile[2] = self.__offset + self.imwidth * i * 3 # tile offset (3 bytes per pixel)
  117. self.__image.close()
  118. self.__image = Image.open(self.path) # reopen / reset image
  119. self.__image.size = (self.imwidth, band) # set size of the tile band
  120. self.__image.tile = [self.__tile] # set tile
  121. cropped = self.__image.crop((0, 0, self.imwidth, band)) # crop tile band
  122. image.paste(cropped.resize((w, int(band * k)+1), self.__filter), (0, int(i * k)))
  123. i += band
  124. j += 1
  125. print('\r' + 30*' ' + '\r', end=='') # hide printed string
  126. return image
  127.  
  128. def redraw_figures(self):
  129. """ Dummy function to redraw figures in the children classes """
  130. pass
  131.  
  132. def grid(self, **kw):
  133. """ Put CanvasImage widget on the parent widget """
  134. self.__imframe.grid(**kw) # place CanvasImage widget on the grid
  135. self.__imframe.grid(sticky='nswe') # make frame container sticky
  136. self.__imframe.rowconfigure(0, weight=1) # make canvas expandable
  137. self.__imframe.columnconfigure(0, weight=1)
  138.  
  139. def pack(self, **kw):
  140. """ Exception: cannot use pack with this widget """
  141. raise Exception('Cannot use pack with the widget ' + self.__class__.__name__)
  142.  
  143. def place(self, **kw):
  144. """ Exception: cannot use place with this widget """
  145. raise Exception('Cannot use place with the widget ' + self.__class__.__name__)
  146.  
  147. # noinspection PyUnusedLocal
  148. def __scroll_x(self, *args, **kwargs):
  149. """ Scroll canvas horizontally and redraw the image """
  150. self.canvas.xview(*args) # scroll horizontally
  151. self.__show_image() # redraw the image
  152.  
  153. # noinspection PyUnusedLocal
  154. def __scroll_y(self, *args, **kwargs):
  155. """ Scroll canvas vertically and redraw the image """
  156. self.canvas.yview(*args) # scroll vertically
  157. self.__show_image() # redraw the image
  158.  
  159. def __show_image(self):
  160. """ Show image on the Canvas. Implements correct image zoom almost like in Google Maps """
  161. box_image = self.canvas.coords(self.container) # get image area
  162. box_canvas = (self.canvas.canvasx(0), # get visible area of the canvas
  163. self.canvas.canvasy(0),
  164. self.canvas.canvasx(self.canvas.winfo_width()),
  165. self.canvas.canvasy(self.canvas.winfo_height()))
  166. box_img_int = tuple(map(int, box_image)) # convert to integer or it will not work properly
  167. # Get scroll region box
  168. box_scroll = [min(box_img_int[0], box_canvas[0]), min(box_img_int[1], box_canvas[1]),
  169. max(box_img_int[2], box_canvas[2]), max(box_img_int[3], box_canvas[3])]
  170. # Horizontal part of the image is in the visible area
  171. if box_scroll[0] == box_canvas[0] and box_scroll[2] == box_canvas[2]:
  172. box_scroll[0] = box_img_int[0]
  173. box_scroll[2] = box_img_int[2]
  174. # Vertical part of the image is in the visible area
  175. if box_scroll[1] == box_canvas[1] and box_scroll[3] == box_canvas[3]:
  176. box_scroll[1] = box_img_int[1]
  177. box_scroll[3] = box_img_int[3]
  178. # Convert scroll region to tuple and to integer
  179. self.canvas.configure(scrollregion=tuple(map(int, box_scroll))) # set scroll region
  180. x1 = max(box_canvas[0] - box_image[0], 0) # get coordinates (x1,y1,x2,y2) of the image tile
  181. y1 = max(box_canvas[1] - box_image[1], 0)
  182. x2 = min(box_canvas[2], box_image[2]) - box_image[0]
  183. y2 = min(box_canvas[3], box_image[3]) - box_image[1]
  184. if int(x2 - x1) > 0 and int(y2 - y1) > 0: # show image if it in the visible area
  185. if self.__huge and self.__curr_img < 0: # show huge image
  186. h = int((y2 - y1) / self.imscale) # height of the tile band
  187. self.__tile[1][3] = h # set the tile band height
  188. self.__tile[2] = self.__offset + self.imwidth * int(y1 / self.imscale) * 3
  189. self.__image.close()
  190. self.__image = Image.open(self.path) # reopen / reset image
  191. self.__image.size = (self.imwidth, h) # set size of the tile band
  192. self.__image.tile = [self.__tile]
  193. image = self.__image.crop((int(x1 / self.imscale), 0, int(x2 / self.imscale), h))
  194. else: # show normal image
  195. image = self.__pyramid[max(0, self.__curr_img)].crop( # crop current img from pyramid
  196. (int(x1 / self.__scale), int(y1 / self.__scale),
  197. int(x2 / self.__scale), int(y2 / self.__scale)))
  198. #
  199. imagetk = ImageTk.PhotoImage(image.resize((int(x2 - x1), int(y2 - y1)), self.__filter))
  200. imageid = self.canvas.create_image(max(box_canvas[0], box_img_int[0]),
  201. max(box_canvas[1], box_img_int[1]),
  202. anchor='nw', image=imagetk)
  203. self.canvas.lower(imageid) # set image into background
  204. self.canvas.imagetk = imagetk # keep an extra reference to prevent garbage-collection
  205.  
  206. def __move_from(self, event):
  207. """ Remember previous coordinates for scrolling with the mouse """
  208. self.canvas.scan_mark(event.x, event.y)
  209.  
  210. def __move_to(self, event):
  211. """ Drag (move) canvas to the new position """
  212. self.canvas.scan_dragto(event.x, event.y, gain=1)
  213. self.__show_image() # zoom tile and show it on the canvas
  214.  
  215. def outside(self, x, y):
  216. """ Checks if the point (x,y) is outside the image area """
  217. bbox = self.canvas.coords(self.container) # get image area
  218. if bbox[0] < x < bbox[2] and bbox[1] < y < bbox[3]:
  219. return False # point (x,y) is inside the image area
  220. else:
  221. return True # point (x,y) is outside the image area
  222.  
  223. def __wheel(self, event):
  224. """ Zoom with mouse wheel """
  225. x = self.canvas.canvasx(event.x) # get coordinates of the event on the canvas
  226. y = self.canvas.canvasy(event.y)
  227. if self.outside(x, y): return # zoom only inside image area
  228. scale = 1.0
  229. # Respond to Linux (event.num) or Windows (event.delta) wheel event
  230. if event.num == 5 or event.delta == -120: # scroll down, smaller
  231. if round(self.__min_side * self.imscale) < 30: return # image is less than 30 pixels
  232. self.imscale /= self.__delta
  233. scale /= self.__delta
  234. if event.num == 4 or event.delta == 120: # scroll up, bigger
  235. i = min(self.canvas.winfo_width(), self.canvas.winfo_height()) >> 1
  236. if i < self.imscale: return # 1 pixel is bigger than the visible area
  237. self.imscale *= self.__delta
  238. scale *= self.__delta
  239. # Take appropriate image from the pyramid
  240. k = self.imscale * self.__ratio # temporary coefficient
  241. self.__curr_img = min((-1) * int(math.log(k, self.__reduction)), len(self.__pyramid) - 1)
  242. self.__scale = k * math.pow(self.__reduction, max(0, self.__curr_img))
  243. #
  244. self.canvas.scale('all', x, y, scale, scale) # rescale all objects
  245. # Redraw some figures before showing image on the screen
  246. self.redraw_figures() # method for child classes
  247. self.__show_image()
  248.  
  249. def __keystroke(self, event):
  250. """ Scrolling with the keyboard.
  251. Independent from the language of the keyboard, CapsLock, <Ctrl>+<key>, etc. """
  252. if event.state - self.__previous_state == 4: # means that the Control key is pressed
  253. pass # do nothing if Control key is pressed
  254. else:
  255. self.__previous_state = event.state # remember the last keystroke state
  256. # Up, Down, Left, Right keystrokes
  257. if event.keycode in [68, 39, 102]: # scroll right, keys 'd' or 'Right'
  258. self.__scroll_x('scroll', 1, 'unit', event=event)
  259. elif event.keycode in [65, 37, 100]: # scroll left, keys 'a' or 'Left'
  260. self.__scroll_x('scroll', -1, 'unit', event=event)
  261. elif event.keycode in [87, 38, 104]: # scroll up, keys 'w' or 'Up'
  262. self.__scroll_y('scroll', -1, 'unit', event=event)
  263. elif event.keycode in [83, 40, 98]: # scroll down, keys 's' or 'Down'
  264. self.__scroll_y('scroll', 1, 'unit', event=event)
  265.  
  266. def crop(self, bbox):
  267. """ Crop rectangle from the image and return it """
  268. if self.__huge: # image is huge and not totally in RAM
  269. band = bbox[3] - bbox[1] # width of the tile band
  270. self.__tile[1][3] = band # set the tile height
  271. self.__tile[2] = self.__offset + self.imwidth * bbox[1] * 3 # set offset of the band
  272. self.__image.close()
  273. self.__image = Image.open(self.path) # reopen / reset image
  274. self.__image.size = (self.imwidth, band) # set size of the tile band
  275. self.__image.tile = [self.__tile]
  276. return self.__image.crop((bbox[0], 0, bbox[2], band))
  277. else: # image is totally in RAM
  278. return self.__pyramid[0].crop(bbox)
  279.  
  280. def destroy(self):
  281. """ ImageFrame destructor """
  282. self.__image.close()
  283. map(lambda i: i.close, self.__pyramid) # close all pyramid images
  284. del self.__pyramid[:] # delete pyramid list
  285. del self.__pyramid # delete pyramid variable
  286. self.canvas.destroy()
  287. self.__imframe.destroy()
  288.  
  289. class MainWindow(ttk.Frame):
  290. """ Main window class """
  291. def __init__(self, mainframe, path):
  292. """ Initialize the main Frame """
  293. ttk.Frame.__init__(self, master=mainframe)
  294. self.master.title('Advanced Zoom v3.0')
  295. self.master.geometry('800x600') # size of the main window
  296. self.master.rowconfigure(0, weight=1) # make the CanvasImage widget expandable
  297. self.master.columnconfigure(0, weight=1)
  298. canvas = CanvasImage(self.master, path) # create widget
  299. canvas.grid(row=0, column=0) # show widget
  300.  
  301. filename = 'Leaf Test.jpg' # place path to your image here
  302. #filename = 'd:/Data/yandex_z18_1-1.tif' # huge TIFF file 1.4 GB
  303. #filename = 'd:/Data/The_Garden_of_Earthly_Delights_by_Bosch_High_Resolution.jpg'
  304. #filename = 'd:/Data/The_Garden_of_Earthly_Delights_by_Bosch_High_Resolution.tif'
  305. #filename = 'd:/Data/heic1502a.tif'
  306. #filename = 'd:/Data/land_shallow_topo_east.tif'
  307. #filename = 'd:/Data/X1D5_B0002594.3FR'
  308. app = MainWindow(tk.Tk(), path=filename)
  309. app.mainloop()
  310.  
  311. import Tkinter as tk
  312. root = tk.Tk()
  313.  
  314. def motion(event):
  315. x, y = event.x, event.y
  316. print('{}, {}'.format(x, y))
  317.  
  318. root.bind('<Motion>', motion)
  319. root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement