How can I optimize the palette image size with PIL

2019-07-08 03:08发布

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?

1条回答
狗以群分
2楼-- · 2019-07-08 03:18

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
查看更多
登录 后发表回答