简介
由于手上有一块esp32cam,想要把esp32cam驱动起来,并且实现自己编写上位机来实现读取ov2640的数据。后续应该会将数据传输方式改为通过网络协议传输数据,传输速度更块,且可以更方便的去调试程序。
坑点
这里涉及到一些我完成这个项目时的一些坑点。
坑点是:刚开始我写出esp32cam捕捉图像数据后的代码使用串口发送,然后使用arduino串口软件及sscom串口助手可以读取单片机输出的程序,但是vscde的串行监视器插件无法读取串口数据(我还以为是esp32cam的串口还需要重新配置),后调试发现关闭硬件流控可以正常读取数据。
其实通过python上位机通过串口早就可以读取数据,但是我刚开始使用esp32cam上的usb串口发送数据,上位机程序去读取esp32发送的图像数据,发现一旦开始读取数据就会造成单片机的卡死,如何确定单片机程序是否卡死呢?我的做法是在发送串口数据的循环之前加入led闪烁指示灯 ,但是后来我觉得这样可能不太严谨,于是我将led指示灯闪烁创建为一个任务,因为freertos操作系统可以同时运行多个任务,这样就可以测试使用python程序读取串口数据时是否会死机。经测试确实一旦上位机开始读取数据会使单片机死机,后我想到使用usb转ttl模块去读取数据,发现数据可以正常读取。至此数据读取问题已解决,着手处理数据处理问题。
上代码
下面话不多说,直接上代码
第一版:波特率115200,传输的数据为16进制后转为二进制处理,也可实现,但是读取转化数据较慢,于是有了第二版,后面贴(此处引用了espressif__esp32-camera开源库,极大的加快了我的编码进度)
摄像头初始化代码
///////摄像头初始化.c文件
#include "mycamera.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "esp_camera.h"
/******************************************Camera module functions pin definitions***************************/
// ESP32Cam (AiThinker) PIN Map
#define CAM_PIN_PWDN 32 //掉电/省电模式,高电平有效 input
#define CAM_PIN_RESET -1 // software reset will be performed系统复位管脚,低电平有效 input
#define CAM_PIN_XCLK 0 //外部时钟输入端口,可接外部晶振 input
#define CAM_PIN_SIOD 26 //SCCB 总线的数据线,可类比 I2C 的 SDA io
#define CAM_PIN_SIOC 27 //SCCB 总线的时钟线,可类比 I2C 的 SCL input
//D0~D7 摄像头数据总线output
#define CAM_PIN_D7 35
#define CAM_PIN_D6 34
#define CAM_PIN_D5 39
#define CAM_PIN_D4 36
#define CAM_PIN_D3 21
#define CAM_PIN_D2 19
#define CAM_PIN_D1 18
#define CAM_PIN_D0 5
#define CAM_PIN_VSYNC 25 //帧同步信号 output
#define CAM_PIN_HREF 23 //行同步信号 output
#define CAM_PIN_PCLK 22 //像素同步时钟输出信号 output
static const char *TAG = "example:take_picture";
#if ESP_CAMERA_SUPPORTED
static camera_config_t camera_config = {
.pin_pwdn = CAM_PIN_PWDN,
.pin_reset = CAM_PIN_RESET,
.pin_xclk = CAM_PIN_XCLK,
.pin_sccb_sda = CAM_PIN_SIOD,
.pin_sccb_scl = CAM_PIN_SIOC,
.pin_d7 = CAM_PIN_D7,
.pin_d6 = CAM_PIN_D6,
.pin_d5 = CAM_PIN_D5,
.pin_d4 = CAM_PIN_D4,
.pin_d3 = CAM_PIN_D3,
.pin_d2 = CAM_PIN_D2,
.pin_d1 = CAM_PIN_D1,
.pin_d0 = CAM_PIN_D0,
.pin_vsync = CAM_PIN_VSYNC,
.pin_href = CAM_PIN_HREF,
.pin_pclk = CAM_PIN_PCLK,
//XCLK 20MHz or 10MHz for OV2640 double FPS (Experimental)
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG, //YUV422,GRAYSCALE,RGB565,JPEG
.frame_size = FRAMESIZE_QQVGA, //QQVGA-UXGA, For ESP32, do not use sizes above QVGA when not JPEG. The performance of the ESP32-S series has improved a lot, but JPEG mode always gives better frame rates.
.jpeg_quality = 30, //0-63, for OV series camera sensors, lower number means higher quality
.fb_count = 1, //When jpeg mode is used, if fb_count more than one, the driver will work in continuous mode.
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
};
esp_err_t init_camera(void)
{
//initialize the camera
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Camera Init Failed");
return err;
}
ESP_LOGE(TAG, "Camera Init success!");
return ESP_OK;
}
#endif
第一版
-
ESP-idf端的代码
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_log.h>
#include "myled.h"
#include "sdkconfig.h"
#include "mycamera.h"
#include "driver/uart.h"
static const char *TAG = "example:take_picture";
void app_main(void)
{
myled_init();
uart_init();
xTaskCreate(myled_Blink_task, "myled_Blink_task", 2048, NULL, 5, NULL);
init_camera();
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
ESP_LOGI(TAG, "Taking picture...");
camera_fb_t *pic = esp_camera_fb_get();
if (pic) {
// 串口发送图片数据
int i=pic->len;
printf("IMG_START\n");//开始标志
for (int j = 0; j < i; ++j) {
printf("%02x ", pic->buf[j]);
}
printf("\r\n");//结束 标志
printf("IMG_END\n");
ESP_LOGI(TAG, "Picture taken! Its size was: %zu bytes", pic->len);
esp_camera_fb_return(pic);
}
else {
ESP_LOGE(TAG, "Camera capture failed");
}
}
}
上位机端代码(使用python编写)
import serial
import tkinter as tk
from tkinter import messagebox, ttk
from PIL import Image, ImageTk, ImageFile
import io
import binascii
import time
ImageFile.LOAD_TRUNCATED_IMAGES = True
class SerialImageReceiver:
def __init__(self, root):
self.root = root
self.root.title("串口图像接收器(平滑进度)")
self.root.geometry("800x600")
self.ser = None
self.receiving = False
self.image_data_hex = b'' # 有效图像数据(不含标志位)
self.raw_buffer = b'' # 原始接收数据
self.flag_detected = False # 开始标志是否已检测
# 标志位配置
self.start_flag = b'IMG_START\r\n'
self.end_flag = b'IMG_END\r\n'
# 进度相关参数(核心修改)
self.estimated_total = 50000 # 预估总数据量(字节,可根据实际调整)
self.received_bytes = 0 # 已接收的有效数据字节数
self.waiting_progress = 0 # 等待阶段的进度(0-30%)
self.setup_ui()
print("程序启动,等待数据传输...")
def setup_ui(self):
# 串口配置区域(同之前)
config_frame = ttk.LabelFrame(self.root, text="串口配置")
config_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(config_frame, text="端口:").grid(row=0, column=0, padx=5, pady=5)
self.port_var = tk.StringVar(value="COM16")
self.port_entry = ttk.Entry(config_frame, textvariable=self.port_var, width=10)
self.port_entry.grid(row=0, column=1, padx=5, pady=5)
ttk.Label(config_frame, text="波特率:").grid(row=0, column=2, padx=5, pady=5)
self.baud_var = tk.StringVar(value="115200")
self.baud_entry = ttk.Entry(config_frame, textvariable=self.baud_var, width=10)
self.baud_entry.grid(row=0, column=3, padx=5, pady=5)
self.start_btn = ttk.Button(config_frame, text="开始接收", command=self.start_receive)
self.start_btn.grid(row=0, column=4, padx=5, pady=5)
self.stop_btn = ttk.Button(config_frame, text="停止接收", command=self.stop_receive, state=tk.DISABLED)
self.stop_btn.grid(row=0, column=5, padx=5, pady=5)
# 进度条(同之前,但逻辑修改)
self.progress_label = ttk.Label(self.root, text="进度: 0%")
self.progress_label.pack(fill=tk.X, padx=10, pady=5)
self.progress_bar = ttk.Progressbar(self.root, orient="horizontal", length=400, mode="determinate")
self.progress_bar.pack(fill=tk.X, padx=10, pady=5)
# 图像显示区域(同之前)
image_frame = ttk.LabelFrame(self.root, text="图像显示")
image_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
self.image_label = ttk.Label(image_frame, text="等待接收图像...")
self.image_label.pack(fill=tk.BOTH, expand=True)
def console_log(self, message):
timestamp = time.strftime("[%H:%M:%S] ")
print(timestamp + message)
def start_receive(self):
if self.receiving:
messagebox.showwarning("提示", "正在接收数据,请先停止")
return
port = self.port_var.get().strip()
try:
baudrate = int(self.baud_var.get().strip())
except ValueError:
messagebox.showerror("错误", "波特率必须是整数")
return
try:
self.ser = serial.Serial(port, baudrate, timeout=0.01)
if not self.ser.is_open:
messagebox.showerror("错误", f"无法打开串口 {port}")
return
except Exception as e:
messagebox.showerror("错误", f"串口打开失败: {str(e)}")
return
# 重置进度参数(核心修改)
self.receiving = True
self.flag_detected = False
self.image_data_hex = b''
self.raw_buffer = b''
self.received_bytes = 0
self.waiting_progress = 0
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.console_log(f"打开串口 {port},波特率 {baudrate}")
self.update_progress(0) # 重置进度为0
self.root.after(10, self.receive_loop)
def stop_receive(self):
self.receiving = False
if self.ser and self.ser.is_open:
self.ser.close()
self.console_log("串口已关闭")
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
def receive_loop(self):
if not self.receiving or not self.ser or not self.ser.is_open:
return
try:
if self.ser.in_waiting > 0:
data = self.ser.read(self.ser.in_waiting)
self.raw_buffer += data
self.console_log(f"收到数据: 长度={len(data)}字节")
# 阶段1:未检测到开始标志(等待阶段)
if not self.flag_detected:
# 缓慢增长进度(0-30%),避免用户觉得程序无响应
self.waiting_progress += min(0.5, 30 / self.estimated_total * len(data))
current_progress = min(30, self.waiting_progress)
self.update_progress(current_progress)
# 检测到开始标志后进入阶段2
if self.start_flag in self.raw_buffer:
start_idx = self.raw_buffer.index(self.start_flag) + len(self.start_flag)
self.image_data_hex = self.raw_buffer[start_idx:]
self.raw_buffer = self.image_data_hex
self.flag_detected = True
self.received_bytes = len(self.image_data_hex) # 初始化已接收字节数
self.console_log(f"检测到开始标志,当前有效数据长度={self.received_bytes}字节")
# 阶段2:已检测到开始标志(接收数据阶段)
elif self.flag_detected and self.end_flag not in self.raw_buffer:
# 累加已接收的有效数据长度
self.received_bytes = len(self.raw_buffer)
# 计算进度(30%-90%):用已接收字节数 ÷ 预估总长度,映射到30%-90%区间
progress_ratio = min(1.0, self.received_bytes / self.estimated_total)
current_progress = 30 + (90 - 30) * progress_ratio
self.update_progress(current_progress)
self.console_log(f"接收中... 已接收={self.received_bytes}/{self.estimated_total}字节")
# 阶段3:检测到结束标志(完成阶段)
if self.flag_detected and self.end_flag in self.raw_buffer:
end_idx = self.raw_buffer.index(self.end_flag)
self.image_data_hex = self.raw_buffer[:end_idx]
self.console_log(f"检测到结束标志,总有效数据长度={len(self.image_data_hex)}字节")
self.receiving = False
self.update_progress(95) # 处理前先到95%
self.process_image()
except Exception as e:
self.console_log(f"接收错误: {str(e)}")
self.stop_receive()
if self.receiving:
self.root.after(10, self.receive_loop)
def process_image(self):
self.console_log(f"开始处理数据: 原始有效数据长度={len(self.image_data_hex)}字节")
# 清理数据(同之前)
hex_data = self.image_data_hex.replace(b' ', b'')
hex_data = hex_data.replace(b'\n', b'').replace(b'\r', b'')
self.console_log(f"清理后十六进制长度={len(hex_data)}字节")
# 检查非十六进制字符(同之前)
valid_chars = set(b'0123456789abcdefABCDEF')
invalid_chars = [c for c in hex_data if c not in valid_chars]
if invalid_chars:
self.console_log(f"警告: 检测到非十六进制字符: {[chr(c) for c in invalid_chars[:5]]}...")
# 处理奇数长度(同之前)
if len(hex_data) % 2 != 0:
self.console_log("十六进制长度为奇数,自动补0")
hex_data += b'0'
# 转换为二进制并显示(同之前)
try:
image_data = binascii.unhexlify(hex_data)
self.console_log(f"转换成功,二进制长度={len(image_data)}字节")
with open('received_image.jpeg', 'wb') as f:
f.write(image_data)
self.console_log("图像已保存到 received_image.jpeg")
image = Image.open(io.BytesIO(image_data))
max_w, max_h = 700, 400
w, h = image.size
scale = min(max_w/w, max_h/h)
image = image.resize((int(w*scale), int(h*scale)), Image.LANCZOS)
tk_img = ImageTk.PhotoImage(image)
self.image_label.config(image=tk_img, text="")
self.image_label.image = tk_img
self.console_log(f"图像显示成功 (尺寸: {int(w*scale)}x{int(h*scale)})")
except binascii.Error as e:
self.console_log(f"转换失败: {str(e)}")
except Exception as e:
self.console_log(f"图像处理失败: {str(e)}")
self.stop_receive()
self.update_progress(100) # 最终进度100%
def update_progress(self, percent):
# 确保进度在0-100之间
percent = max(0, min(100, percent))
self.progress_bar['value'] = percent
self.progress_label.config(text=f"进度: {int(percent)}%")
if __name__ == '__main__':
root = tk.Tk()
app = SerialImageReceiver(root)
root.mainloop()
第二版
-
esp-idf端
#include <stdio.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <esp_log.h> #include "myled.h" #include "sdkconfig.h" #include "mycamera.h" #include "driver/uart.h" static const char *TAG = "example:take_picture"; /*串口测试数据*/ // -------------------------- 串口初始化 -------------------------- #define UART_BAUD_RATE 921600 #define UART_NUM UART_NUM_0 #define BUF_SIZE (1024) void uart_init(void) { uart_config_t uart_config = { .baud_rate = UART_BAUD_RATE, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .source_clk = UART_SCLK_APB }; // 配置串口参数 uart_param_config(UART_NUM, &uart_config); // 设置串口引脚(UART0默认引脚:TX=1,RX=3,无需额外配置) uart_set_pin(UART_NUM, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); // 安装串口驱动(使用中断,提高效率) uart_driver_install(UART_NUM, BUF_SIZE * 2, 0, 0, NULL, 0); } // -------------------------- 串口初始化 -------------------------- /* */ /*串口测试数据*/ void app_main(void) { // ESP_LOGI("MAIN", "Hello, World!"); myled_init(); uart_init(); xTaskCreate(myled_Blink_task, "myled_Blink_task", 2048, NULL, 5, NULL); init_camera(); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); ESP_LOGI(TAG, "Taking picture..."); camera_fb_t *pic = esp_camera_fb_get(); if (pic) { // 串口发送图片数据 int i=pic->len; printf("IMG_START\n"); uart_write_bytes(UART_NUM_0, pic->buf, pic->len); // 用uart库直接发二进制 printf("\r\n"); printf("IMG_END\n"); ESP_LOGI(TAG, "Picture taken! Its size was: %zu bytes", pic->len); esp_camera_fb_return(pic); } else { ESP_LOGE(TAG, "Camera capture failed"); } } }python上位机端
import serial import tkinter as tk from tkinter import messagebox, ttk from PIL import Image, ImageTk, ImageFile import io import time # 支持加载可能被截断的图像 ImageFile.LOAD_TRUNCATED_IMAGES = True class SerialImageReceiver: def __init__(self, root): self.root = root self.root.title("高速串口图像接收器") self.root.geometry("800x600") # 核心状态变量 self.ser = None self.receiving = False self.raw_buffer = b'' # 原始接收缓冲区 self.flag_detected = False # 开始标志检测状态 self.image_data = b'' # 二进制图像数据 # 通信协议标志(与ESP32严格一致) self.start_flag = b'IMG_START\r\n' self.end_flag = b'IMG_END\r\n' # 进度控制参数(根据实际图像大小调整) self.estimated_max_size = 50000 # 增大预估尺寸,适配更多场景 self.received_size = 0 self.waiting_progress = 0 # 初始化界面 self.setup_ui() print("程序启动,等待图像数据...") def setup_ui(self): # 1. 串口配置区域 config_frame = ttk.LabelFrame(self.root, text="串口设置") config_frame.pack(fill=tk.X, padx=10, pady=5) ttk.Label(config_frame, text="端口:").grid(row=0, column=0, padx=5, pady=5) self.port_var = tk.StringVar(value="COM16") # 替换为实际串口(如COM3) self.port_entry = ttk.Entry(config_frame, textvariable=self.port_var, width=10) self.port_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Label(config_frame, text="波特率:").grid(row=0, column=2, padx=5, pady=5) self.baud_var = tk.StringVar(value="921600") # 与ESP32发送端一致 self.baud_entry = ttk.Entry(config_frame, textvariable=self.baud_var, width=10) self.baud_entry.grid(row=0, column=3, padx=5, pady=5) self.start_btn = ttk.Button(config_frame, text="开始接收", command=self.start_receive) self.start_btn.grid(row=0, column=4, padx=5, pady=5) self.stop_btn = ttk.Button(config_frame, text="停止接收", command=self.stop_receive, state=tk.DISABLED) self.stop_btn.grid(row=0, column=5, padx=5, pady=5) # 2. 进度显示区域 progress_frame = ttk.Frame(self.root) progress_frame.pack(fill=tk.X, padx=10, pady=2) self.status_label = ttk.Label(progress_frame, text="状态: 未连接") self.status_label.pack(side=tk.LEFT, padx=5) self.progress_bar = ttk.Progressbar(progress_frame, orient="horizontal", length=500, mode="determinate") self.progress_bar.pack(side=tk.RIGHT, expand=True, fill=tk.X, padx=5) # 3. 图像显示区域 image_frame = ttk.LabelFrame(self.root, text="图像预览") image_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) self.image_label = ttk.Label(image_frame, text="等待接收图像...") self.image_label.pack(fill=tk.BOTH, expand=True) def log(self, message): """带时间戳的日志输出""" timestamp = time.strftime("[%H:%M:%S] ") print(timestamp + message) def update_status(self, progress, text): """更新进度条和状态文本""" self.progress_bar['value'] = min(100, max(0, progress)) self.status_label.config(text=f"状态: {text}") self.root.update_idletasks() # 强制刷新UI,避免进度条卡顿 def start_receive(self): """启动接收流程""" if self.receiving: messagebox.showwarning("提示", "已在接收中,请先停止") return # 获取串口参数 port = self.port_var.get().strip() try: baudrate = int(self.baud_var.get().strip()) except ValueError: messagebox.showerror("错误", "波特率必须为整数") return # 尝试打开串口(使用pyserial的正确参数) try: self.ser = serial.Serial( port=port, baudrate=baudrate, timeout=0.01, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS ) if not self.ser.is_open: raise Exception("串口未正常打开") except Exception as e: messagebox.showerror("串口错误", f"打开失败: {str(e)}") return # 初始化接收状态 self.receiving = True self.flag_detected = False self.raw_buffer = b'' self.image_data = b'' self.received_size = 0 self.waiting_progress = 0 # 更新UI self.start_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) self.update_status(0, f"已连接 {port} ({baudrate}波特率)") self.log(f"串口打开成功: {port} {baudrate}") # 启动接收循环 self.receive_loop() def stop_receive(self): """停止接收并清理资源""" self.receiving = False if self.ser and self.ser.is_open: self.ser.close() self.log("串口已关闭") # 重置UI self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.update_status(0, "已停止接收") self.image_label.config(text="等待接收图像...", image="") def receive_loop(self): """核心接收循环,分阶段处理数据""" if not self.receiving or not self.ser or not self.ser.is_open: return try: # 读取可用数据(一次最多4096字节,平衡效率和响应速度) if self.ser.in_waiting > 0: data = self.ser.read(min(self.ser.in_waiting, 4096)) self.raw_buffer += data self.log(f"接收数据: {len(data)}字节 (累计: {len(self.raw_buffer)}字节)") # 阶段1: 等待开始标志 (0-30%) if not self.flag_detected: # 缓慢增长进度,避免用户误以为程序无响应 self.waiting_progress += min(1, 30 * len(data) / self.estimated_max_size) self.update_status(self.waiting_progress, "等待图像开始标志...") if self.start_flag in self.raw_buffer: # 提取开始标志后的有效数据 start_idx = self.raw_buffer.index(self.start_flag) + len(self.start_flag) self.raw_buffer = self.raw_buffer[start_idx:] self.flag_detected = True self.received_size = len(self.raw_buffer) self.log("检测到开始标志,开始接收图像数据") self.update_status(30, "接收图像数据中...") # 阶段2: 接收图像数据 (30-95%) elif self.flag_detected and self.end_flag not in self.raw_buffer: self.received_size = len(self.raw_buffer) # 计算进度(映射到30-95%区间) progress = 30 + 65 * min(1.0, self.received_size / self.estimated_max_size) self.update_status(progress, f"已接收: {self.received_size}字节") # 阶段3: 检测到结束标志,处理图像 if self.flag_detected and self.end_flag in self.raw_buffer: end_idx = self.raw_buffer.index(self.end_flag) self.image_data = self.raw_buffer[:end_idx] self.log(f"检测到结束标志,图像大小: {len(self.image_data)}字节") self.receiving = False self.update_status(95, "处理图像中...") self.process_image() except Exception as e: self.log(f"接收错误: {str(e)}") self.stop_receive() return # 继续循环(短延迟保证响应速度) if self.receiving: self.root.after(1, self.receive_loop) def process_image(self): """快速处理并显示图像""" try: # 保存图像(可选) with open("received_image.jpg", "wb") as f: f.write(self.image_data) # 快速显示图像 image = Image.open(io.BytesIO(self.image_data)) # 高效缩放(保持比例) image.thumbnail((700, 500)) # 限制最大尺寸 tk_img = ImageTk.PhotoImage(image) self.image_label.config(image=tk_img, text="") self.image_label.image = tk_img # 防止图像被垃圾回收 self.log(f"图像显示成功: {image.size[0]}x{image.size[1]}") self.update_status(100, "图像接收完成") except Exception as e: self.log(f"图像处理失败: {str(e)}") self.update_status(100, "处理失败,请重试") # 可选:关闭自动重连,改为手动控制(避免串口持续占用) # 如需自动重连,保留下面一行;否则注释掉 # self.root.after(500, self.start_receive) if __name__ == "__main__": # 确保使用pyserial库(避免serial库冲突) try: import serial.tools.list_ports except ImportError: messagebox.showerror("库错误", "请安装pyserial库:pip install pyserial") else: root = tk.Tk() app = SerialImageReceiver(root) root.mainloop()展示
-
代码演示效果如下:
-
2025-10-02 18-15-45
3752

被折叠的 条评论
为什么被折叠?



