Open In App

Image Manipulation Using Quadtrees

Improve
Improve
Like Article
Like
Save
Share
Report

Quadtrees are an effective method to store and locate data of points in a two-dimensional plane. Another effective use of quadtrees is in the field of image manipulation.

Unlike in storage of points, in image manipulation we get a complete quadtree with the leaf nodes consisting of individual pixels of the image. Due to this, we can utilize an array to store the nodes of the tree. This leads to less memory needed (compared to linked representation) for processing.

The lowest level of the quadtree would contain N nodes equivalent to the number of pixels in the image. The next level would contain N / 4 nodes.
Thus, the total number of nodes necessary can be found by: N + N / 4 + N / 16 + … + 1.
To get an upperbound we can use sum of geometric progression to infinity
Nodes = N / (1 – 1 / 4) = 4 / 3 * N
This is the size of the array necessary.
Thus, the amount of memory needed is O(N)




class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
class Pixel(object):
    def __init__(self, color = [0, 0, 0], 
            topLeft = Point(0, 0), 
            bottomRight = Point(0, 0)):
        self.R = color[0]
        self.G = color[1]
        self.B = color[2]
        self.topLeft = topLeft
        self.bottomRight = bottomRight


The above code in Python shown demonstrates the class definition of a pixel.

Insertion
Unlike in a classic quadtree, for image manipulation, we can insert all the nodes in O(N) time complexity.
First, we insert all the leaf nodes directly into the last N positions of the array. The following code snippet demonstrates this:




# Store leaves into array
count = 0
for i in range(image.size[0] - 1, 0, -2):
    for j in range(image.size[1] - 1, 0, -2):
        self.tree[self.size - 1 - 4 * count] = Pixel(self.image[i, j], 
                Point(i, j), 
                Point(i, j))
        self.tree[self.size - 2 - 4 * count] = Pixel(self.image[i, j - 1], 
                Point(i, j - 1),
                Point(i, j - 1))
        self.tree[self.size - 3 - 4 * count] = Pixel(self.image[i - 1, j], 
                Point(i - 1, j), 
                Point(i - 1, j))
        self.tree[self.size - 4 - 4 * count] = Pixel(self.image[i - 1, j - 1], 
                Point(i - 1, j - 1), 
                Point(i - 1, j - 1))
        count += 1


In the above snippet, self.tree is the array of nodes, self.size is the total size of the array, and self.image is the pixels of the image, count is used to traverse the tree.

For nodes that aren’t leaves, the R, G, B values are calculated by taking the average of the values of the children.
The topLeft and bottomRight are obtained by taking the maximum and minimum values of the x and y of the children.




# Calculate and create parent nodes
for i in range(self.size - 4 * count - 1, -1, -1):
    self.tree[i] = Pixel(
        [(self.tree[4 * i + 1].R + self.tree[4 * i + 2].R + self.tree[4 * i + 3].R + self.tree[4 * i + 4].R) / 4,
         (self.tree[4 * i + 1].G + self.tree[4 * i + 2].G + self.tree[4 * i + 3].G + self.tree[4 * i + 4].G) / 4,
         (self.tree[4 * i + 1].B + self.tree[4 * i + 2].B + self.tree[4 * i + 3].B + self.tree[4 * i + 4].B) / 4],
        self.tree[4 * i + 1].topLeft,
        self.tree[4 * i + 4].bottomRight)


Here we can see, we take the values of R, G, B of the four children, and we divide by 4, thus getting the average.

Calculation of these values happen in O(1) time assuming values of the children are known. Since we are moving in reverse order, the values of the children are computed before that of parents.
Thus insertion occurs in O(N).

Application in Image Manipulation
Let us say we wish to convert a high quality image to a thumbnail. Once we have created a quadtree for the image, by selecting a height of the quadtree we can select the quality of the image we obtain. If the height is equal to the height of the quadtree, then we retain the original image. At lower heights, we obtain images of lesser quality.

In case we do not wish to reduce the quality of the image, we can try to compress the image by what is known as “pruning”. In this, leafs with colours close to that of their parents are removed. This is continuously done until no further leaves can be removed. The lowest level is then taken to form the image, using only leaf nodes. While this does not reduce the quality of the image drastically, it can lead to a small compression of the image.

Complete Code:




from PIL import Image
import math
class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
  
class Pixel(object):
    def __init__(self, color = [0, 0, 0], 
            topLeft = Point(0, 0), 
            bottomRight = Point(0, 0)):
        self.R = color[0]
        self.G = color[1]
        self.B = color[2]
        self.topLeft = topLeft
        self.bottomRight = bottomRight
  
class quadtree():
    def __init__(self, image):
  
        # Total number of nodes of tree
        self.size = 0
  
        # Store image pixelmap
        self.image = image.load()
  
        # Array of nodes
        self.tree = []
        self.x = image.size[0]
        self.y = image.size[1]
  
        # Total number of leaf nodes
        size = image.size[0] * image.size[1]
  
        # Count number of nodes
        while(size >= 1):
            self.size += size
            size /= 4
  
        size = image.size[0] * image.size[1]
  
        # Initialize array elements
        for i in range(0, self.size):
            self.tree.append(Pixel())
  
        # Store leaves into array
        count = 0
        for i in range(image.size[0] - 1, 0, -2):
            for j in range(image.size[1] - 1, 0, -2):
                self.tree[self.size - 1 - 4 * count] = Pixel(self.image[i, j], 
                        Point(i, j), 
                        Point(i, j))
                self.tree[self.size - 2 - 4 * count] = Pixel(self.image[i, j - 1], 
                        Point(i, j - 1),
                        Point(i, j - 1))
                self.tree[self.size - 3 - 4 * count] = Pixel(self.image[i - 1, j], 
                        Point(i - 1, j), 
                        Point(i - 1, j))
                self.tree[self.size - 4 - 4 * count] = Pixel(self.image[i - 1, j - 1], 
                        Point(i - 1, j - 1), 
                        Point(i - 1, j - 1))
                count += 1
  
        # Calculate and create parent nodes
        for i in range(self.size - 4 * count - 1, -1, -1):
            self.tree[i] = Pixel(
                [(self.tree[4 * i + 1].R + self.tree[4 * i + 2].R + self.tree[4 * i + 3].R + self.tree[4 * i + 4].R) / 4,
                 (self.tree[4 * i + 1].G + self.tree[4 * i + 2].G + self.tree[4 * i + 3].G + self.tree[4 * i + 4].G) / 4,
                 (self.tree[4 * i + 1].B + self.tree[4 * i + 2].B + self.tree[4 * i + 3].B + self.tree[4 * i + 4].B) / 4],
                self.tree[4 * i + 1].topLeft,
                self.tree[4 * i + 4].bottomRight)
  
    def disp(self, level):
        start = 0
  
        # Calculate position of starting node of height
        for i in range(0, level):
            start = 4 * start + 1
  
        # Invalid height given
        if (start > self.size):
            return
  
        # Create a new image
        img = Image.new("RGB", (self.x, self.y), "black")
        pixels = img.load()
  
        # Move from starting to last node on given height
        for i in self.tree[start : 4 * start]:
            x1 = i.topLeft.x
            y1 = i.topLeft.y
            x2 = i.bottomRight.x
            y2 = i.bottomRight.y
            for x in range(x1, x2 + 1):
                for y in range(y1, y2 + 1):
  
                    # Set colour
                    pixels[x, y] = (i.R, i.G, i.B)
  
        # Display image
        img.show()




Last Updated : 09 Feb, 2018
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads