【SSD202 开发实战 18】JPEG 编解码与图片处理
一、开发环境准备
1.1 硬件平台
- SSD202D 开发板(支持 MIPI/TTL 屏幕,推荐使用官方评估板)
- TF 卡(存储测试图片)
- 串口调试工具(115200 波特率)
1.2 软件环境
- SSD202 SDK(Alkaid 版本,推荐 IKAYAKI_DLM00V015)
- 交叉编译工具链(arm-linux-gcc)
- libjpeg 库(v9 版本,已适配 SSD202 平台)
- 文件传输工具(TFTP/SCP)
1.3 库文件配置
将 libjpeg 的动态链接库拷贝到 SSD202 开发板的/usr/lib目录下:
# 交叉编译libjpeg后,拷贝到开发板 scp libjpeg.so.9 root@192.168.1.100:/usr/lib/二、JPEG 解码原理与实现
2.1 JPEG 解码流程
- 初始化 JPEG 解压对象并设置错误处理
- 指定输入文件(或内存缓冲区)
- 读取 JPEG 文件头获取图像信息
- 设置解压参数(输出格式、缩放比例等)
- 启动解压过程
- 逐行读取解压后的图像数据
- 完成解压并释放资源
2.2 完整 JPEG 解码代码
/** * @file jpeg_decoder.c * @brief SSD202平台JPEG解码实现,输出RGB888格式数据 * @author 嵌入式全栈工程师 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <jpeglib.h> #include <setjmp.h> /** * @brief JPEG解码错误处理结构体 */ typedef struct { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; } my_error_mgr; /** * @brief JPEG解码错误处理函数 */ METHODDEF(void) my_error_exit(j_common_ptr cinfo) { my_error_mgr *myerr = (my_error_mgr *)cinfo->err; (*cinfo->err->output_message)(cinfo); longjmp(myerr->setjmp_buffer, 1); } /** * @brief JPEG图片解码函数 * @param[in] filename 输入JPEG文件名 * @param[out] width 输出图像宽度 * @param[out] height 输出图像高度 * @param[out] channels 输出图像通道数 * @return 解码后的RGB数据缓冲区,失败返回NULL */ unsigned char* jpeg_decode(const char *filename, int *width, int *height, int *channels) { struct jpeg_decompress_struct cinfo; my_error_mgr jerr; FILE *infile; JSAMPARRAY buffer; int row_stride; unsigned char *rgb_data = NULL; unsigned char *ptr = NULL; // 打开JPEG文件 if ((infile = fopen(filename, "rb")) == NULL) { fprintf(stderr, "无法打开文件: %s\n", filename); return NULL; } // 初始化JPEG解压对象 cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); fclose(infile); return NULL; } jpeg_create_decompress(&cinfo); // 指定输入文件 jpeg_stdio_src(&cinfo, infile); // 读取JPEG文件头 (void)jpeg_read_header(&cinfo, TRUE); // 设置输出格式为RGB888 cinfo.out_color_space = JCS_RGB; *channels = 3; // 启动解压过程 (void)jpeg_start_decompress(&cinfo); // 获取图像尺寸 *width = cinfo.output_width; *height = cinfo.output_height; // 计算每行数据长度 row_stride = cinfo.output_width * cinfo.output_components; // 分配RGB数据缓冲区 rgb_data = (unsigned char *)malloc(*width * *height * *channels); if (rgb_data == NULL) { fprintf(stderr, "内存分配失败\n"); jpeg_destroy_decompress(&cinfo); fclose(infile); return NULL; } ptr = rgb_data; // 分配一行数据缓冲区 buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1); // 逐行读取解压数据 while (cinfo.output_scanline < cinfo.output_height) { (void)jpeg_read_scanlines(&cinfo, buffer, 1); memcpy(ptr, buffer[0], row_stride); ptr += row_stride; } // 完成解压 (void)jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose(infile); return rgb_data; } // 测试代码 int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "用法: %s <jpeg文件名>\n", argv[0]); return -1; } int width, height, channels; unsigned char *rgb_data = jpeg_decode(argv[1], &width, &height, &channels); if (rgb_data == NULL) { fprintf(stderr, "JPEG解码失败\n"); return -1; } printf("JPEG解码成功: %dx%d, %d通道\n", width, height, channels); // TODO: 这里可以添加SSD202显示代码,将RGB数据显示到屏幕上 free(rgb_data); return 0; }三、JPEG 编码原理与实现
3.1 JPEG 编码流程
- 初始化 JPEG 压缩对象并设置错误处理
- 指定输出文件(或内存缓冲区)
- 设置压缩参数(图像尺寸、颜色空间、质量等)
- 启动压缩过程
- 逐行写入图像数据
- 完成压缩并释放资源
3.2 完整 JPEG 编码代码
/** * @file jpeg_encoder.c * @brief SSD202平台JPEG编码实现,输入RGB888格式数据 * @author 嵌入式全栈工程师 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <jpeglib.h> /** * @brief JPEG图片编码函数 * @param[in] filename 输出JPEG文件名 * @param[in] rgb_data 输入RGB图像数据 * @param[in] width 图像宽度 * @param[in] height 图像高度 * @param[in] quality 压缩质量(1-100) * @return 0成功,非0失败 */ int jpeg_encode(const char *filename, const unsigned char *rgb_data, int width, int height, int quality) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; FILE *outfile; JSAMPROW row_pointer[1]; int row_stride; // 打开输出文件 if ((outfile = fopen(filename, "wb")) == NULL) { fprintf(stderr, "无法创建文件: %s\n", filename); return -1; } // 初始化JPEG压缩对象 cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); // 指定输出文件 jpeg_stdio_dest(&cinfo, outfile); // 设置压缩参数 cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; // 设置默认压缩参数 jpeg_set_defaults(&cinfo); // 设置压缩质量 jpeg_set_quality(&cinfo, quality, TRUE); // 启动压缩过程 jpeg_start_compress(&cinfo, TRUE); // 计算每行数据长度 row_stride = width * 3; // 逐行写入图像数据 while (cinfo.next_scanline < cinfo.image_height) { row_pointer[0] = (JSAMPLE *)&rgb_data[cinfo.next_scanline * row_stride]; (void)jpeg_write_scanlines(&cinfo, row_pointer, 1); } // 完成压缩 jpeg_finish_compress(&cinfo); fclose(outfile); jpeg_destroy_compress(&cinfo); return 0; } // 测试代码 int main(int argc, char *argv[]) { if (argc != 5) { fprintf(stderr, "用法: %s <输出文件名> <宽度> <高度> <质量>\n", argv[0]); return -1; } int width = atoi(argv[2]); int height = atoi(argv[3]); int quality = atoi(argv[4]); // 模拟生成RGB测试数据(渐变图) unsigned char *rgb_data = (unsigned char *)malloc(width * height * 3); if (rgb_data == NULL) { fprintf(stderr, "内存分配失败\n"); return -1; } for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int idx = (y * width + x)*3; rgb_data[idx] = (x * 255)/width; // R通道 rgb_data[idx + 1] = (y * 255)/height; // G通道 rgb_data[idx + 2] = 128; // B通道 } } // 进行JPEG编码 if (jpeg_encode(argv[1], rgb_data, width, height, quality) == 0) { printf("JPEG编码成功: %s\n", argv[1]); } else { fprintf(stderr, "JPEG编码失败\n"); } free(rgb_data); return 0; }四、SSD202 平台图片处理实战
4.1 图片缩放实现(最近邻插值算法)
/** * @brief 图片缩放函数(最近邻插值) * @param[in] src_data 输入图像数据 * @param[in] src_w 输入宽度 * @param[in] src_h 输入高度 * @param[out] dst_data 输出图像数据 * @param[in] dst_w 输出宽度 * @param[in] dst_h 输出高度 * @param[in] channels 通道数 */ void image_resize_nearest(const unsigned char *src_data, int src_w, int src_h, unsigned char *dst_data, int dst_w, int dst_h, int channels) { float scale_w = (float)src_w / dst_w; float scale_h = (float)src_h / dst_h; for (int y = 0; y < dst_h; y++) { for (int x = 0; x < dst_w; x++) { // 计算源图像坐标 int src_x = (int)(x * scale_w); int src_y = (int)(y * scale_h); src_x = src_x < src_w ? src_x : src_w - 1; src_y = src_y < src_h ? src_y : src_h - 1; // 复制像素数据 int src_idx = (src_y * src_w + src_x) * channels; int dst_idx = (y * dst_w + x) * channels; memcpy(&dst_data[dst_idx], &src_data[src_idx], channels); } } }4.2 图片旋转实现(90 度顺时针旋转)
/** * @brief 图片90度顺时针旋转 * @param[in] src_data 输入图像数据 * @param[in] width 输入宽度 * @param[in] height 输入高度 * @param[out] dst_data 输出图像数据 * @param[in] channels 通道数 */ void image_rotate90_clockwise(const unsigned char *src_data, int width, int height, unsigned char *dst_data, int channels) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int src_idx = (y * width + x) * channels; int dst_idx = (x * height + (height - 1 - y)) * channels; memcpy(&dst_data[dst_idx], &src_data[src_idx], channels); } } }4.3 图片灰度化实现(加权平均法)
/** * @brief 图片RGB转灰度图 * @param[in] rgb_data 输入RGB图像数据 * @param[out] gray_data 输出灰度图像数据 * @param[in] width 图像宽度 * @param[in] height 图像高度 */ void image_rgb2gray(const unsigned char *rgb_data, unsigned char *gray_data, int width, int height) { for (int i = 0; i < width * height; i++) { int r = rgb_data[i * 3]; int g = rgb_data[i * 3+1]; int b = rgb_data[i * 3+2]; // 加权平均法:Y = 0.299R + 0.587G + 0.114B gray_data[i] = (unsigned char)(0.299*r + 0.587*g + 0.114*b); } }五、SSD202 显示集成实战
5.1 显示接口适配
SSD202 平台使用 FBDEV(FrameBuffer)或 SDK 提供的显示 API 进行图像显示。以下是 FrameBuffer 显示示例:
#include <fcntl.h> #include <sys/mman.h> #include <linux/fb.h> #include <sys/ioctl.h> /** * @brief 初始化FrameBuffer * @param[in] dev_path FrameBuffer设备路径 * @param[out] fbfd 文件描述符 * @param[out] fbp 内存映射地址 * @param[out] var 显示参数 * @return 0成功,非0失败 */ int fb_init(const char *dev_path, int *fbfd, unsigned char **fbp, struct fb_var_screeninfo *var) { *fbfd = open(dev_path, O_RDWR); if (*fbfd == -1) { perror("打开FrameBuffer失败"); return -1; } if (ioctl(*fbfd, FBIOGET_VSCREENINFO, var) == -1) { perror("获取显示参数失败"); close(*fbfd); return -1; } // 计算屏幕缓冲区大小 int screensize = var->xres * var->yres * var->bits_per_pixel / 8; // 内存映射 *fbp = (unsigned char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, *fbfd, 0); if (*fbp == (unsigned char *)-1) { perror("内存映射失败"); close(*fbfd); return -1; } return 0; } /** * @brief 显示RGB图像到FrameBuffer * @param[in] fbp 内存映射地址 * @param[in] var 显示参数 * @param[in] rgb_data RGB图像数据 * @param[in] width 图像宽度 * @param[in] height 图像高度 * @param[in] x 显示X坐标 * @param[in] y 显示Y坐标 */ void fb_display_rgb(unsigned char *fbp, struct fb_var_screeninfo *var, const unsigned char *rgb_data, int width, int height, int x, int y) { int line_length = var->xres * var->bits_per_pixel / 8; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { int rgb_idx = (j * width + i)*3; int fb_idx = ((y + j) * line_length)+((x + i) * var->bits_per_pixel / 8); // 根据屏幕格式转换RGB数据 if (var->bits_per_pixel == 32) { fbp[fb_idx] = rgb_data[rgb_idx + 2]; // B fbp[fb_idx + 1] = rgb_data[rgb_idx + 1]; // G fbp[fb_idx + 2] = rgb_data[rgb_idx]; // R fbp[fb_idx + 3] = 0xff; // A } else if (var->bits_per_pixel == 24) { fbp[fb_idx] = rgb_data[rgb_idx + 2]; // B fbp[fb_idx + 1] = rgb_data[rgb_idx + 1]; // G fbp[fb_idx + 2] = rgb_data[rgb_idx]; // R } } } }5.2 完整应用示例(JPEG 解码 + 缩放 + 显示)
int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "用法: %s <jpeg文件名>\n", argv[0]); return -1; } // 1. JPEG解码 int width, height, channels; unsigned char *rgb_data = jpeg_decode(argv[1], &width, &height, &channels); if (rgb_data == NULL) { fprintf(stderr, "JPEG解码失败\n"); return -1; } printf("JPEG解码成功: %dx%d, %d通道\n", width, height, channels); // 2. 图片缩放(缩放到320x240) int dst_width = 320, dst_height = 240; unsigned char *scaled_data = (unsigned char *)malloc(dst_width * dst_height * channels); if (scaled_data == NULL) { fprintf(stderr, "内存分配失败\n"); free(rgb_data); return -1; } image_resize_nearest(rgb_data, width, height, scaled_data, dst_width, dst_height, channels); printf("图片缩放成功: %dx%d\n", dst_width, dst_height); // 3. 初始化FrameBuffer int fbfd; unsigned char *fbp; struct fb_var_screeninfo var; if (fb_init("/dev/fb0", &fbfd, &fbp, &var) != 0) { fprintf(stderr, "FrameBuffer初始化失败\n"); free(rgb_data); free(scaled_data); return -1; } printf("FrameBuffer初始化成功: %dx%d, %dbpp\n", var.xres, var.yres, var.bits_per_pixel); // 4. 显示图像到屏幕中央 int x = (var.xres - dst_width)/2; int y = (var.yres - dst_height)/2; fb_display_rgb(fbp, &var, scaled_data, dst_width, dst_height, x, y); printf("图像显示成功: 位置(%d, %d)\n", x, y); // 5. 释放资源 free(rgb_data); free(scaled_data); munmap(fbp, var.xres * var.yres * var.bits_per_pixel / 8); close(fbfd); return 0; }六、编译与运行指南
6.1 交叉编译 Makefile
# Makefile for SSD202 JPEG demo CC = arm-linux-gcc CFLAGS = -Wall -O2 -I$(SDK_PATH)/include LDFLAGS = -L$(SDK_PATH)/lib -ljpeg -lm TARGET = jpeg_demo SRC = jpeg_decoder.c jpeg_encoder.c image_process.c fb_display.c main.c all: $(TARGET) $(TARGET): $(SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) clean: rm -f $(TARGET) *.o6.2 运行步骤
- 编译生成可执行文件
make SDK_PATH=/path/to/ssd202/sdk - 将可执行文件和测试图片拷贝到开发板
scp jpeg_demo test.jpg root@192.168.1.100:/root/ - 在开发板上运行
cd /root chmod +x jpeg_demo ./jpeg_demo test.jpg
七、性能优化建议
内存优化
- 使用内存池管理图像缓冲区,避免频繁 malloc/free
- 对于大尺寸图片,采用分块处理策略,减少内存占用
速度优化
- 优先使用 SSD202 硬件加速(如 VPU)进行 JPEG 编解码
- 图片处理算法使用定点运算替代浮点运算,提高执行效率
- 利用 CPU 缓存特性,优化数据访问模式,减少缓存失效
功耗优化
- 编解码完成后及时关闭相关硬件模块
- 合理设置 CPU 频率,在性能和功耗之间取得平衡
八、总结与进阶
本文详细介绍了 SSD202 平台上 JPEG 编解码与图片处理的完整实现流程,包括 libjpeg 库的使用、常用图片处理算法(缩放、旋转、灰度化)以及 SSD202 显示接口的集成。通过本文的代码示例,开发者可以快速掌握 SSD202 平台上的图像处理能力,并应用于实际项目中。
进阶学习方向:
- 探索 SSD202 硬件加速编解码接口(如 MI_JPEG 模块)
- 集成 OpenCV 库实现更复杂的图像处理算法
- 实现 JPEG 图片的实时流处理(如摄像头采集 + JPEG 编码 + 网络传输)
希望本文能帮助您在 SSD202 开发中更高效地处理图像数据,欢迎在评论区交流讨论!
