墨水屏加摄像头有没有搞头?之前就想把这两者结合做一个玩具了,原理很简单,上电后就进行拍摄然后转成墨水屏用的格式并刷新。
正好近期看到DF新出的p4开发板,具备esp32家族目前最强大的性能,于是实现了这个想法,效果如下图所示:
上电就可以进行拍照,直接看自拍镜获得预览效果,照片可以断电永久显示,且具有独特的墨水屏风格。
不同与墨水屏传统的在电脑转换再上传的思路,得益于P4强大的性能,直接在esp32上实现了图片的抖动和转换过程。

项目是通过micropython实现的,这里首先感谢Vincent提供了抢先版的带摄像头的micropython固件,站在前人的肩膀上继续开发
firmware.rar
注意 固件的刷写地址是0x002000
然后运行py文件,连接全彩的墨水屏就可以得到图中的效果了!
- import time
- import machine
- import framebuf
- from machine import SPI, Pin
- import struct
-
- EPD_WIDTH = 800
- EPD_HEIGHT = 480
-
- class EPD:
- def __init__(self):
- self.reset_pin = Pin(33, Pin.OUT)
- self.dc_pin = Pin(32, Pin.OUT)
- self.busy_pin = Pin(34, Pin.IN)
- self.cs_pin = Pin(31, Pin.OUT)
- self.clk_pin = Pin(28)
- self.din_pin = Pin(29)
- self.miso_pin = Pin(30)
- self.spi = SPI(1,baudrate=100000,sck=self.clk_pin, mosi=self.din_pin, miso=None)
- self.buffer = bytearray(EPD_WIDTH * EPD_HEIGHT // 2)
- self.fb = framebuf.FrameBuffer(self.buffer, EPD_WIDTH, EPD_HEIGHT, framebuf.GS4_HMSB)
- self.width = EPD_WIDTH
- self.height = EPD_HEIGHT
- self.BLACK = 0x0
- self.WHITE = 0x1
- self.YELLOW = 0x2
- self.RED = 0x3
- self.BLUE = 0x5
- self.GREEN = 0x6
-
- def reset(self):
- self.reset_pin.value(1)
- time.sleep_ms(20)
- self.reset_pin.value(0)
- time.sleep_ms(2)
- self.reset_pin.value(1)
- time.sleep_ms(20)
-
- def send_command(self, command):
- self.dc_pin.value(0)
- self.cs_pin.value(0)
- self.spi.write(command.to_bytes(1,'big'))
- self.cs_pin.value(1)
-
- def send_data(self, data):
- self.dc_pin.value(1)
- self.cs_pin.value(0)
- self.spi.write(data.to_bytes(1,'big'))
- self.cs_pin.value(1)
-
- def send_data2(self, data):
- self.dc_pin.value(1)
- self.cs_pin.value(0)
- self.spi.write(data)
- self.cs_pin.value(1)
-
- def ReadBusyH(self):
- print("e-Paper busy H")
- while self.busy_pin.value() == 0: # 0: busy, 1: idle
- time.sleep_ms(5)
- print("e-Paper busy H release")
-
- def TurnOnDisplay(self):
- self.send_command(0x04) # POWER_ON
- self.ReadBusyH()
-
- self.send_command(0x12) # DISPLAY_REFRESH
- self.send_data(0x00)
- self.ReadBusyH()
-
- self.send_command(0x02) # POWER_OFF
- self.send_data(0x00)
- self.ReadBusyH()
-
- def init(self):
- # EPD hardware init start
- self.reset()
- self.ReadBusyH()
- time.sleep_ms(30)
-
- self.send_command(0xAA)
- self.send_data(0x49)
- self.send_data(0x55)
- self.send_data(0x20)
- self.send_data(0x08)
- self.send_data(0x09)
- self.send_data(0x18)
-
- self.send_command(0x01)
- self.send_data(0x3F)
-
- self.send_command(0x00)
- self.send_data(0x5F)
- self.send_data(0x69)
-
- self.send_command(0x03)
- self.send_data(0x00)
- self.send_data(0x54)
- self.send_data(0x00)
- self.send_data(0x44)
-
- self.send_command(0x05)
- self.send_data(0x40)
- self.send_data(0x1F)
- self.send_data(0x1F)
- self.send_data(0x2C)
-
- self.send_command(0x06)
- self.send_data(0x6F)
- self.send_data(0x1F)
- self.send_data(0x17)
- self.send_data(0x49)
-
- self.send_command(0x08)
- self.send_data(0x6F)
- self.send_data(0x1F)
- self.send_data(0x1F)
- self.send_data(0x22)
-
- self.send_command(0x30)
- self.send_data(0x03)
-
- self.send_command(0x50)
- self.send_data(0x3F)
-
- self.send_command(0x60)
- self.send_data(0x02)
- self.send_data(0x00)
-
- self.send_command(0x61)
- self.send_data(0x03)
- self.send_data(0x20)
- self.send_data(0x01)
- self.send_data(0xE0)
-
- self.send_command(0x84)
- self.send_data(0x01)
-
- self.send_command(0xE3)
- self.send_data(0x2F)
-
- self.send_command(0x04)
- self.ReadBusyH()
-
- def show(self):
- # Send image data to the display
- self.send_command(0x10)
- self.send_data2(self.buffer)
- self.TurnOnDisplay()
-
- def clear(self, color=0x11):
- # Clear the screen with a specific color
- self.send_command(0x10)
- self.fb.fill(self.RED)
- self.show()
- self.TurnOnDisplay()
-
- def sleep(self):
- # Put the display to sleep
- self.send_command(0x07) # DEEP_SLEEP
- self.send_data(0XA5)
-
- def crop_pic(buf):
- width = 800
- height_in = 640
- crop_top = 80
- crop_bottom = 80
- height_out = 480
-
- row_bytes = width * 2 # 每行字节数
- buf_out = bytearray(row_bytes * height_out)
-
- start = crop_top * row_bytes # 起始字节位置
-
- for i in range(height_out):
- src_start = start + i * row_bytes
- src_end = src_start + row_bytes
- dst_start = i * row_bytes
- dst_end = dst_start + row_bytes
- buf_out[dst_start:dst_end] = buf[src_start:src_end]
-
- return buf_out
-
- #-------------------------------------------------------
- import array
-
- width = 800
- height = 480
-
- # 调色板RGB888定义
- palette_rgb888 = [
- (0, 0, 0), # 0 黑
- (255, 255, 255), # 1 白
- (255, 255, 0), # 2 黄
- (255, 0, 0), # 3 红
- (0, 0, 0), # 4 黑(重复)
- (0, 0, 255), # 5 蓝
- (0, 255, 0), # 6 绿
- ]
-
- # RGB888 转 RGB565
- def rgb888_to_rgb565(r, g, b):
- r5 = (r * 31) // 255
- g6 = (g * 63) // 255
- b5 = (b * 31) // 255
- return (r5 << 11) | (g6 << 5) | b5
-
- # RGB565 拆分为 R5,G6,B5
- def rgb565_to_components(c):
- r = (c >> 11) & 0x1F
- g = (c >> 5) & 0x3F
- b = c & 0x1F
- return r, g, b
-
- # 计算两个RGB565颜色距离(平方和)
- def rgb565_distance(c1, c2):
- r1, g1, b1 = rgb565_to_components(c1)
- r2, g2, b2 = rgb565_to_components(c2)
- return (r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2
-
- # 找最近颜色索引
- def find_nearest_color_rgb565(pixel, palette):
- dmin = 1 << 30
- idx_min = 0
- for i, pc in enumerate(palette):
- d = rgb565_distance(pixel, pc)
- if d < dmin:
- dmin = d
- idx_min = i
- return idx_min
-
- # 将字节数组转换成array.array('B')的一维RGB565分量数组
- def bytes_to_rgb565_components_array(byte_array):
- pixels = array.array('B')
- length = len(byte_array)//2
- for i in range(length):
- val = byte_array[2*i] | (byte_array[2*i+1]<<8)
- r = (val >> 11) & 0x1F
- g = (val >> 5) & 0x3F
- b = val & 0x1F
- pixels.append(r)
- pixels.append(g)
- pixels.append(b)
- return pixels
-
- # Floyd-Steinberg 抖动算法,基于一维array.array('B')
- def floyd_steinberg_dither_rgb565(width, height, pixels, palette):
- max_r, max_g, max_b = 31, 63, 31
- output = [0] * (width * height)
-
- w_right = 7
- w_down_left = 3
- w_down = 5
- w_down_right = 1
- weight_sum = 16
-
- for y in range(height):
- for x in range(width):
- idx = y * width + x
- base = idx * 3
- r = pixels[base]
- g = pixels[base + 1]
- b = pixels[base + 2]
-
- val = (r << 11) | (g << 5) | b
- nearest_idx = find_nearest_color_rgb565(val, palette)
- new_val = palette[nearest_idx]
- r_new, g_new, b_new = rgb565_to_components(new_val)
- output[idx] = nearest_idx
-
- err_r = r - r_new
- err_g = g - g_new
- err_b = b - b_new
-
- def add_error(nx, ny, w):
- if 0 <= nx < width and 0 <= ny < height:
- nidx = ny * width + nx
- nb = nidx * 3
- pixels[nb] = max(0, min(max_r, pixels[nb] + (err_r * w) // weight_sum))
- pixels[nb + 1] = max(0, min(max_g, pixels[nb + 1] + (err_g * w) // weight_sum))
- pixels[nb + 2] = max(0, min(max_b, pixels[nb + 2] + (err_b * w) // weight_sum))
-
- add_error(x + 1, y, w_right)
- add_error(x - 1, y + 1, w_down_left)
- add_error(x, y + 1, w_down)
- add_error(x + 1, y + 1, w_down_right)
-
- return output
-
- # 4bit索引打包(两个4bit索引合成1字节)
- def pack_4bit(indices):
- if len(indices) % 2 != 0:
- indices.append(0)
- packed = bytearray(len(indices) // 2)
- for i in range(0, len(indices), 2):
- packed[i // 2] = (indices[i] << 4) | indices[i + 1]
- return packed
-
-
- #------------------MAIN PROGRAM-----------------------
-
- import camera,time
- camera.init()
- time.sleep(2)
-
- print('0')
- img = camera.capture() # bytes
- crop_img = crop_pic(img)
- print(len(crop_img))
- camera.deinit()
- print('1')
- palette = [rgb888_to_rgb565(*c) for c in palette_rgb888]
- print('2')
- pixels = bytes_to_rgb565_components_array(crop_img)
- print('3')
- indices = floyd_steinberg_dither_rgb565(width, height, pixels, palette)
- print('4')
- buf = pack_4bit(indices)
- print('5')
-
- scr = EPD()
- scr.init()
- picfb = framebuf.FrameBuffer(buf, 800, 480, framebuf.GS4_HMSB)
- scr.fb.blit(picfb,0,0)
- scr.show()
- scr.sleep()
复制代码
项目展望:
目前由于算法比较粗暴,图片转换的速度很慢,实测在4分钟左右,如果优化算法,将会提升速度,不过这个项目也许用linux的板子更合适,几秒就能完成这个任务。

|