Personal blog written from scratch using Node.js, Bootstrap, and MySQL. https://jrtechs.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

322 lines
13 KiB

  1. Alright, this post is long overdue; today, we are using quadtrees to partition images. I wrote this code before writing the post on [generic quad trees](https://jrtechs.net/data-science/implementing-a-quadtree-in-python). However, I haven't had time to turn it into a blog post until now. Let's dive right into this post where I use a custom quadtree implementation and OpenCV to partition images.
  2. But first, why might you want to use quadtrees on an image? In the last post on quadtrees, we discussed how quadtrees get used for efficient spatial search.
  3. That blog post covered point quadtrees where every element in the quadtree got represented as a single fixed point.
  4. With images, each node in the quadtree represents a region of the image.
  5. We can generate our quadtree in a similar fashion where instead of dividing based on how many points are in the region, we can divide based on the contrast in the cell.
  6. The end goal is to create partitions that minimize the contrast contained within each node/cell.
  7. By doing so, we can compress our image while preserving essential details.
  8. With that said, let's jump into the python code. Like most of my open CV projects, we start by importing the standard dependencies, loading a test image, and then defining some helper functions that easily display images in notebooks.
  9. The full Jupyter notebook for this post is in my [Random Scripts repository](https://github.com/jrtechs/RandomScripts/tree/master/notebooks) on Github.
  10. ```python
  11. # Open cv library
  12. import cv2
  13. # matplotlib for displaying the images
  14. from matplotlib import pyplot as plt
  15. import matplotlib.patches as patches
  16. import random
  17. import math
  18. import numpy as np
  19. img = cv2.imread('night2.jpg')
  20. def printI(img):
  21. fig= plt.figure(figsize=(20, 20))
  22. rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  23. plt.imshow(rgb)
  24. def printI2(i1, i2):
  25. fig= plt.figure(figsize=(20, 10))
  26. ax1 = fig.add_subplot(1,2,1)
  27. ax1.imshow(cv2.cvtColor(i1, cv2.COLOR_BGR2RGB))
  28. ax2 = fig.add_subplot(1,2,2)
  29. ax2.imshow(cv2.cvtColor(i2, cv2.COLOR_BGR2RGB))
  30. ```
  31. The test image is a long exposure shot of a street light with a full moon in the background. Notice that the moon and streetlight's overexposed nature blow them out, creating a star beam effect.
  32. We would expect to retain details in the moon and telephone pole when compressing the image.
  33. ```python
  34. printI(img)
  35. ```
  36. ![image of moon and street light](media/quadtree/output_3_0.png)
  37. Like our last implementation of a quadtree, a node is simply a representation of a spatial region.
  38. In our case, it is the top-left point of the image, followed by its width and height.
  39. To divide our image, we need to get a sense of node "purity."
  40. We are using the mean squared error of the pixels to determine that.
  41. Additionally, we are doing a weighted average of the color layers, favoring the green layer the most.
  42. Why we are favoring the green layer can be a point of future blog posts, but it has to do with the quantum efficiency of silicon to different types of light.
  43. I added normalization to our error function by dividing it by a large number; this makes it easier to tune the resulting hyperparameter in the end.
  44. ```python
  45. class Node():
  46. def __init__(self, x0, y0, w, h):
  47. self.x0 = x0
  48. self.y0 = y0
  49. self.width = w
  50. self.height = h
  51. self.children = []
  52. def get_width(self):
  53. return self.width
  54. def get_height(self):
  55. return self.height
  56. def get_points(self):
  57. return self.points
  58. def get_points(self, img):
  59. return img[self.x0:self.x0 + self.get_width(), self.y0:self.y0+self.get_height()]
  60. def get_error(self, img):
  61. pixels = self.get_points(img)
  62. b_avg = np.mean(pixels[:,:,0])
  63. b_mse = np.square(np.subtract(pixels[:,:,0], b_avg)).mean()
  64. g_avg = np.mean(pixels[:,:,1])
  65. g_mse = np.square(np.subtract(pixels[:,:,1], g_avg)).mean()
  66. r_avg = np.mean(pixels[:,:,2])
  67. r_mse = np.square(np.subtract(pixels[:,:,2], r_avg)).mean()
  68. e = r_mse * 0.2989 + g_mse * 0.5870 + b_mse * 0.1140
  69. return (e * img.shape[0]* img.shape[1])/90000000
  70. ```
  71. After we have our nodes, we can create our quadtree data structure.
  72. As a design decision, the image gets stored in the quadtree where the nodes only contain partitioning information and not the image itself.
  73. To recursively parse the tree or display it, we merely need to pass the image pointer around rather than have copies of the image at each node of the tree.
  74. Additionally, we add two visualization methods to the quadtree class. One that displays a wireframe view of the nodes, the other that visualizes each leaf node by rendering that region's average color.
  75. ```python
  76. class QTree():
  77. def __init__(self, stdThreshold, minPixelSize, img):
  78. self.threshold = stdThreshold
  79. self.min_size = minPixelSize
  80. self.minPixelSize = minPixelSize
  81. self.img = img
  82. self.root = Node(0, 0, img.shape[0], img.shape[1])
  83. def get_points(self):
  84. return img[self.root.x0:self.root.x0 + self.root.get_width(), self.root.y0:self.root.y0+self.root.get_height()]
  85. def subdivide(self):
  86. recursive_subdivide(self.root, self.threshold, self.minPixelSize, self.img)
  87. def graph_tree(self):
  88. fig = plt.figure(figsize=(10, 10))
  89. plt.title("Quadtree")
  90. c = find_children(self.root)
  91. print("Number of segments: %d" %len(c))
  92. for n in c:
  93. plt.gcf().gca().add_patch(patches.Rectangle((n.y0, n.x0), n.height, n.width, fill=False))
  94. plt.gcf().gca().set_xlim(0,img.shape[1])
  95. plt.gcf().gca().set_ylim(img.shape[0], 0)
  96. plt.axis('equal')
  97. plt.show()
  98. return
  99. def render_img(self, thickness = 1, color = (0,0,255)):
  100. imgc = self.img.copy()
  101. c = find_children(self.root)
  102. for n in c:
  103. pixels = n.get_points(self.img)
  104. # grb
  105. gAvg = math.floor(np.mean(pixels[:,:,0]))
  106. rAvg = math.floor(np.mean(pixels[:,:,1]))
  107. bAvg = math.floor(np.mean(pixels[:,:,2]))
  108. imgc[n.x0:n.x0 + n.get_width(), n.y0:n.y0+n.get_height(), 0] = gAvg
  109. imgc[n.x0:n.x0 + n.get_width(), n.y0:n.y0+n.get_height(), 1] = rAvg
  110. imgc[n.x0:n.x0 + n.get_width(), n.y0:n.y0+n.get_height(), 2] = bAvg
  111. if thickness > 0:
  112. for n in c:
  113. # Draw a rectangle
  114. imgc = cv2.rectangle(imgc, (n.y0, n.x0), (n.y0+n.get_height(), n.x0+n.get_width()), color, thickness)
  115. return imgc
  116. ```
  117. The recursive subdivision of a quadtree is very similar to that of a standard decision tree.
  118. We define two stopping criteria: node size and contrast.
  119. Like a decision tree, creating nodes that are too small is pedantic because it doesn't abstract the image and overfits.
  120. With an image, if we let our nodes become one pixel in size, it effectively just becomes the original image.
  121. Regarding contrast, if there is a lot of contrast, we want to continue dividing, where if there is little contrast, we want to stop dividing-- preserving global features of the image while throwing away local details.
  122. ```python
  123. def recursive_subdivide(node, k, minPixelSize, img):
  124. if node.get_error(img)<=k:
  125. return
  126. w_1 = int(math.floor(node.width/2))
  127. w_2 = int(math.ceil(node.width/2))
  128. h_1 = int(math.floor(node.height/2))
  129. h_2 = int(math.ceil(node.height/2))
  130. if w_1 <= minPixelSize or h_1 <= minPixelSize:
  131. return
  132. x1 = Node(node.x0, node.y0, w_1, h_1) # top left
  133. recursive_subdivide(x1, k, minPixelSize, img)
  134. x2 = Node(node.x0, node.y0+h_1, w_1, h_2) # btm left
  135. recursive_subdivide(x2, k, minPixelSize, img)
  136. x3 = Node(node.x0 + w_1, node.y0, w_2, h_1)# top right
  137. recursive_subdivide(x3, k, minPixelSize, img)
  138. x4 = Node(node.x0+w_1, node.y0+h_1, w_2, h_2) # btm right
  139. recursive_subdivide(x4, k, minPixelSize, img)
  140. node.children = [x1, x2, x3, x4]
  141. def find_children(node):
  142. if not node.children:
  143. return [node]
  144. else:
  145. children = []
  146. for child in node.children:
  147. children += (find_children(child))
  148. return children
  149. ```
  150. If we partition the same image using two different sets of hyperparameters, we can see how we can manipulate how much the quadtree algorithm partitions the image.
  151. If we set the sum of square error threshold low, the quadtree will produce many cells, where if we assign the threshold high, it will create fewer cells.
  152. ```python
  153. qtTemp = QTree(4, 3, img) #contrast threshold, min cell size, img
  154. qtTemp.subdivide() # recursively generates quad tree
  155. qtTemp.graph_tree()
  156. qtTemp2 = QTree(9, 5, img)
  157. qtTemp2.subdivide()
  158. qtTemp2.graph_tree()
  159. ```
  160. ![render of quadtree with small cells](media/quadtree/output_11_1.png)
  161. ![render of quadtree with large cells](media/quadtree/output_11_3.png)
  162. As a final esthetic, I want to display the rendered version alongside the original photograph.
  163. For the sake of simplicity, I am adding a white border surrounding the two images and contacting them together to form a diptych.
  164. ```python
  165. def concat_images(img1, img2, boarder=5, color=(255,255,255)):
  166. img1_boarder = cv2.copyMakeBorder(
  167. img1,
  168. boarder, #top
  169. boarder, #btn
  170. boarder, #left
  171. boarder, #right
  172. cv2.BORDER_CONSTANT,
  173. value=color
  174. )
  175. img2_boarder = cv2.copyMakeBorder(
  176. img2,
  177. boarder, #top
  178. boarder, #btn
  179. 0, #left
  180. boarder, #right
  181. cv2.BORDER_CONSTANT,
  182. value=color
  183. )
  184. return np.concatenate((img1_boarder, img2_boarder), axis=1)
  185. ```
  186. Next, we wrap our quadtree algorithm with our output visualization to make creating the diptychs easier.
  187. The left is the original image, where the right is the rendered quadtree version.
  188. Each leaf node in the quadtree gets visualized by taking the average pixel values from the cell.
  189. Moreover, I added an outline to each cell to emphasize the cells produced.
  190. I found that either a red or black outline worked the best.
  191. ```python
  192. def displayQuadTree(img_name, threshold=7, minCell=3, img_boarder=20, line_boarder=1, line_color=(0,0,255)):
  193. imgT= cv2.imread(img_name)
  194. qt = QTree(threshold, minCell, imgT)
  195. qt.subdivide()
  196. qtImg= qt.render_img(thickness=line_boarder, color=line_color)
  197. file_name = "output/" + img_name.split("/")[-1]
  198. cv2.imwrite(file_name,qtImg)
  199. file_name_2 = "output/diptych-" + img_name[-6] + img_name[-5] + ".jpg"
  200. hConcat = concat_images(imgT, qtImg, boarder=img_boarder, color=(255,255,255))
  201. cv2.imwrite(file_name_2,hConcat)
  202. printI(hConcat)
  203. displayQuadTree("night2.jpg", threshold=3, img_boarder=20, line_color=(0,0,0), line_boarder = 1)
  204. ```
  205. ![street light diptych](media/quadtree/output_16_0.png)
  206. Every time I see these images, I think about how humans, cameras, and algorithms view and interpret reality.
  207. ```python
  208. displayQuadTree("night4.jpg", threshold=5)
  209. ```
  210. ![magic studio sign diptych](media/quadtree/output_18_0.png)
  211. In particular, night photography pairs remarkably well with this algorithm since many esthetics that night photographers have picked up distorts reality.
  212. Humans can rarely see vivid stars in the night nor light trails, yet if you set a camera with a long enough exposure, you will capture just that: and it is beautiful.
  213. With the increased prevalence of post-processing and filters on cameras, the photos we see now are never perfect representations of reality.
  214. ```python
  215. displayQuadTree("../final/russell-final-1.jpg", threshold=12, line_color=(0,0,0))
  216. ```
  217. ![rit bus diptych](media/quadtree/output_20_0.png)
  218. I'm still vexed as to whether these distortions of reality are a good thing or a bad thing, or if this distinction is even pertinent.
  219. ```python
  220. displayQuadTree("../final/russell-final-4.jpg", threshold=12, line_color=(0,0,0))
  221. ```
  222. ![diptych of street lights](media/quadtree/output_22_0.png)
  223. I am intrigued as to how detail are both illuminated and hidden away using the quadtrees.
  224. The main composition of the image remains unchanged, yet the more subtle details are cast away.
  225. ```python
  226. displayQuadTree("../final/russell-final-7.jpg", threshold=12, line_color=(0,0,0))
  227. ```
  228. ![car headlight diptych](media/quadtree/output_24_0.png)
  229. ```python
  230. displayQuadTree("../final/russell-final-10.jpg", threshold=12, line_color=(0,0,255))
  231. ```
  232. ![night shadows diptych](media/quadtree/output_25_0.png)
  233. ```python
  234. displayQuadTree("../final/russell-final-14.jpg", threshold=12, line_color=(0,0,0))
  235. ```
  236. ![rit magic studio diptych](media/quadtree/output_26_0.png)
  237. Despite being intuitively aware of the differences between reality and the images we see, it is hard for our minds to quantify this stark difference.
  238. On the one hand, these images are the only thing that I have from my nights out at RIT doing photography, thus making these images evidence of my experience-- the only tangible thing I can cling onto.
  239. Yet, on the other hand, it fails to capture the essence of RIT at night altogether.
  240. I framed these images with a tripod, and their long exposure shots distort light in a way that the human eye can't perceive.
  241. The edits with both Lightroom and my python script further distorts the original scene.
  242. The problem with seeing the world through a camera is that you miss everything the camera doesn't see.
  243. I can't show you precisely what last night's sky looked like, not really, but you can see tonight's, and it will be beautiful.
  244. ![](media/quadtree/full.jpg)