First of all, my question is different to How do I convert image to 2-bit per pixel? and unfortunately its solution does not work in my case...
I need to convert images to 2-bit per pixel grayscale BMP format. The sample image has the following properties:
Color Model: RGB
Depth: 4
Is Indexed: 1
Dimension: 800x600
Size: 240,070 bytes (4 bits per pixel but only last 2 bits are used to identify the gray scales as 0/1/2/3 in decimal or 0000/0001/0010/0011 in binary, plus 70 bytes BMP metadata or whatever)
The Hex values of the beginning part of the sample BMP image:
The 3s represent white pixels at the beginning of the image. Further down there are some 0s, 1s and 2s representing black, dark gray and light gray:
With the command below,
convert pic.png -colorspace gray +matte -depth 2 out.bmp
I can get visually correct 4-level grayscale image, but wrong depth or size per pixel:
Color Model: RGB
Depth: 8 (expect 4)
Dimension: 800x504
Size: 1,209,738 bytes (something like 3 bytes per pixel, plus metadata)
(no mention of indexed colour space)
Please help...
Change a picture to grayscale or to black-and-whiteRight-click the picture that you want to change, and then click Format Picture on the shortcut menu. Click the Picture tab. Under Image control, in the Color list, click Grayscale or Black and White.
Images in bitmap mode are called 1‑bit images because they have a bit depth of 1. Uses up to 256 shades of gray. Grayscale images are 8‑bit images. Every pixel in a grayscale image has a brightness value ranging from 0 (black) to 255 (white).
OK, I have written a Python script following Mark's hints (see comments under original question) to manually create a 4-level gray scale BMP with 4bpp. This specific BMP format construction is for the 4.3 inch e-paper display module made by WaveShare. Specs can be found here: http://www.waveshare.com/wiki/4.3inch_e-Paper
Here's how to pipe the original image to my code and save the outcome.
convert in.png -colorspace gray +matte -colors 4 -depth 2 -resize '800x600>' pgm:- | ./4_level_gray_4bpp_BMP_converter.py > out.bmp
Contents of 4_level_gray_4bpp_BMP_converter.py
:
#!/usr/bin/env python
"""
### Sample BMP header structure, total = 70 bytes
### !!! little-endian !!!
Bitmap file header 14 bytes
42 4D "BM"
C6 A9 03 00 FileSize = 240,070 <= dynamic value
00 00 Reserved
00 00 Reserved
46 00 00 00 Offset = 70 = 14+56
DIB header (bitmap information header)
BITMAPV3INFOHEADER 56 bytes
28 00 00 00 Size = 40
20 03 00 00 Width = 800 <= dynamic value
58 02 00 00 Height = 600 <= dynamic value
01 00 Planes = 1
04 00 BitCount = 4
00 00 00 00 compression
00 00 00 00 SizeImage
00 00 00 00 XPerlPerMeter
00 00 00 00 YPerlPerMeter
04 00 00 00 Colours used = 4
00 00 00 00 ColorImportant
00 00 00 00 Colour definition index 0
55 55 55 00 Colour definition index 1
AA AA AA 00 Colour definition index 2
FF FF FF 00 Colour definition index 3
"""
# to insert File Size, Width and Height with hex strings in order
BMP_HEADER = "42 4D %s 00 00 00 00 46 00 00 00 28 00 00 00 %s %s 01 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 55 55 55 00 AA AA AA 00 FF FF FF 00"
BMP_HEADER_SIZE = 70
BPP = 4
BYTE = 8
ALIGNMENT = 4 # bytes per row
import sys
from re import findall
DIMENTIONS = 1
PIXELS = 3
BLACK = "0"
DARK_GRAY = "1"
GRAY = "2"
WHITE = "3"
# sample data:
# ['P5\n', '610 590\n', '255\n', '<1 byte per pixel for 4 levels of gray>']
# where item 1 is always P5, item 2 is width heigh, item 3 is always 255, items 4 is pixels/colours
data = sys.stdin.readlines()
width = int(data[DIMENTIONS].strip().split(' ')[0])
height = int(data[DIMENTIONS].strip().split(' ')[1])
if not width*height == len(data[PIXELS]):
print "Error: pixel data (%s bytes) and image size (%dx%d pixels) do not match" % (len(data[PIXELS]),width,height)
sys.exit()
colours = [] # enumerate 4 gray levels
for p in data[PIXELS]:
if not p in colours:
colours.append(p)
if len(colours) == 4:
break
# it's possible for the converted pixels to have less than 4 gray levels
colours = sorted(colours) # sort from low to high
# map each colour to e-paper gray indexes
# creates hex string of pixels
# e.g. "0033322222110200....", which is 4 level gray with 4bpp
if len(colours) == 1: # unlikely, but let's have this case here
pixels = data[PIXELS].replace(colours[0],BLACK)
elif len(colours) == 2: # black & white
pixels = data[PIXELS].replace(colours[0],BLACK)\
.replace(colours[1],WHITE)
elif len(colours) == 3:
pixels = data[PIXELS].replace(colours[0],DARK_GRAY)\
.replace(colours[1],GRAY)\
.replace(colours[2],WHITE)
else: # 4 grays as expected
pixels = data[PIXELS].replace(colours[0],BLACK)\
.replace(colours[1],DARK_GRAY)\
.replace(colours[2],GRAY)\
.replace(colours[3],WHITE)
# BMP pixel array starts from last row to first row
# and must be aligned to 4 bytes or 8 pixels
padding = "F" * ((BYTE/BPP) * ALIGNMENT - width % ((BYTE/BPP) * ALIGNMENT))
aligned_pixels = ''.join([pixels[i:i+width]+padding for i in range(0, len(pixels), width)][::-1])
# convert hex string to represented byte values
def Hex2Bytes(hexStr):
hexStr = ''.join(hexStr.split(" "))
bytes = []
for i in range(0, len(hexStr), 2):
byte = int(hexStr[i:i+2],16)
bytes.append(chr(byte))
return ''.join(bytes)
# convert integer to 4-byte little endian hex string
# e.g. 800 => 0x320 => 00000320 (big-endian) =>20030000 (little-endian)
def i2LeHexStr(i):
be_hex = ('0000000'+hex(i)[2:])[-8:]
n = 2 # split every 2 letters
return ''.join([be_hex[i:i+n] for i in range(0, len(be_hex), n)][::-1])
BMP_HEADER = BMP_HEADER % (i2LeHexStr(len(aligned_pixels)/(BYTE/BPP)+BMP_HEADER_SIZE),i2LeHexStr(width),i2LeHexStr(height))
sys.stdout.write(Hex2Bytes(BMP_HEADER+aligned_pixels))
Edit: everything about this e-paper display and my code to display things on it can be found here: https://github.com/yy502/ePaperDisplay
This works for me in Imagemagick 6.9.10.23 Q16 Mac OSX Sierra
Input:
convert logo.png -colorspace gray -depth 2 -type truecolor logo_depth8_gray_rgb.bmp
Adding -type truecolor converts the image to RGB, but in gray tones as per the -colorspace gray. And the depth 2 creates only 4 colors in the histogram.
identify -verbose logo_depth8_gray_rgb.bmp
Image:
Filename: logo_depth8_gray_rgb.bmp
Format: BMP (Microsoft Windows bitmap image)
Class: DirectClass
Geometry: 640x480+0+0
Units: PixelsPerCentimeter
Colorspace: sRGB
Type: Grayscale
Base type: Undefined
Endianness: Undefined
Depth: 8/2-bit
Channel depth:
red: 2-bit
green: 2-bit
blue: 2-bit
Channel statistics:
Pixels: 307200
Red:
min: 0 (0)
max: 255 (1)
mean: 228.414 (0.895742)
standard deviation: 66.9712 (0.262632)
kurtosis: 4.29925
skewness: -2.38354
entropy: 0.417933
Green:
min: 0 (0)
max: 255 (1)
mean: 228.414 (0.895742)
standard deviation: 66.9712 (0.262632)
kurtosis: 4.29925
skewness: -2.38354
entropy: 0.417933
Blue:
min: 0 (0)
max: 255 (1)
mean: 228.414 (0.895742)
standard deviation: 66.9712 (0.262632)
kurtosis: 4.29925
skewness: -2.38354
entropy: 0.417933
Image statistics:
Overall:
min: 0 (0)
max: 255 (1)
mean: 228.414 (0.895742)
standard deviation: 66.9712 (0.262632)
kurtosis: 4.29928
skewness: -2.38355
entropy: 0.417933
Colors: 4 <--------
Histogram: <--------
12730: (0,0,0) #000000 black
24146: (85,85,85) #555555 srgb(85,85,85)
9602: (170,170,170) #AAAAAA srgb(170,170,170)
260722: (255,255,255) #FFFFFF white
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With