适配Waveshare三色电子墨水屏的图像转换问题:ESP32内存不足求解
我正尝试将图像文件处理为可在黑/白/红三色电子墨水屏上显示的格式,但遇到了输出分辨率相关的问题。
根据显示屏的示例代码,它需要两个字节数组(一个用于黑白,一个用于红色),每个数组为15000字节,该电子墨水屏的分辨率为400×300。
我使用以下Python脚本生成两个BMP文件:一个黑白文件、一个红色文件。脚本运行正常,但每个文件大小达360000字节,无法放入ESP32内存中。输入的PNG图像大小为195316字节。
我使用的库中有一个函数EPD_4IN2B_V2_Display(BLACKWHITEBUFFER, REDBUFFER);,它要求完整图像(黑白通道、红色通道各一个)加载到内存中,但当前的图像大小无法在ESP32上运行。而示例中每个颜色通道仅使用15KB,因此我认为自己在图像处理环节遗漏了某些步骤。
能否有人指出我遗漏的内容?我该如何更新Python图像处理脚本以解决此问题?
我使用的是Waveshare 4.2英寸三色电子墨水屏和Waveshare ESP32驱动板,大部分Python代码基于StackOverflow的一篇帖子,但始终找不到问题所在。
输入图像、黑白通道输出、红色通道输出如下:


现有Python代码
import io import traceback from wand.image import Image as WandImage from PIL import Image # This function takes as input a filename for an image # It resizes the image into the dimensions supported by the ePaper Display # It then remaps the image into a tri-color scheme using a palette (affinity) # for remapping, and the Floyd Steinberg algorithm for dithering # It then splits the image into two component parts: # a white and black image (with the red pixels removed) # a white and red image (with the black pixels removed) # It then converts these into PIL Images and returns them # The PIL Images can be used by the ePaper library to display def getImagesToDisplay(filename): print(filename) red_image = None black_image = None try: with WandImage(filename=filename) as img: img.resize(400, 300) with WandImage() as palette: with WandImage(width = 1, height = 1, pseudo ="xc:red") as red: palette.sequence.append(red) with WandImage(width = 1, height = 1, pseudo ="xc:black") as black: palette.sequence.append(black) with WandImage(width = 1, height = 1, pseudo ="xc:white") as white: palette.sequence.append(white) palette.concat() img.remap(affinity=palette, method='floyd_steinberg') red = img.clone() black = img.clone() red.opaque_paint(target='black', fill='white') black.opaque_paint(target='red', fill='white') red_image = Image.open(io.BytesIO(red.make_blob("bmp"))) black_image = Image.open(io.BytesIO(black.make_blob("bmp"))) red_bytes = io.BytesIO(red.make_blob("bmp")) black_bytes = io.BytesIO(black.make_blob("bmp")) except Exception as ex: print ('traceback.format_exc():\n%s',traceback.format_exc()) return (red_image, black_image, red_bytes, black_bytes) if __name__ == "__main__": print("Running...") file_path = "testimage-tree.png" with open(file_path, "rb") as f: image_data = f.read() red_image, black_image, red_bytes, black_bytes = getImagesToDisplay(file_path) print("bw: ", red_bytes) print("red: ", black_bytes) black_image.save("output/bw.bmp") red_image.save("output/red.bmp") print("BW file size:", len(black_image.tobytes())) print("Red file size:", len(red_image.tobytes()))
你的核心问题是:当前生成的BMP文件是每像素3字节(24位真彩色)的格式,而Waveshare的电子墨水屏需要的是位压缩格式——每8个像素用1字节存储,这样400×300的分辨率总字节数就是 (400×300)/8 = 15000字节,正好和示例中的大小匹配。
接下来修改你的Python脚本,主要做两个关键调整:
- 将处理后的图像转换为单通道的二值图像(仅黑/白),而不是保留24位彩色格式。
- 将二值图像按位打包,生成符合要求的字节数组,而不是直接保存为BMP文件。
修改后的完整代码
import io import traceback from wand.image import Image as WandImage from PIL import Image def getImagesToDisplay(filename): print(filename) red_buffer = None black_buffer = None try: with WandImage(filename=filename) as img: img.resize(400, 300) # 创建三色调色板 with WandImage() as palette: with WandImage(width=1, height=1, pseudo="xc:red") as red: palette.sequence.append(red) with WandImage(width=1, height=1, pseudo="xc:black") as black: palette.sequence.append(black) with WandImage(width=1, height=1, pseudo="xc:white") as white: palette.sequence.append(white) palette.concat() # 映射为三色图像 img.remap(affinity=palette, method='floyd_steinberg') # 生成红色通道二值图像:红色为有效像素(对应字节位1),其他为0 red = img.clone() red.opaque_paint(target='black', fill='white') red.opaque_paint(target='red', fill='black') red.format = 'pbm' # 转换为单通道二值格式 red_pil = Image.open(io.BytesIO(red.make_blob())) # 生成黑白通道二值图像:黑色为有效像素(对应字节位1),其他为0 black = img.clone() black.opaque_paint(target='red', fill='white') black.opaque_paint(target='black', fill='black') black.format = 'pbm' black_pil = Image.open(io.BytesIO(black.make_blob())) # 将二值图像打包为字节数组(每8像素1字节,高位对应左侧像素) def image_to_buffer(pil_img): buffer = bytearray() pixels = pil_img.load() width, height = pil_img.size for y in range(height): byte = 0 for x in range(width): # 黑色像素时设置对应位 if pixels[x, y] == 0: byte |= (1 << (7 - (x % 8))) # 每8个像素写入一个字节 if (x + 1) % 8 == 0: buffer.append(byte) byte = 0 # 处理每行末尾不足8个像素的剩余位 if width % 8 != 0: buffer.append(byte) return buffer red_buffer = image_to_buffer(red_pil) black_buffer = image_to_buffer(black_pil) except Exception as ex: print('traceback.format_exc():\n%s', traceback.format_exc()) return (black_buffer, red_buffer) if __name__ == "__main__": print("Running...") file_path = "testimage-tree.png" black_buffer, red_buffer = getImagesToDisplay(file_path) print("BW buffer size:", len(black_buffer)) # 输出应为15000 print("Red buffer size:", len(red_buffer)) # 输出应为15000 # 保存为二进制文件,直接供ESP32驱动库使用 with open("output/black_buffer.bin", "wb") as f: f.write(black_buffer) with open("output/red_buffer.bin", "wb") as f: f.write(red_buffer)
关键修改说明
- 转换为二值图像:通过
opaque_paint将目标颜色转为黑色,其他转为白色,再保存为PBM单通道二值格式,彻底消除彩色BMP的冗余数据。 - 位打包逻辑:
image_to_buffer函数遍历每个像素,将8个像素的状态(黑/白)打包到1个字节中,完全匹配Waveshare显示屏的内存布局要求。 - 输出二进制文件:直接生成
.bin格式的字节数组,无需解析BMP头,可直接被ESP32的驱动库读取使用。
现在生成的两个字节数组大小都是15000字节,完全符合显示屏的要求,能够正常加载到ESP32内存中。
内容的提问来源于stack exchange,提问作者Mike Buss

