GLM-OCR文件处理进阶:C语言实现批量图片读取与识别结果输出
GLM-OCR文件处理进阶:C语言实现批量图片读取与识别结果输出
如果你已经用C语言调过几次GLM-OCR的接口,完成了单张图片的识别,可能会觉得不过瘾。毕竟,实际项目里,我们面对的更可能是一个塞满了几百张图片的文件夹,需要的是批量处理,然后把结果整整齐齐地保存下来,方便后续分析。
今天,我们就来把这个流程打通。我会带你用C语言,从零开始,一步步实现一个能读取文件夹里所有图片、调用GLM-OCR批量识别、最后把结果以JSON格式输出到文件的小工具。整个过程,我们会特别关注那些工程上容易踩坑的地方,比如怎么高效遍历文件、如何管理好内存避免泄漏,以及怎么给程序做个简单的“体检”(性能测试)。
准备好了吗?我们这就开始。
1. 环境准备与项目搭建
工欲善其事,必先利其器。我们先来把需要的“家伙事儿”准备好。
1.1 核心工具与库
首先,确保你的开发环境已经就绪。你需要一个C语言编译器,比如GCC或者Clang。在Linux或macOS上,它们通常默认就安装了。在Windows上,你可以使用MinGW或者MSVC。
接下来是几个关键的库:
- GLM-OCR的C语言接口库:这是主角,负责图片的文字识别。你需要从官方渠道获取其动态链接库(
.so、.dylib或.dll)和对应的头文件(.h)。 - stb_image.h:一个非常出色的单头文件图像加载库。它轻量、简单,支持PNG、JPEG、BMP等多种格式,完美符合我们“用C语言读图片”的需求。你只需要去它的GitHub页面下载这一个文件,放到你的项目里就行。
- cJSON:一个超轻量级的C语言JSON解析器/生成器。我们要把识别结果输出成JSON格式,用它再合适不过。同样,它也是一个
.c和一个.h文件,集成起来毫无压力。
你可以手动下载这些库,也可以使用包管理器。这里假设你把它们都放在了项目根目录下的一个libs文件夹里。
1.2 项目目录结构
一个清晰的项目结构能让后续开发省心不少。建议你这样组织:
glm_ocr_batch_demo/ ├── src/ │ ├── main.c # 主程序入口 │ ├── file_utils.c # 文件遍历相关函数 │ ├── image_loader.c # 图片加载(使用stb_image) │ └── ocr_engine.c # OCR引擎封装与结果处理 ├── libs/ │ ├── glm_ocr.h # GLM-OCR头文件 │ ├── glm_ocr.so # GLM-OCR库文件(Linux示例) │ ├── stb_image.h # 图像加载库 │ └── cJSON.h # JSON库头文件 │ └── cJSON.c # JSON库源文件 ├── input_images/ # 存放待识别的图片 ├── output/ # 程序输出的JSON文件目录 ├── Makefile # 编译脚本(或CMakeLists.txt) └── README.md现在,打开你的代码编辑器,在src/main.c里,我们先引入必要的头文件,搭个架子。
// src/main.c #include <stdio.h> #include <stdlib.h> #include <string.h> // 引入我们自己的模块 #include "file_utils.h" #include "image_loader.h" #include "ocr_engine.h" int main(int argc, char *argv[]) { printf("GLM-OCR 批量处理工具启动...\n"); // TODO: 解析命令行参数,获取输入文件夹路径 // TODO: 遍历文件夹,获取图片文件列表 // TODO: 循环处理每张图片 // TODO: 将识别结果汇总并输出为JSON文件 printf("处理完成!\n"); return 0; }框架搭好了,接下来我们逐个击破里面的“TODO”。
2. 核心模块实现:分而治之
我们把大任务拆成几个独立的小模块,这样代码好写,也好维护。
2.1 文件遍历模块
这个模块的任务是:给定一个文件夹路径,找出里面所有支持的图片文件(比如.jpg,.png,.bmp)。
在C语言里,遍历文件夹依赖于操作系统。我们这里以POSIX标准(Linux/macOS)为例,使用<dirent.h>。如果是Windows,需要使用<windows.h>里的FindFirstFile/FindNextFile,原理类似。
// src/file_utils.h #ifndef FILE_UTILS_H #define FILE_UTILS_H // 定义一个结构体来存储文件信息链表 typedef struct ImageFileNode { char file_path[512]; // 文件的完整路径 struct ImageFileNode *next; // 指向下一个节点 } ImageFileNode; // 函数声明 ImageFileNode* find_image_files(const char *dir_path); void free_file_list(ImageFileNode *head); void print_file_list(const ImageFileNode *head); #endif// src/file_utils.c #include "file_utils.h" #include <dirent.h> #include <string.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> // 检查文件扩展名是否为图片格式 static int is_image_file(const char *filename) { const char *ext = strrchr(filename, '.'); if (!ext) return 0; ext++; // 跳过点号 // 转换为小写进行比较(简单处理,生产环境需更严谨) char lower_ext[10]; for(int i=0; ext[i] && i<9; i++){ lower_ext[i] = tolower(ext[i]); } return (strcmp(lower_ext, "jpg") == 0 || strcmp(lower_ext, "jpeg") == 0 || strcmp(lower_ext, "png") == 0 || strcmp(lower_ext, "bmp") == 0); } // 遍历目录,寻找图片文件 ImageFileNode* find_image_files(const char *dir_path) { DIR *dir = opendir(dir_path); if (!dir) { perror("无法打开目录"); return NULL; } ImageFileNode *head = NULL; ImageFileNode *tail = NULL; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { // 跳过当前目录(.)和上级目录(..) if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // 构建完整路径 char full_path[1024]; snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); // 检查是否是普通文件且为图片格式 struct stat path_stat; if (stat(full_path, &path_stat) == 0 && S_ISREG(path_stat.st_mode)) { if (is_image_file(entry->d_name)) { // 创建新节点 ImageFileNode *new_node = (ImageFileNode*)malloc(sizeof(ImageFileNode)); if (!new_node) { perror("内存分配失败"); closedir(dir); free_file_list(head); // 清理已分配的内存 return NULL; } strncpy(new_node->file_path, full_path, sizeof(new_node->file_path)-1); new_node->file_path[sizeof(new_node->file_path)-1] = '\0'; new_node->next = NULL; // 添加到链表尾部 if (!head) { head = new_node; tail = new_node; } else { tail->next = new_node; tail = new_node; } } } } closedir(dir); return head; } // 释放文件列表内存 void free_file_list(ImageFileNode *head) { ImageFileNode *current = head; while (current) { ImageFileNode *next = current->next; free(current); current = next; } } // 打印文件列表(调试用) void print_file_list(const ImageFileNode *head) { int count = 0; const ImageFileNode *current = head; while (current) { printf("%d: %s\n", ++count, current->file_path); current = current->next; } printf("共找到 %d 个图片文件。\n", count); }这个模块的核心是find_image_files函数,它返回一个链表,链表的每个节点都存储了一个图片文件的完整路径。记得用完后要调用free_file_list来释放内存,这是避免内存泄漏的第一步。
2.2 图片加载模块
有了文件路径,下一步就是把图片数据读进内存,转换成GLM-OCR接口能接受的格式。我们用stb_image.h来做这件事,它简单到只需包含一个头文件。
// src/image_loader.h #ifndef IMAGE_LOADER_H #define IMAGE_LOADER_H // 图片数据结构 typedef struct { unsigned char *data; // 图像像素数据 (通常是RGB或RGBA) int width; // 图像宽度 int height; // 图像高度 int channels; // 颜色通道数 (3 for RGB, 4 for RGBA) } ImageData; // 函数声明 ImageData load_image(const char *file_path); void free_image(ImageData *img); #endif// src/image_loader.c #include "image_loader.h" #define STB_IMAGE_IMPLEMENTATION // 重要:必须在包含头文件前定义这个宏 #include "../libs/stb_image.h" #include <stdio.h> #include <stdlib.h> ImageData load_image(const char *file_path) { ImageData img = {NULL, 0, 0, 0}; // stbi_load会自动分配内存,记得最后要用stbi_image_free释放 // 这里我们强制加载为3通道(RGB),因为大多数OCR模型期望RGB输入 img.data = stbi_load(file_path, &img.width, &img.height, &img.channels, 3); if (!img.data) { fprintf(stderr, "错误:无法加载图片 %s。原因:%s\n", file_path, stbi_failure_reason()); img.channels = 3; // 即使失败,也设置期望的通道数 } else { img.channels = 3; // 因为我们要求加载为3通道 printf("成功加载图片:%s (%d x %d, %d channels)\n", file_path, img.width, img.height, img.channels); } return img; } void free_image(ImageData *img) { if (img && img->data) { stbi_image_free(img->data); img->data = NULL; img->width = img->height = img->channels = 0; } }stb_image帮我们处理了不同格式图片的解码,返回统一的RGB数据。注意STB_IMAGE_IMPLEMENTATION宏只需要在一个源文件中定义一次。
2.3 OCR引擎与结果处理模块
这是连接我们代码和GLM-OCR库的桥梁。我们在这里初始化OCR引擎、调用识别函数,并把返回的识别结果转换成我们想要的格式。
// src/ocr_engine.h #ifndef OCR_ENGINE_H #define OCR_ENGINE_H #include "../libs/cJSON.h" // 我们使用cJSON来构建结果 // OCR识别结果结构 typedef struct { char **text_lines; // 识别出的文本行数组 int line_count; // 文本行数 float confidence; // 整体置信度(可选) } OcrResult; // 函数声明 int init_ocr_engine(const char *model_path); OcrResult recognize_image(const ImageData *img); void free_ocr_result(OcrResult *result); cJSON* build_json_result(const char *image_path, const OcrResult *result); #endif// src/ocr_engine.c #include "ocr_engine.h" #include "../libs/glm_ocr.h" // GLM-OCR的C接口头文件 #include <stdio.h> #include <stdlib.h> #include <string.h> // 假设GLM-OCR的句柄和函数(具体函数名需根据实际接口调整) static void* ocr_handle = NULL; int init_ocr_engine(const char *model_path) { // 这里调用GLM-OCR提供的初始化函数 // 例如:ocr_handle = glm_ocr_init(model_path); // 如果实际接口不同,请参照官方文档修改 printf("正在初始化OCR引擎,模型路径:%s\n", model_path); // 模拟初始化成功 ocr_handle = (void*)0x1; // 伪句柄,实际应为库返回的指针 if (ocr_handle) { printf("OCR引擎初始化成功。\n"); return 0; // 成功 } else { fprintf(stderr, "OCR引擎初始化失败。\n"); return -1; // 失败 } } OcrResult recognize_image(const ImageData *img) { OcrResult result = {NULL, 0, 0.0f}; if (!ocr_handle || !img || !img->data) { fprintf(stderr, "错误:OCR引擎未初始化或图像数据无效。\n"); return result; } // 调用GLM-OCR识别函数 // 假设接口函数为:glm_ocr_recognize(handle, image_data, width, height, channels, ...) // 例如: // char** lines = NULL; // int line_count = 0; // float conf = glm_ocr_recognize(ocr_handle, img->data, img->width, img->height, img->channels, &lines, &line_count); printf("正在识别图像 (%d x %d)...\n", img->width, img->height); // --- 模拟识别过程(实际项目中替换为真实调用)--- // 这里我们模拟返回一些结果,用于演示流程 result.line_count = 2; result.confidence = 0.92f; result.text_lines = (char**)malloc(result.line_count * sizeof(char*)); if (result.text_lines) { result.text_lines[0] = strdup("这是第一行识别文本。"); result.text_lines[1] = strdup("这是第二行识别文本。"); } // --- 模拟结束 --- if (result.line_count > 0) { printf("识别完成,找到 %d 行文本。\n", result.line_count); } else { printf("未识别到文本。\n"); } return result; } void free_ocr_result(OcrResult *result) { if (result && result->text_lines) { for (int i = 0; i < result->line_count; i++) { if (result->text_lines[i]) { free(result->text_lines[i]); } } free(result->text_lines); result->text_lines = NULL; result->line_count = 0; result->confidence = 0.0f; } } // 构建单个图片识别结果的JSON对象 cJSON* build_json_result(const char *image_path, const OcrResult *result) { cJSON *img_json = cJSON_CreateObject(); if (!img_json) return NULL; cJSON_AddStringToObject(img_json, "file_path", image_path); cJSON_AddNumberToObject(img_json, "line_count", result->line_count); cJSON_AddNumberToObject(img_json, "confidence", (double)result->confidence); // 添加文本行数组 cJSON *lines_array = cJSON_CreateArray(); if (lines_array) { for (int i = 0; i < result->line_count; i++) { cJSON *line_item = cJSON_CreateString(result->text_lines[i]); cJSON_AddItemToArray(lines_array, line_item); } cJSON_AddItemToObject(img_json, "text_lines", lines_array); } return img_json; }这个模块里,init_ocr_engine、recognize_image这两个函数需要你根据GLM-OCR库提供的实际C语言API文档进行修改。我这里用注释和模拟数据展示了调用逻辑。重点是build_json_result函数,它使用cJSON库将识别结果构建成一个结构化的JSON对象,这是后续输出到文件的基础。
3. 主程序串联与进阶处理
现在,我们把所有模块像拼图一样组合起来,并在主程序中处理一些进阶问题。
3.1 整合主程序逻辑
回到main.c,我们把之前的TODO填上。
// src/main.c (完整版) #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "file_utils.h" #include "image_loader.h" #include "ocr_engine.h" // 简单的命令行参数解析 void print_usage(const char *program_name) { printf("用法: %s <输入图片目录> [输出JSON文件路径]\n", program_name); printf("示例: %s ./input_images ./output/results.json\n", program_name); } int main(int argc, char *argv[]) { // 1. 解析参数 if (argc < 2) { print_usage(argv[0]); return 1; } const char *input_dir = argv[1]; const char *output_file = (argc >= 3) ? argv[2] : "./output/ocr_results.json"; printf("GLM-OCR 批量处理工具启动...\n"); printf("输入目录: %s\n", input_dir); printf("输出文件: %s\n", output_file); // 2. 初始化OCR引擎 (假设模型在当前目录的`models`文件夹) if (init_ocr_engine("./models/glm_ocr_model.bin") != 0) { fprintf(stderr, "初始化OCR引擎失败,程序退出。\n"); return 1; } // 3. 查找图片文件 printf("\n正在扫描目录: %s\n", input_dir); ImageFileNode *file_list = find_image_files(input_dir); if (!file_list) { fprintf(stderr, "未找到图片文件或目录错误。\n"); // 注意:这里也应该清理OCR引擎资源 return 1; } print_file_list(file_list); // 4. 创建总的JSON结果对象 cJSON *root_json = cJSON_CreateObject(); cJSON_AddStringToObject(root_json, "project", "GLM-OCR Batch Processing"); cJSON_AddStringToObject(root_json, "input_dir", input_dir); cJSON_AddStringToObject(root_json, "output_file", output_file); cJSON_AddStringToObject(root_json, "timestamp", "2023-10-27T10:30:00Z"); // 应使用time库生成 cJSON *images_array = cJSON_CreateArray(); cJSON_AddItemToObject(root_json, "images", images_array); // 5. 遍历并处理每个图片文件 printf("\n开始批量识别处理...\n"); clock_t start_time = clock(); ImageFileNode *current = file_list; int processed_count = 0; int success_count = 0; while (current) { processed_count++; printf("\n[%d/%d] 处理: %s\n", processed_count, 0, current->file_path); // 总文件数需要先统计 // 5.1 加载图片 ImageData img = load_image(current->file_path); if (!img.data) { fprintf(stderr, " 跳过,图片加载失败。\n"); current = current->next; continue; } // 5.2 调用OCR识别 OcrResult ocr_result = recognize_image(&img); // 5.3 构建该图片的JSON结果并添加到数组 cJSON *img_result_json = build_json_result(current->file_path, &ocr_result); if (img_result_json) { cJSON_AddItemToArray(images_array, img_result_json); success_count++; } // 5.4 释放本张图片占用的资源 free_ocr_result(&ocr_result); free_image(&img); printf(" 处理完成。\n"); current = current->next; } clock_t end_time = clock(); double total_time = (double)(end_time - start_time) / CLOCKS_PER_SEC; // 6. 统计信息 cJSON_AddNumberToObject(root_json, "total_files", processed_count); cJSON_AddNumberToObject(root_json, "successful_files", success_count); cJSON_AddNumberToObject(root_json, "processing_time_seconds", total_time); // 7. 将JSON对象写入文件 printf("\n正在将结果写入文件: %s\n", output_file); char *json_str = cJSON_Print(root_json); // 格式化的JSON字符串 if (json_str) { // 确保输出目录存在(简单示例,生产环境需更健壮) FILE *fp = fopen(output_file, "w"); if (fp) { fputs(json_str, fp); fclose(fp); printf("结果已成功保存。\n"); } else { perror("无法打开输出文件"); } free(json_str); } else { fprintf(stderr, "生成JSON字符串失败。\n"); } // 8. 清理所有资源 cJSON_Delete(root_json); free_file_list(file_list); // 这里应调用GLM-OCR的清理函数,例如:glm_ocr_free(ocr_handle); printf("\n======= 处理报告 =======\n"); printf("总处理文件: %d\n", processed_count); printf("成功识别文件: %d\n", success_count); printf("总耗时: %.2f 秒\n", total_time); if (processed_count > 0) { printf("平均每张图片: %.2f 秒\n", total_time / processed_count); } printf("=========================\n"); return 0; }3.2 编译与运行
我们需要一个Makefile来编译这个项目,因为它链接了多个源文件和外部库。
# Makefile CC = gcc CFLAGS = -Wall -Wextra -O2 -g -I./libs LDFLAGS = -L. -lm -lpthread # 假设GLM-OCR动态库名为libglm_ocr.so LDLIBS = -lglm_ocr TARGET = glm_ocr_batch SRCS = src/main.c src/file_utils.c src/image_loader.c src/ocr_engine.c libs/cJSON.c OBJS = $(SRCS:.c=.o) all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(TARGET) $(OBJS) ./output/*.json run: $(TARGET) ./$(TARGET) ./input_images ./output/results.json .PHONY: all clean run在终端中,进入项目目录,执行以下命令:
# 1. 编译程序 make # 2. 将GLM-OCR的动态库文件(如libglm_ocr.so)放到项目根目录或系统库路径 # 3. 在input_images目录下放入一些测试图片(jpg/png等) # 4. 运行程序 make run # 或者直接运行 # ./glm_ocr_batch ./input_images ./output/my_results.json如果一切顺利,你会在output目录下看到一个results.json文件,里面包含了所有图片的识别结果。
3.3 进阶话题:错误处理与内存泄漏排查
写C程序,这两点至关重要。
错误处理:上面的代码已经加入了一些基本的错误检查(如文件打开失败、内存分配失败)。在生产环境中,你需要更细致地处理每一种可能的错误,并给出清晰的日志信息,方便定位问题。
内存泄漏排查:C语言需要手动管理内存。我们的代码中,所有通过malloc、strdup、cJSON_Create*分配的内存,都必须有对应的free或cJSON_Delete。可以使用工具来辅助检查:
- Valgrind(Linux/macOS):
valgrind --leak-check=full ./glm_ocr_batch ./input_images - AddressSanitizer(GCC/Clang): 在编译时加上
-fsanitize=address标志。
运行这些工具,确保程序结束时没有“definitely lost”的内存块。
3.4 性能基准测试
想知道我们的程序处理速度如何?可以写个简单的测试脚本,或者直接在代码里加入更精细的计时。
// 在main.c的循环处理部分,可以针对每张图片计时 #include <time.h> // ... while (current) { clock_t img_start = clock(); // ... 处理图片的代码 ... clock_t img_end = clock(); double img_time = (double)(img_end - img_start) / CLOCKS_PER_SEC; printf(" 单张图片处理耗时: %.3f 秒\n", img_time); // 可以将img_time也记录到该图片的JSON结果中 // ... }通过分析单张图片的处理时间,你可以判断瓶颈是在图片加载、OCR识别还是文件I/O上,从而进行有针对性的优化。
4. 总结与展望
走完这一趟,你应该已经拥有了一个功能完整的C语言批量图片OCR处理工具。从遍历文件夹、加载图片,到调用底层AI接口、结构化输出结果,我们覆盖了一个典型离线批处理任务的核心流程。
过程中,我们刻意强调了C语言编程中那些“琐碎”但关键的部分:手动管理内存、细致的错误检查、跨平台的文件操作考量。这些正是从“写个小demo”到“开发可靠工具”必须跨越的坎儿。
这个程序本身还有很多可以打磨的地方。比如,你可以加入多线程来并行处理图片,大幅提升吞吐量;可以设计更灵活的配置文件,来指定模型路径、支持的文件格式、输出格式等;还可以将结果直接导入数据库,或者增加一个简单的进度条来提升用户体验。
最重要的是,你现在有了一个清晰、模块化的代码框架。无论是想接入另一个OCR引擎,还是想把输出格式从JSON改成XML,或者增加图片预处理步骤,你都知道该去修改哪个文件里的哪个函数了。这就是结构化编程带来的好处。
希望这个实践能帮你更深入地理解如何用C语言驾驭AI模型,完成实实在在的工程任务。代码的世界,动手搭一遍,比看十遍都管用。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
