jd3096 发表于 2025-7-28 15:24:42

手搓赛博拍立得——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的板子更合适,几秒就能完成这个任务。



木子哦 发表于 2025-7-28 20:22:00

大佬,有视频么

木子哦 发表于 2025-7-28 20:22:02

大佬,有视频么

木子哦 发表于 2025-7-28 20:22:06

大佬,有视频么

PY学习笔记 发表于 2025-7-30 10:27:46

你可以试一下我的新固件,拍完照直接使用jpeg库解码出来rgb565 bele再显示
页: [1]
查看完整版本: 手搓赛博拍立得——FireBeetle 2 ESP32-P4 试用体验