37浏览
查看: 37|回复: 3

手搓赛博拍立得——FireBeetle 2 ESP32-P4 试用体验

[复制链接]
墨水屏加摄像头有没有搞头?之前就想把这两者结合做一个玩具了,原理很简单,上电后就进行拍摄然后转成墨水屏用的格式并刷新。
正好近期看到DF新出的p4开发板,具备esp32家族目前最强大的性能,于是实现了这个想法,效果如下图所示:
上电就可以进行拍照,直接看自拍镜获得预览效果,照片可以断电永久显示,且具有独特的墨水屏风格。
不同与墨水屏传统的在电脑转换再上传的思路,得益于P4强大的性能,直接在esp32上实现了图片的抖动和转换过程。
手搓赛博拍立得——FireBeetle 2 ESP32-P4 试用体验图1
项目是通过micropython实现的,这里首先感谢Vincent提供了抢先版的带摄像头的micropython固件,站在前人的肩膀上继续开发
下载附件firmware.rar
注意 固件的刷写地址是0x002000


然后运行py文件,连接全彩的墨水屏就可以得到图中的效果了!
  1. import time
  2. import machine
  3. import framebuf
  4. from machine import SPI, Pin
  5. import struct
  6. EPD_WIDTH = 800
  7. EPD_HEIGHT = 480
  8. class EPD:
  9.     def __init__(self):
  10.         self.reset_pin = Pin(33, Pin.OUT)  
  11.         self.dc_pin = Pin(32, Pin.OUT)
  12.         self.busy_pin = Pin(34, Pin.IN)
  13.         self.cs_pin = Pin(31, Pin.OUT)
  14.         self.clk_pin = Pin(28)
  15.         self.din_pin = Pin(29)
  16.         self.miso_pin = Pin(30)
  17.         self.spi = SPI(1,baudrate=100000,sck=self.clk_pin, mosi=self.din_pin, miso=None)
  18.         self.buffer = bytearray(EPD_WIDTH * EPD_HEIGHT // 2)
  19.         self.fb = framebuf.FrameBuffer(self.buffer, EPD_WIDTH, EPD_HEIGHT, framebuf.GS4_HMSB)
  20.         self.width = EPD_WIDTH
  21.         self.height = EPD_HEIGHT
  22.         self.BLACK  = 0x0
  23.         self.WHITE  = 0x1
  24.         self.YELLOW = 0x2
  25.         self.RED    = 0x3
  26.         self.BLUE   = 0x5
  27.         self.GREEN  = 0x6
  28.     def reset(self):
  29.         self.reset_pin.value(1)
  30.         time.sleep_ms(20)
  31.         self.reset_pin.value(0)  
  32.         time.sleep_ms(2)
  33.         self.reset_pin.value(1)
  34.         time.sleep_ms(20)
  35.     def send_command(self, command):
  36.         self.dc_pin.value(0)  
  37.         self.cs_pin.value(0)
  38.         self.spi.write(command.to_bytes(1,'big'))
  39.         self.cs_pin.value(1)
  40.     def send_data(self, data):
  41.         self.dc_pin.value(1)  
  42.         self.cs_pin.value(0)
  43.         self.spi.write(data.to_bytes(1,'big'))
  44.         self.cs_pin.value(1)
  45.    
  46.     def send_data2(self, data):
  47.         self.dc_pin.value(1)  
  48.         self.cs_pin.value(0)
  49.         self.spi.write(data)
  50.         self.cs_pin.value(1)
  51.         
  52.     def ReadBusyH(self):
  53.         print("e-Paper busy H")
  54.         while self.busy_pin.value() == 0:  # 0: busy, 1: idle
  55.             time.sleep_ms(5)
  56.         print("e-Paper busy H release")
  57.     def TurnOnDisplay(self):
  58.         self.send_command(0x04)  # POWER_ON
  59.         self.ReadBusyH()
  60.         
  61.         self.send_command(0x12)  # DISPLAY_REFRESH
  62.         self.send_data(0x00)
  63.         self.ReadBusyH()
  64.         
  65.         self.send_command(0x02)  # POWER_OFF
  66.         self.send_data(0x00)
  67.         self.ReadBusyH()
  68.     def init(self):
  69.         # EPD hardware init start
  70.         self.reset()
  71.         self.ReadBusyH()
  72.         time.sleep_ms(30)
  73.         self.send_command(0xAA)   
  74.         self.send_data(0x49)
  75.         self.send_data(0x55)
  76.         self.send_data(0x20)
  77.         self.send_data(0x08)
  78.         self.send_data(0x09)
  79.         self.send_data(0x18)
  80.         self.send_command(0x01)
  81.         self.send_data(0x3F)
  82.         self.send_command(0x00)  
  83.         self.send_data(0x5F)
  84.         self.send_data(0x69)
  85.         self.send_command(0x03)
  86.         self.send_data(0x00)
  87.         self.send_data(0x54)
  88.         self.send_data(0x00)
  89.         self.send_data(0x44)
  90.         self.send_command(0x05)
  91.         self.send_data(0x40)
  92.         self.send_data(0x1F)
  93.         self.send_data(0x1F)
  94.         self.send_data(0x2C)
  95.         self.send_command(0x06)
  96.         self.send_data(0x6F)
  97.         self.send_data(0x1F)
  98.         self.send_data(0x17)
  99.         self.send_data(0x49)
  100.         self.send_command(0x08)
  101.         self.send_data(0x6F)
  102.         self.send_data(0x1F)
  103.         self.send_data(0x1F)
  104.         self.send_data(0x22)
  105.         self.send_command(0x30)
  106.         self.send_data(0x03)
  107.         self.send_command(0x50)
  108.         self.send_data(0x3F)
  109.         self.send_command(0x60)
  110.         self.send_data(0x02)
  111.         self.send_data(0x00)
  112.         self.send_command(0x61)
  113.         self.send_data(0x03)
  114.         self.send_data(0x20)
  115.         self.send_data(0x01)
  116.         self.send_data(0xE0)
  117.         self.send_command(0x84)
  118.         self.send_data(0x01)
  119.         self.send_command(0xE3)
  120.         self.send_data(0x2F)
  121.         self.send_command(0x04)
  122.         self.ReadBusyH()
  123.     def show(self):
  124.         # Send image data to the display
  125.         self.send_command(0x10)
  126.         self.send_data2(self.buffer)
  127.         self.TurnOnDisplay()
  128.     def clear(self, color=0x11):
  129.         # Clear the screen with a specific color
  130.         self.send_command(0x10)
  131.         self.fb.fill(self.RED)
  132.         self.show()
  133.         self.TurnOnDisplay()
  134.     def sleep(self):
  135.         # Put the display to sleep
  136.         self.send_command(0x07)  # DEEP_SLEEP
  137.         self.send_data(0XA5)
  138. def crop_pic(buf):
  139.     width = 800
  140.     height_in = 640
  141.     crop_top = 80
  142.     crop_bottom = 80
  143.     height_out = 480
  144.     row_bytes = width * 2  # 每行字节数
  145.     buf_out = bytearray(row_bytes * height_out)
  146.     start = crop_top * row_bytes  # 起始字节位置
  147.     for i in range(height_out):
  148.         src_start = start + i * row_bytes
  149.         src_end = src_start + row_bytes
  150.         dst_start = i * row_bytes
  151.         dst_end = dst_start + row_bytes
  152.         buf_out[dst_start:dst_end] = buf[src_start:src_end]
  153.     return buf_out
  154. #-------------------------------------------------------
  155. import array
  156.    
  157. width = 800
  158. height = 480
  159. # 调色板RGB888定义
  160. palette_rgb888 = [
  161.     (0, 0, 0),       # 0 黑
  162.     (255, 255, 255), # 1 白
  163.     (255, 255, 0),   # 2 黄
  164.     (255, 0, 0),     # 3 红
  165.     (0, 0, 0),       # 4 黑(重复)
  166.     (0, 0, 255),     # 5 蓝
  167.     (0, 255, 0),     # 6 绿
  168. ]
  169. # RGB888 转 RGB565
  170. def rgb888_to_rgb565(r, g, b):
  171.     r5 = (r * 31) // 255
  172.     g6 = (g * 63) // 255
  173.     b5 = (b * 31) // 255
  174.     return (r5 << 11) | (g6 << 5) | b5
  175. # RGB565 拆分为 R5,G6,B5
  176. def rgb565_to_components(c):
  177.     r = (c >> 11) & 0x1F
  178.     g = (c >> 5) & 0x3F
  179.     b = c & 0x1F
  180.     return r, g, b
  181. # 计算两个RGB565颜色距离(平方和)
  182. def rgb565_distance(c1, c2):
  183.     r1, g1, b1 = rgb565_to_components(c1)
  184.     r2, g2, b2 = rgb565_to_components(c2)
  185.     return (r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2
  186. # 找最近颜色索引
  187. def find_nearest_color_rgb565(pixel, palette):
  188.     dmin = 1 << 30
  189.     idx_min = 0
  190.     for i, pc in enumerate(palette):
  191.         d = rgb565_distance(pixel, pc)
  192.         if d < dmin:
  193.             dmin = d
  194.             idx_min = i
  195.     return idx_min
  196. # 将字节数组转换成array.array('B')的一维RGB565分量数组
  197. def bytes_to_rgb565_components_array(byte_array):
  198.     pixels = array.array('B')
  199.     length = len(byte_array)//2
  200.     for i in range(length):
  201.         val = byte_array[2*i] | (byte_array[2*i+1]<<8)
  202.         r = (val >> 11) & 0x1F
  203.         g = (val >> 5) & 0x3F
  204.         b = val & 0x1F
  205.         pixels.append(r)
  206.         pixels.append(g)
  207.         pixels.append(b)
  208.     return pixels
  209. # Floyd-Steinberg 抖动算法,基于一维array.array('B')
  210. def floyd_steinberg_dither_rgb565(width, height, pixels, palette):
  211.     max_r, max_g, max_b = 31, 63, 31
  212.     output = [0] * (width * height)
  213.     w_right = 7
  214.     w_down_left = 3
  215.     w_down = 5
  216.     w_down_right = 1
  217.     weight_sum = 16
  218.     for y in range(height):
  219.         for x in range(width):
  220.             idx = y * width + x
  221.             base = idx * 3
  222.             r = pixels[base]
  223.             g = pixels[base + 1]
  224.             b = pixels[base + 2]
  225.             val = (r << 11) | (g << 5) | b
  226.             nearest_idx = find_nearest_color_rgb565(val, palette)
  227.             new_val = palette[nearest_idx]
  228.             r_new, g_new, b_new = rgb565_to_components(new_val)
  229.             output[idx] = nearest_idx
  230.             err_r = r - r_new
  231.             err_g = g - g_new
  232.             err_b = b - b_new
  233.             def add_error(nx, ny, w):
  234.                 if 0 <= nx < width and 0 <= ny < height:
  235.                     nidx = ny * width + nx
  236.                     nb = nidx * 3
  237.                     pixels[nb] = max(0, min(max_r, pixels[nb] + (err_r * w) // weight_sum))
  238.                     pixels[nb + 1] = max(0, min(max_g, pixels[nb + 1] + (err_g * w) // weight_sum))
  239.                     pixels[nb + 2] = max(0, min(max_b, pixels[nb + 2] + (err_b * w) // weight_sum))
  240.             add_error(x + 1, y, w_right)
  241.             add_error(x - 1, y + 1, w_down_left)
  242.             add_error(x, y + 1, w_down)
  243.             add_error(x + 1, y + 1, w_down_right)
  244.     return output
  245. # 4bit索引打包(两个4bit索引合成1字节)
  246. def pack_4bit(indices):
  247.     if len(indices) % 2 != 0:
  248.         indices.append(0)
  249.     packed = bytearray(len(indices) // 2)
  250.     for i in range(0, len(indices), 2):
  251.         packed[i // 2] = (indices[i] << 4) | indices[i + 1]
  252.     return packed
  253. #------------------MAIN PROGRAM-----------------------
  254. import camera,time
  255. camera.init()
  256. time.sleep(2)
  257. print('0')
  258. img = camera.capture()  # bytes
  259. crop_img = crop_pic(img)
  260. print(len(crop_img))
  261. camera.deinit()
  262. print('1')
  263. palette = [rgb888_to_rgb565(*c) for c in palette_rgb888]
  264. print('2')
  265. pixels = bytes_to_rgb565_components_array(crop_img)
  266. print('3')
  267. indices = floyd_steinberg_dither_rgb565(width, height, pixels, palette)
  268. print('4')
  269. buf = pack_4bit(indices)
  270. print('5')
  271. scr = EPD()
  272. scr.init()
  273. picfb = framebuf.FrameBuffer(buf, 800, 480, framebuf.GS4_HMSB)
  274. scr.fb.blit(picfb,0,0)
  275. scr.show()
  276. scr.sleep()
复制代码
项目展望:
目前由于算法比较粗暴,图片转换的速度很慢,实测在4分钟左右,如果优化算法,将会提升速度,不过这个项目也许用linux的板子更合适,几秒就能完成这个任务。
手搓赛博拍立得——FireBeetle 2 ESP32-P4 试用体验图3


木子哦  管理员

发表于 1 小时前

大佬,有视频么
回复

使用道具 举报

木子哦  管理员

发表于 1 小时前

大佬,有视频么
回复

使用道具 举报

木子哦  管理员

发表于 1 小时前

大佬,有视频么
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail