Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I optimize the palette image size with PIL?

My goal is to draw some polygons on a black image such that the total size of the resulting image is as small as possible.

So I read an article on wiki about indexed colors (link) and it seems like it's a good choice for me (since I should support only a black color and the 5 other ones, i.e. 6 colors in total) and png image format should support 'P' mode (i.e., palette images).

That's the reason I created this piece of code to see what image size I'll get for 6 colors and 1224x1024 image:

from PIL import Image, ImageDraw
# Create a black 1224x1024 image
img = Image.new('P', (1224, 1024))
img.putpalette([
    0, 0, 0,  # black background
    236, 98, 98, # red color
    236, 98, 97,
    236, 98, 96,
    236, 98, 95,
    236, 98, 94,
])

draw = ImageDraw.Draw(img)
# Draw a random red polygon at the top left corner of the image
draw.polygon(xy=list(1,1,2,2,3,3,4,4), fill=1)
del draw

img.save('1.png', format='PNG')

The result image size is 768 bytes which seems too much for me.

Is there something I can fix in my code to make the result image size even smaller?

like image 362
James Larkin Avatar asked Dec 09 '25 09:12

James Larkin


1 Answers

768 bytes does not seem unreasonable to me to represent a 1.2 megapixel image. You could try running the file produced by PIL through pngcrush like this to see if it can shave a few bytes:

pngcrush input.png result.png

If you really only want to draw a few solid-coloured polygons on a black background, I would suggest you look to a vector format, such as SVG example here rather than raster format PNG et al.

You can also use rsvg to render SVG images to PNG if you need to but neither your application, nor the reason you need such small images is clear from your question, so I have no idea if that is an option for you.

Here is a 300 byte SVG image with a black background, 2 rectangles and a polygon like a red star shape at top-left:

<svg width="1224" height="1024">
  <rect width="100%" height="100%" fill="black"/>
  <polygon points="9.9, 1.1, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78" fill="red"/>
  <rect x="100" y="200" width="100" height="400" fill="blue"/>
  <rect x="800" y="280" width="100" height="200" fill="lime"/>
</svg>

enter image description here


You could load an SVG into a Numpy array like this:

#!/usr/bin/env python3

import cairosvg
import io
from PIL import Image

def svgRead(filename):
   """Load an SVG file and return image in Numpy array"""
   # Make memory buffer
   mem = io.BytesIO()
   # Convert SVG to PNG in memory
   cairosvg.svg2png(url=filename, write_to=mem)
   # Convert PNG to Numpy array
   return np.array(Image.open(mem))

# Read SVG file into Numpy array
res = svgRead('image.svg')

If you are hell-bent on making the files even smaller, at the price of reduced compatibility with other image viewers, you could devise your own very simple format somewhat similar to SVG, so you could store the image represented by the SVG example I gave as a simple text file:

b,1224,1024,#00000
r,100,200,100,400,#0000ff
r,800,280,100,200,#00ff00
p,9.9,1.1,3.3,21.78,19.8,8.58,0,8.58,16.5,21.78,#ff0000

And I make that 127 bytes.


You can view your SVG files by loading them into any web-browser, or make one into a PNG with ImageMagick in Terminal like this:

magick input.svg result.png
like image 50
Mark Setchell Avatar answered Dec 10 '25 23:12

Mark Setchell



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!