Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Save 1 bit deep binary image in Python

I have a binary image in Python and I want to save it in my pc. I need it to be a 1 bit deep png image once stored in my computer. How can I do that? I tried with both PIL and cv2 but I'm not able to save it with 1 bit depth.

like image 225
Jimbo Avatar asked May 10 '18 07:05

Jimbo


3 Answers

I found myself in a situation where I needed to create a lot of binary images, and was frustrated with the available info online. Thanks to the answers and comments here and elsewhere on SO, I was able to find an acceptable solution. The comment from @Jimbo was the best so far. Here is some code to reproduce my exploration of some ways to save binary images in python:

Load libraries and data:

from skimage import data, io, util #'0.16.2'
import matplotlib.pyplot as plt #'3.0.3'
import PIL #'6.2.1'
import cv2 #'4.1.1'
check = util.img_as_bool(data.checkerboard())

The checkerboard image from skimage has dimensions of 200x200. Without compression, as a 1-bit image it should be represented by (200*200/8) 5000 bytes

To save with skimage, note that the package will complain if the data is not uint, hence the conversion. Saving the image takes an average of 2.8ms and has a 408 byte file size

io.imsave('bw_skimage.png',util.img_as_uint(check),plugin='pil',optimize=True,bits=1)

Using matplotlib, 4.2ms and 693 byte file size

plt.imsave('bw_mpl.png',check,cmap='gray')

Using PIL, 0.5ms and 164 byte file size

img = PIL.Image.fromarray(check)
img.save('bw_pil.png',bits=1,optimize=True)

Using cv2, also complains about a bool input. The following command takes 0.4ms and results in a 2566 byte file size, despite the png compression...

_ = cv2.imwrite('bw_cv2.png', check.astype(int), [cv2.IMWRITE_PNG_BILEVEL, 1])

PIL was clearly the best for speed and file size.

I certainly missed some optimizations, comments welcome!

like image 180
chepyle Avatar answered Oct 18 '22 06:10

chepyle


Use:

cv2.imwrite(<image_name>, img, [cv2.IMWRITE_PNG_BILEVEL, 1])

(this will still use compression, so in practice it will most likely have less than 1 bit per pixel)

like image 3
Rosa Gronchi Avatar answered Oct 18 '22 06:10

Rosa Gronchi


If you're not loading pngs or anything the format does behave pretty reasonably to just write it. Then your code doesn't need PIL or any of the headaches of various imports and imports on imports etc.

import struct
import zlib
from math import ceil


def write_png_1bit(buf, width, height, stride=None):
    if stride is None:
        stride = int(ceil(width / 8))
    raw_data = b"".join(
        b'\x00' + buf[span:span + stride] for span in range(0, (height - 1) * stride, stride))

    def png_pack(png_tag, data):
        chunk_head = png_tag + data
        return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))

    return b"".join([
        b'\x89PNG\r\n\x1a\n',
        png_pack(b'IHDR', struct.pack("!2I5B", width, height, 1, 0, 0, 0, 0)),
        png_pack(b'IDAT', zlib.compress(raw_data, 9)),
        png_pack(b'IEND', b'')])

Adapted from: http://code.activestate.com/recipes/577443-write-a-png-image-in-native-python/ (MIT)

by reading the png spec: https://www.w3.org/TR/PNG-Chunks.html

Keep in mind the 1 bit data from buf, should be written left to right like the png spec wants in normal non-interlace mode (which we declared). And the excess data pads the final bit if it exists, and stride is the amount of bytes needed to encode a scanline. Also, if you want those 1 bit to have palette colors you'll have to write a PLTE block and switch the type to 3 rather than 0. Etc.

like image 2
Tatarize Avatar answered Oct 18 '22 04:10

Tatarize