title: How to limit number of colors in an image to a specific set in python
body: <p>I'm trying to create a python script that will take an image with n number of colors and reduce it to be f where f is a specified number of colors chosen by me, I found in similar questions how to get the major colors in an image, compare each pixel to them and assign the closest one. I was able to implement that but I wanted to change the script so that I could give it specific colors and check if they are first present in the image, after that I want to limit the number of pixel colors to those that I was able to find</p>
  <p>here's what I have:</p>
  <pre class="lang-py prettyprint-override"><code>import numpy as np
  from PIL import Image
  
  image = Image.open('cat.png')
  
  image_nd = np.array(image)
  print(image_nd.shape)
  image_colors = {}
  
  label_colors = [
      [124, 204, 4, 255], #Grass
      [252, 172, 204, 255], #Village
      [172, 204, 52, 255], #Trees
      [156, 156, 172, 255], #Snow
      [236, 52, 252, 255], #Shrooms
      [244, 236, 196, 255], #Sand
      [116, 148, 188, 255], #Rocks
      [172, 84, 36, 255], # Red Sand
      [236, 28, 36, 255], # Lava
      [156, 220, 236, 255], #Ice
      [108, 108, 44, 255], #Dirt
      [124, 52, 204, 255], #Cobblestone
      [180, 204, 252, 255] #Water
  ]
  
  image_labels = []
  
  # This bool is used to check if the label color is already in image_labels
  a = False
  # For each pixel
  for row in image_nd:
      for pxl in row:
          # check against the label_colors
          pxl = tuple(pxl)
          
          for color in label_colors:
              if pxl == color:
                  # check if we haven't added the color to image_labels already
                  for labels in image_labels:
                      if labels == color:
                          # if it's already there set the bool to be true
                          a = True
                  if a == False:
                      # if we haven't added the color to image_labels then added to it
                      image_labels.append(color)
  
              
  
  
  # this method takes the colors to be checked against in the image, and the image as a nparray
  def closest(colors, image):
      colors = np.array(colors).astype(np.int32)
      image = image.astype(np.int32)
      distances = np.argmin(np.array([np.sqrt(np.sum((color - image) ** 2, axis=2)) for color in colors]), axis=0)
      return colors[distances].astype(np.uint8)
  
  image_nd = closest(image_labels, image_nd)
  
  aliased = Image.fromarray(image_nd)
  aliased.save(&quot;pattern_2_al.png&quot;)
  </code></pre>
  <p>after running it though, nothing happens, It might be because the image is 11520x3840 so it might just take a long time, but I want to make sure I'm not doing anything wrong in my code or if maybe there's a better way to do this.
  Any help is greatly appreciated
  I apologize for any unclear bits in the question, this my first time posting a question and on top of that I'm very new to python and image processing</p>
  <p>Here's the script that generates the image of the map with the interpolation filter:</p>
  <pre class="lang-py prettyprint-override"><code>import os
  import PIL.Image
  import numpy as np
  import sys
  import png
  
  loc_dict = {1:(0,1),
              2:(1,1),
              3:(0,0),
              4:(1,0)}
  
  class level:
      def __init__(self, directory, image_file=None, depth = 1, x_loc=0, y_loc=0):
          self.children = []
          if image_file is None:
              image_file = directory + 'base.png'
          #make sure the image file exists
          assert os.path.isfile(image_file), 'Bad File:' + image_file
          self.image_file = image_file
          self.depth = depth
          self.x_loc = x_loc
          self.y_loc = y_loc
          #self.has_background = (0,0,0,0) in list(PIL.Image.open(self.image_file).getdata())
          for i in range(1,5):
              new_dir = directory + str(i) + '/'
              if os.path.isdir(new_dir):
                  child = level(directory=new_dir, image_file=directory + str(i) + '.png', depth = depth+1,
                                x_loc = self.x_loc*2+loc_dict[i][0], y_loc = self.y_loc*2+loc_dict[i][1])
                  self.children.append(child)
              else:
                  self.children.append(None)
      def has_children(self):
          return any(child is not None for child in self.children);
  
      def num_levels(self):
          return max(1 if child is None else child.num_levels()+1 for child in self.children)
  
      def num_leaves(self):
          if not self.has_children():
              return 1
          else:
              return sum(0 if child is None else child.num_leaves() for child in self.children)
  
      def num_leaves_with_background(self):
          if not self.has_children():
              return int(self.has_background)
          else:
              return sum(0 if child is None else child.num_leaves_with_background() for child in self.children)
          
      def gen_image(self):
          if not self.has_children():
              return np.array(PIL.Image.open(self.image_file))
          else:
              images = [None if child is None else child.gen_image() for child in self.children]
              shapes = [child.shape for child in filter(lambda x: x is not None, images)]
              #make sure they're all the same
              assert [shape == shape[0] for shape in shapes], 'Bad shapes' + str(shapes)
              images = [np.zeros(shapes[0], dtype=np.uint8) if image is None else image for image in images]
              top = np.concatenate((images[0],images[1]), axis = 1)
              bottom = np.concatenate((images[2],images[3]), axis = 1)
              full = np.concatenate((top,bottom), axis=0)
              return full
      def visit(self, visitor):
          visitor.visit(self)
          for child in self.children:
              if child is not None:
                  child.visit(visitor)
  
      def visit_leaves(self, visitor):
          if not self.has_children():
              visitor.visit(self)
          else:
              for child in self.children:
                  if child is not None:
  
                      child.visit_leaves(visitor)
  
  class Bounds:
      def __init__(self, x_min=sys.maxsize, x_max=0, y_min=sys.maxsize, y_max=0):
          self.x_min = x_min
          self.x_max = x_max
          self.y_min = y_min
          self.y_max = y_max
  
      def __str__(self):
          return '((' + str(self.x_min) + ',' + str(self.x_max) +')(' + str(self.y_min) + ',' + str(self.y_max) + '))'
  
                      
  class find_bounds:
      def __init__(self):
          self.bounds = Bounds()
  
      def visit(self, node):
          self.bounds.x_min = min(self.bounds.x_min, node.x_loc)
          self.bounds.y_min = min(self.bounds.y_min, node.y_loc)
          self.bounds.x_max = max(self.bounds.x_max, node.x_loc)
          self.bounds.y_max = max(self.bounds.y_max, node.y_loc)
  
          
      @staticmethod
      def find_bounds(top):
          fb = find_bounds()
          top.visit_leaves(fb)
          print('Bounds:', str(fb.bounds))
  
  
          
  class gen_image:
      def __init__(self, bounds):
          self.size = (bounds.x_max-bounds.x_min, bounds.y_max-bounds.y_min)
          self.bounds = bounds
          #Three dimentional array initialized with 0s 
          #The dimentions for the array are 
          #Number of columns = sizeY*384 
          #Number of rows = sizeX*384 
          #Number of z rows = 4 
          self.array = np.zeros((self.size[1]*384, self.size[0]*384, 4), dtype = np.uint8)
      def visit(self, node):
          #if this node is within these bounds...
          if (node.x_loc &gt;= self.bounds.x_min and
              node.x_loc &lt; self.bounds.x_max and
              node.y_loc &gt;= self.bounds.y_min and
              node.y_loc &lt; self.bounds.y_max):
              # initialize cur with the node image file
              cur = np.array(PIL.Image.open(node.image_file))
              self.array[(self.size[1]-(node.y_loc-self.bounds.y_min+1))*384:(self.size[1]-(node.y_loc-self.bounds.y_min))*384,
                         (node.x_loc-self.bounds.x_min)*384:(node.x_loc-self.bounds.x_min+1)*384,:] = cur
      def gen_image(top, bounds, filename):
          gi = gen_image(bounds)
          top.visit_leaves(gi)
          image  = PIL.Image.fromarray(gi.array)
          image.save(filename)
  
          ###################THIS IS MY CODE ABDERRAHIM JAMI####################
          
          #I tried to use the PIL convert function to reduce the number of colors in the palette. Unfortunately I don't 
          #have control over what the colors are as it only chooses the most frequent ones it can find in the image
          #Which is not always the case. There might be times where a shade of green from the antialiasing filter is more predominant than the shades of red used to represent 
          #specs of lava formations in the map....
          # result = image.convert('P', palette = PIL.Image.ADAPTIVE, colors = 13)
          # result.save(filename)
  
  
  
  ################# ANOTHER CHANGE I MADE ###########
  # class find_depth:
  #     def __init__(self):
  #         self.max_depth=0
  
  #     def visit(self
  # for i in range(50):
  #     name = str(i).zfill(3)
  #     dir_name = sys.argv[1] + '/' + name + '/tl/'
  #     print('Generating:', dir_name)
  #     top = level(dir_name)
  #     #find_bounds.find_bounds(top)
  #     gen_image.gen_image(top, Bounds(x_min=14, x_max=47, y_min=21, y_max=37), dir_name + '/full-' + name + '.png')
  
  
  ################# ANOTHER CHANGE I MADE ###########
  name = &quot;map_myworld_BR&quot;
  dir_name = sys.argv[1] + '/' + sys.argv[2] + '/br/'
  print('Generating:', dir_name)
  top = level(dir_name)
  #find_bounds.find_bounds(top)'
  
  
  
  
  #this is another change I made... the section of the map the fullmap image is generated from is limited by these bounds values.. by editing these values it is possible 
  #to center the map in the final fullmap image depeending on the orientation of the map in mapcrafter...
  
  #WE HAVE THE MAP RENDERED FROM 4 DIFFERENT ANGLES TL, TR, BR AND BL 
  # testBounds = Bounds(x_min=14, x_max=47, y_min=21, y_max=37)
  # testBounds = Bounds(x_min=60, x_max=70, y_min=55, y_max=59)
  
  
  #THIS IS FOR TL
  # testBounds = Bounds(x_min=25, x_max=55, y_min=60, y_max=70)
  #THIS IS FOR TOP-RIGHT
  # testBounds = Bounds(x_min=65, x_max=90, y_min=68, y_max=78)
  #THIS IS FOR BOTTOM-LEFT
  # testBounds = Bounds(x_min=75, x_max=90, y_min=50, y_max=60)
  
  
  #THIS IS FOR BOTTOM-RIGHT
  testBounds = Bounds(x_min=90, x_max=105, y_min=50, y_max=60)
  
  gen_image.gen_image(top, testBounds, dir_name + '/full-' + name + '.png')
  print(testBounds);
  
  
  #array = top.gen_image()
  #print('Shape:', array.shape)
  #image = PIL.Image.fromarray(array)
  #image = png.from_array(array, mode='RGBA')
  #image.save(sys.argv[1] + '/full.png')
  
  #print('Level:', top.num_levels())
  #print('Leaves:', top.num_leaves())
  #print('Leaves with background:', top.num_leaves_with_background())
  </code></pre>
  <p>The script traverses an octree folder structure where every level in the tree is a zoom level, the leaf nodes in the tree contain the actual images (map sections on maximum zoom) that are used by the script to generate the map, to generate the image file the script uses the function image.FromArray()
  The testBounds variable contains bounds which from my understanding delimits the area on the map from which the image is to be generated from</p>
  <p><a href="https://ibb.co/kSJktfQ" rel="nofollow noreferrer">https://ibb.co/kSJktfQ</a></p>
  <p>The image on the left is taken from the octree structure (you can see that there's no anti-aliasing applied) the screenshot on the right is taken from the image created by this script</p>
  <p>Thanks to anyone who's dedicating his time to helping me with this. I appreciate it</p>
  <p>Edit #3: I think I found a workaround ...
  I eventually calculate the Euclidian distance between each pixel in the image and each of the label_colors I want the image to contain and assign the closest one. Like so:</p>
  <pre class="lang-py prettyprint-override"><code>import numpy as np
  from PIL import Image
  #Opens the image 
  image = Image.open('full-map_myworld_12_05_21.png')
  #Stores it as a numpy array
  image_nd = np.array(image)
  #Prints out the shape of array for debugging
  print(image_nd.shape)
  
  #Simply what the name says.. 
  label_colors = [
  
      [124, 204, 4, 252], #Grass
      [252, 172, 204, 252], #Village
      [172, 204, 52, 252], #Trees
      [156, 156, 172, 252], #Snow
      [236, 52, 252, 252], #Shrooms
      [244, 236, 196, 252], #Sand
      [116, 148, 188, 252], #Rocks
      [172, 84, 36, 252], # Red Sand
      [236, 28, 36, 252], # Lava
      [156, 220, 236, 252], #Ice
      [108, 108, 44, 252], #Dirt
      [124, 52, 204, 252], #Cobblestone
      [180, 204, 252, 252] #Water
  ]
  
  def closest(colors, image):
      #Converts elements into int32 
      colors = np.array(colors).astype(np.int32)
      image = image.astype(np.int32)
      #Finds Euclidean distance between each pixel in the image and the colors in the label_colors list
      distances = np.argmin(np.array([np.sqrt(np.sum((color - image) ** 2, axis=2)) for color in colors]), axis=0)
      return colors[distances].astype(np.uint8)
  
  
  
  image_nd = closest(label_colors, image_nd)
  #Creates the image from an array returned from closest
  aliased = Image.fromarray(image_nd)
  #Saves it with the given name.
  aliased.save(&quot;pattern_2_al.png&quot;)
  </code></pre>
  <p>It does get the job done... sort of, some parts of the image where the antialiasing is applied doesn't change the color as well as I would want but it's a step closer ...
  here a comparison
  <a href="https://ibb.co/rmNbxw3" rel="nofollow noreferrer">https://ibb.co/rmNbxw3</a></p>