手搓赛博拍立得——FireBeetle 2 ESP32-P4 试用体验
墨水屏加摄像头有没有搞头?之前就想把这两者结合做一个玩具了,原理很简单,上电后就进行拍摄然后转成墨水屏用的格式并刷新。正好近期看到DF新出的p4开发板,具备esp32家族目前最强大的性能,于是实现了这个想法,效果如下图所示:
上电就可以进行拍照,直接看自拍镜获得预览效果,照片可以断电永久显示,且具有独特的墨水屏风格。
不同与墨水屏传统的在电脑转换再上传的思路,得益于P4强大的性能,直接在esp32上实现了图片的抖动和转换过程。
项目是通过micropython实现的,这里首先感谢Vincent提供了抢先版的带摄像头的micropython固件,站在前人的肩膀上继续开发
注意 固件的刷写地址是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 = buf
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 | (byte_array<<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 = * (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
g = pixels
b = pixels
val = (r << 11) | (g << 5) | b
nearest_idx = find_nearest_color_rgb565(val, palette)
new_val = palette
r_new, g_new, b_new = rgb565_to_components(new_val)
output = 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 = max(0, min(max_r, pixels + (err_r * w) // weight_sum))
pixels = max(0, min(max_g, pixels + (err_g * w) // weight_sum))
pixels = max(0, min(max_b, pixels + (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 = (indices << 4) | indices
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 =
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的板子更合适,几秒就能完成这个任务。
大佬,有视频么 大佬,有视频么 大佬,有视频么 你可以试一下我的新固件,拍完照直接使用jpeg库解码出来rgb565 bele再显示
页:
[1]