RK3399 电子相册进阶:目录遍历 + 链表存储,实现自动加载图片(附完整项目)

前言

上一篇我们已经实现了 RK3399 开发板的 JPEG 图片显示和 BMP 转 JPEG 功能,但有个明显的不足:图片路径需要手动写死在代码里,如果要新增或替换图片,就得修改代码重新编译,非常不方便。

这篇文章我们升级功能:通过目录文件操作 API自动扫描指定文件夹下的所有图片,用链表动态存储图片路径(解决数组固定大小的局限性),最终实现 “一键启动、自动加载所有图片、循环播放” 的完整电子相册。

整个过程由浅入深,从 API 讲解到链表实现,再到项目整合,新手也能一步步跟着做,而且项目亮点十足,非常适合写进简历,应对面试官的深入提问~

一、核心知识点铺垫:为什么需要这两个技术?

在整合项目前,先搞懂两个核心技术的作用:

技术解决的问题优势
目录文件操作 API自动获取指定文件夹下的所有图片文件名不用手动写路径,新增图片直接放文件夹即可
链表存储动态存储不确定数量的图片路径图片数量可多可少,不浪费内存(数组做不到)

举个例子:如果用数组存储图片路径,假设数组大小是 10,最多只能存 10 张图;但用链表,不管是 5 张还是 20 张,都能动态扩展存储,灵活性拉满~

二、目录文件操作 API 详解(自动扫图核心)

要实现 “自动找图片”,需要用到 Linux 系统的 3 个核心 API:opendir(打开目录)、readdir(读取文件)、closedir(关闭目录)。

2.1 核心 API 速查表

API 函数头文件函数原型核心功能参数说明返回值说明
opendir<sys/types.h>+<dirent.h>DIR *opendir(const char *name)打开指定目录name:目录路径(如/root/photos成功:返回目录指针(DIR*);失败:NULL
readdir<dirent.h>struct dirent *readdir(DIR *dirp)读取目录下一个文件的信息dirpopendir返回的目录指针成功:返回文件信息结构体;失败 / 读完:NULL
closedir<dirent.h>int closedir(DIR *dirp)关闭目录,释放资源dirpopendir返回的目录指针成功:0;失败:-1

2.2 关键结构体:struct dirent(存储文件信息)

readdir返回的struct dirent结构体,包含了文件的核心信息,我们只需要关注两个字段:

c

运行

struct dirent {
    unsigned char  d_type;      // 文件类型(重点!)
    char           d_name[256]; // 文件名(重点!如1.jpg、2.bmp)
};
重点字段说明:
  • d_type:文件类型,用来过滤图片文件(避免把文件夹、设备文件当成图片),常用取值:
    • DT_REG:普通文件(我们要的图片文件就是这种);
    • DT_DIR:目录文件(跳过,不处理);
    • DT_LNK:符号链接(跳过)。
  • d_name:存储文件名(含后缀),比如"test.jpg""background.bmp"

2.3 示例 1:获取指定目录下的所有图片文件名

下面代码实现:打开/root/photos目录,自动过滤出.jpg.jpeg.bmp格式的图片,打印文件名。

c

运行

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>

// 图片格式过滤函数:判断文件名是否为图片
int is_image(const char *filename) {
    // 支持的图片后缀(可扩展,如添加.png)
    const char *suffixes[] = {".jpg", ".jpeg", ".bmp", ".JPG", ".BMP"};
    int suffix_num = sizeof(suffixes) / sizeof(suffixes[0]);
    
    // 遍历所有后缀,判断文件名是否以其中一个结尾
    for (int i = 0; i < suffix_num; i++) {
        if (strstr(filename, suffixes[i]) != NULL) {
            return 1; // 是图片
        }
    }
    return 0; // 不是图片
}

int main() {
    DIR *dirp;
    struct dirent *file_info;
    const char *photo_dir = "/root/photos"; // 图片文件夹路径

    // 1. 打开目录
    dirp = opendir(photo_dir);
    if (dirp == NULL) {
        perror("opendir failed");
        return -1;
    }
    printf("成功打开目录:%s\n", photo_dir);

    // 2. 循环读取目录下的所有文件
    while ((file_info = readdir(dirp)) != NULL) {
        // 跳过目录文件(.和..是当前目录和上级目录,也跳过)
        if (file_info->d_type != DT_REG) {
            continue;
        }

        // 3. 过滤出图片文件
        if (is_image(file_info->d_name)) {
            printf("找到图片:%s\n", file_info->d_name);
        }
    }

    // 4. 关闭目录(必须调用,释放资源)
    closedir(dirp);
    return 0;
}

2.4 编译与运行效果

编译命令(Ubuntu 交叉编译):

bash

运行

aarch64-linux-gnu-gcc dir_scan.c -o dir_scan
运行步骤:
  1. 在 RK3399 的/root/目录下创建photos文件夹,放入几张图片;
  2. dir_scan拷贝到 RK3399,执行chmod +x dir_scan
  3. 运行:./dir_scan

三、链表实现:动态存储图片路径

现在我们已经能自动找到图片了,接下来用有头双向循环链表存储图片路径(这种链表操作方便,首尾衔接,遍历效率高)。

3.1 链表基础:节点结构设计

每个节点存储一张图片的完整路径(目录 + 文件名),链表节点定义:

c

运行

// 链表节点结构体
typedef struct Node {
    char img_path[1024];  // 存储图片完整路径(如/root/photos/1.jpg)
    struct Node *prev;    // 前驱指针(指向前一个节点)
    struct Node *next;    // 后继指针(指向后一个节点)
} Node;

// 链表头结构体(不存储数据,只用来管理链表)
typedef struct List {
    Node *head;  // 指向链表的头节点
    int count;   // 链表中节点的数量(图片总数)
} List;

3.2 链表核心操作函数(4 个必备函数)

3.2.1 1. 创建链表头(初始化链表)

c

运行

// 创建链表头,返回链表指针
List *list_create() {
    List *list = (List *)malloc(sizeof(List));
    if (list == NULL) {
        perror("malloc list failed");
        return NULL;
    }

    // 初始化链表头:head指向自己(双向循环链表的特点)
    list->head = (Node *)malloc(sizeof(Node));
    list->head->prev = list->head;
    list->head->next = list->head;
    list->count = 0; // 初始时没有节点

    return list;
}
3.2.2 2. 创建节点并插入链表

c

运行

// 向链表尾部插入一个节点(存储图片路径)
int list_insert(List *list, const char *img_path) {
    if (list == NULL || img_path == NULL) {
        return -1;
    }

    // 1. 创建新节点
    Node *new_node = (Node *)malloc(sizeof(Node));
    if (new_node == NULL) {
        perror("malloc node failed");
        return -1;
    }

    // 2. 给新节点赋值(存储图片路径)
    strncpy(new_node->img_path, img_path, sizeof(new_node->img_path)-1);
    new_node->img_path[sizeof(new_node->img_path)-1] = '\0'; // 避免字符串溢出

    // 3. 插入链表尾部(双向循环链表插入逻辑)
    Node *tail = list->head->prev; // 找到链表尾部节点
    new_node->prev = tail;         // 新节点的前驱指向尾部
    new_node->next = list->head;   // 新节点的后继指向链表头
    tail->next = new_node;         // 尾部节点的后继指向新节点
    list->head->prev = new_node;   // 链表头的前驱指向新节点

    list->count++; // 节点数量+1
    return 0;
}
3.2.3 3. 遍历链表(获取所有图片路径)

c

运行

// 遍历链表,打印所有图片路径(测试用)
void list_traverse(List *list) {
    if (list == NULL || list->count == 0) {
        printf("链表为空\n");
        return;
    }

    Node *p = list->head->next; // 从第一个数据节点开始遍历
    int i = 0;
    while (p != list->head) { // 循环到链表头结束
        printf("第%d张图片路径:%s\n", ++i, p->img_path);
        p = p->next; // 移动到下一个节点
    }
}
3.2.4 4. 释放链表(避免内存泄漏)

c

运行

// 释放链表所有资源
void list_destroy(List *list) {
    if (list == NULL) {
        return;
    }

    Node *p = list->head->next;
    Node *temp;
    while (p != list->head) {
        temp = p;
        p = p->next;
        free(temp); // 释放每个数据节点
    }

    free(list->head); // 释放链表头
    free(list);       // 释放链表
    list = NULL;
}

3.3 链表操作示例(整合目录扫描)

下面代码实现:扫描目录→找到图片→插入链表→遍历链表。

c

运行

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>

// 链表节点和链表头定义(同上)
typedef struct Node {
    char img_path[1024];
    struct Node *prev;
    struct Node *next;
} Node;

typedef struct List {
    Node *head;
    int count;
} List;

// 链表操作函数(同上:list_create、list_insert、list_traverse、list_destroy)
// ...(此处省略上面的4个函数,实际代码中需要包含)

// 图片格式过滤函数(同上)
int is_image(const char *filename) {
    const char *suffixes[] = {".jpg", ".jpeg", ".bmp", ".JPG", ".BMP"};
    int suffix_num = sizeof(suffixes) / sizeof(suffixes[0]);
    for (int i = 0; i < suffix_num; i++) {
        if (strstr(filename, suffixes[i]) != NULL) {
            return 1;
        }
    }
    return 0;
}

// 扫描目录,将图片路径插入链表
int scan_photos_to_list(const char *dir_path, List *list) {
    DIR *dirp = opendir(dir_path);
    if (dirp == NULL) {
        perror("opendir failed");
        return -1;
    }

    struct dirent *file_info;
    char full_path[1024]; // 存储图片完整路径

    while ((file_info = readdir(dirp)) != NULL) {
        // 跳过非普通文件
        if (file_info->d_type != DT_REG) {
            continue;
        }

        // 过滤图片文件
        if (is_image(file_info->d_name)) {
            // 拼接完整路径:目录路径 + 文件名
            snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, file_info->d_name);
            // 插入链表
            list_insert(list, full_path);
        }
    }

    closedir(dirp);
    return 0;
}

int main() {
    const char *photo_dir = "/root/photos";
    // 1. 创建链表
    List *img_list = list_create();
    if (img_list == NULL) {
        return -1;
    }

    // 2. 扫描目录,将图片路径存入链表
    if (scan_photos_to_list(photo_dir, img_list) != 0) {
        list_destroy(img_list);
        return -1;
    }

    // 3. 遍历链表,打印图片路径
    printf("共找到%d张图片:\n", img_list->count);
    list_traverse(img_list);

    // 4. 释放链表
    list_destroy(img_list);
    return 0;
}

3.4 运行效果

编译后在 RK3399 上运行,会打印出所有图片的完整路径,链表节点数量等于图片数量,实现了动态存储~

四、终极整合:完整电子相册项目(目录扫描 + 链表 + JPEG 显示)

现在把前面的技术整合起来,实现完整的电子相册:

  1. 自动扫描/root/photos目录下的所有图片;
  2. 用链表存储图片完整路径;
  3. 循环显示所有图片(每张显示 3 秒);
  4. 支持 ESC 键退出。

4.1 完整代码

c

运行

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <setjmp.h>
#include <jpeglib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <SDL2/SDL.h> // 用于事件处理(ESC退出)

// 链表相关定义和函数
typedef struct Node {
    char img_path[1024];
    struct Node *prev;
    struct Node *next;
} Node;

typedef struct List {
    Node *head;
    int count;
} List;

List *list_create() {
    List *list = (List *)malloc(sizeof(List));
    if (list == NULL) {
        perror("malloc list failed");
        return NULL;
    }
    list->head = (Node *)malloc(sizeof(Node));
    list->head->prev = list->head;
    list->head->next = list->head;
    list->count = 0;
    return list;
}

int list_insert(List *list, const char *img_path) {
    if (list == NULL || img_path == NULL) {
        return -1;
    }
    Node *new_node = (Node *)malloc(sizeof(Node));
    if (new_node == NULL) {
        perror("malloc node failed");
        return -1;
    }
    strncpy(new_node->img_path, img_path, sizeof(new_node->img_path)-1);
    new_node->img_path[sizeof(new_node->img_path)-1] = '\0';
    Node *tail = list->head->prev;
    new_node->prev = tail;
    new_node->next = list->head;
    tail->next = new_node;
    list->head->prev = new_node;
    list->count++;
    return 0;
}

void list_destroy(List *list) {
    if (list == NULL) {
        return;
    }
    Node *p = list->head->next;
    Node *temp;
    while (p != list->head) {
        temp = p;
        p = p->next;
        free(temp);
    }
    free(list->head);
    free(list);
    list = NULL;
}

// 图片格式过滤
int is_image(const char *filename) {
    const char *suffixes[] = {".jpg", ".jpeg", ".bmp", ".JPG", ".BMP"};
    int suffix_num = sizeof(suffixes) / sizeof(suffixes[0]);
    for (int i = 0; i < suffix_num; i++) {
        if (strstr(filename, suffixes[i]) != NULL) {
            return 1;
        }
    }
    return 0;
}

// 扫描图片到链表
int scan_photos_to_list(const char *dir_path, List *list) {
    DIR *dirp = opendir(dir_path);
    if (dirp == NULL) {
        perror("opendir failed");
        return -1;
    }
    struct dirent *file_info;
    char full_path[1024];
    while ((file_info = readdir(dirp)) != NULL) {
        if (file_info->d_type != DT_REG) {
            continue;
        }
        if (is_image(file_info->d_name)) {
            snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, file_info->d_name);
            list_insert(list, full_path);
        }
    }
    closedir(dirp);
    return 0;
}

// LCD初始化(显存映射)
int lcd_init(int *fd_lcd, int **pfile_lcd, struct fb_var_screeninfo *var_info) {
    *fd_lcd = open("/dev/fb0", O_RDWR);
    if (*fd_lcd < 0) {
        perror("open /dev/fb0 failed");
        return -1;
    }
    struct fb_fix_screeninfo fix_info;
    if (ioctl(*fd_lcd, FBIOGET_FSCREENINFO, &fix_info) < 0) {
        perror("ioctl FBIOGET_FSCREENINFO failed");
        return -1;
    }
    if (ioctl(*fd_lcd, FBIOGET_VSCREENINFO, var_info) < 0) {
        perror("ioctl FBIOGET_VSCREENINFO failed");
        return -1;
    }
    *pfile_lcd = (int *)mmap(NULL, fix_info.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, *fd_lcd, 0);
    if (*pfile_lcd == MAP_FAILED) {
        perror("mmap fb0 failed");
        return -1;
    }
    return 0;
}

// JPEG图片显示函数
int jpeg_show(const char *img_path, int *pfile_lcd, struct fb_var_screeninfo var_info) {
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_decompress(&cinfo);

    FILE *infile = fopen(img_path, "rb");
    if (infile == NULL) {
        fprintf(stderr, "open %s failed\n", img_path);
        return -1;
    }
    jpeg_stdio_src(&cinfo, infile);

    jpeg_read_header(&cinfo, TRUE);
    cinfo.scale_num = 1;
    cinfo.scale_denom = 1;
    cinfo.out_color_space = JCS_RGB;
    jpeg_start_decompress(&cinfo);

    unsigned char *buffer = (unsigned char *)malloc(cinfo.output_width * cinfo.output_components);
    if (buffer == NULL) {
        perror("malloc buffer failed");
        return -1;
    }

    // 清空屏幕(黑色背景)
    memset(pfile_lcd, 0, var_info.xres * var_info.yres * 4);

    int x, color;
    while (cinfo.output_scanline < cinfo.output_height) {
        jpeg_read_scanlines(&cinfo, &buffer, 1);
        for (x = 0; x < cinfo.output_width; x++) {
            color = (255 << 24) | (buffer[x*3] << 16) | (buffer[x*3+1] << 8) | buffer[x*3+2];
            *(pfile_lcd + x + var_info.xres * (cinfo.output_scanline - 1)) = color;
        }
    }

    free(buffer);
    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);
    fclose(infile);
    return 0;
}

int main() {
    const char *photo_dir = "/root/photos";
    List *img_list = list_create();
    if (img_list == NULL) {
        return -1;
    }

    // 扫描图片到链表
    if (scan_photos_to_list(photo_dir, img_list) != 0 || img_list->count == 0) {
        printf("未找到图片!\n");
        list_destroy(img_list);
        return -1;
    }
    printf("共找到%d张图片,开始循环播放...(ESC键退出)\n", img_list->count);

    // 初始化LCD
    int fd_lcd;
    int *pfile_lcd;
    struct fb_var_screeninfo var_info;
    if (lcd_init(&fd_lcd, &pfile_lcd, &var_info) != 0) {
        list_destroy(img_list);
        return -1;
    }

    // 初始化SDL(用于事件处理,监测ESC键)
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        fprintf(stderr, "SDL init failed: %s\n", SDL_GetError());
        list_destroy(img_list);
        return -1;
    }

    // 循环播放图片
    Node *current_node = img_list->head->next; // 当前显示的节点
    SDL_Event event;
    int running = 1;
    while (running) {
        // 显示当前图片
        jpeg_show(current_node->img_path, pfile_lcd, var_info);
        printf("正在显示:%s\n", current_node->img_path);

        // 延时3秒(同时监测ESC键)
        int delay = 3000; // 3000毫秒
        while (delay > 0 && running) {
            SDL_PollEvent(&event);
            if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) {
                running = 0; // ESC键退出
                break;
            }
            usleep(1000); // 1毫秒
            delay--;
        }

        // 切换到下一张图片(链表循环)
        current_node = current_node->next;
        if (current_node == img_list->head) { // 到尾部了,回到头部
            current_node = img_list->head->next;
        }
    }

    // 释放所有资源
    SDL_Quit();
    munmap(pfile_lcd, var_info.xres * var_info.yres * 4);
    close(fd_lcd);
    list_destroy(img_list);
    printf("程序退出!\n");
    return 0;
}

4.2 编译与运行

编译命令(需链接 SDL2 和 libjpeg):

bash

运行

aarch64-linux-gnu-gcc album.c -o album -I/home/你的用户名/work/rk3399/armlib/include \
-L/home/你的用户名/work/rk3399/armlib/lib -ljpeg -lSDL2
运行步骤:
  1. 在 RK3399 的/root/photos目录放入图片;
  2. 拷贝album到开发板,执行chmod +x album
  3. 运行:./album
  4. 效果:LCD 循环显示所有图片,按 ESC 键退出。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值