HarmonyOS APP《画伴梦工厂》开发第28篇:图像识别——儿童绘画内容理解
第3.5篇 图像识别——儿童绘画内容理解系列HarmonyOS 从入门到实践 · 画伴梦工厂实战难度⭐⭐⭐ 高级前置知识3.1 HTTP 网络请求、3.4 图片压缩与 Base64 编解码涉及源文件products/default/src/main/ets/services/ImageRecognitionService.ets在画伴梦工厂中识别一幅儿童手绘的内容画了什么角色、在哪里、什么情绪是生成动画的关键前提。传统图像标签服务只能输出恐龙、草地这样的通用标签无法提取角色身份、场景氛围、情绪色彩这些对动画生成至关重要的结构化信息。这里我们使用市场上常见的多模态模型支持同时接收文本和图像输入在图像识别任务上表现出色且推理成本极低。本文将详细拆解如何通过它实现专为儿童绘画设计的智能识别管线。一、多模态模型考量维度多模态模型传统图像分类 API输入形式文本 图像多模态仅图像结构化输出JSON 格式自由定义字段固定标签 置信度推理能力理解绘画主题、情绪、建议仅识别物体名称每千张成本~$0.15通常 $1 ~ $3核心优势在于多模态模型 不仅能识别这是一只恐龙还能理解这是一只快乐的小恐龙在草地上晒太阳适合做跳跃和摇尾巴的动画。这种开放域理解能力正是儿童画创作场景所需要的。二、数据模型以类型安全定义识别结果首先定义识别结果的结构。这是整个服务的契约决定了 AI 返回什么、页面消费什么exportinterfaceRecognizedElement{name:string;// 元素名称如小恐龙confidence:number;// 置信度 0-100color:string;// UI 展示用的主题色}exportinterfaceDrawingRecognitionResult{protagonist:string;// 主角描述scene:string;// 场景描述emotion:string;// 情绪描述animationSuggestion:string;// 动画动作建议summary:string;// 综合摘要elements:RecognizedElement[];// 识别出的元素列表}DrawingRecognitionResult是面向动画生成流程设计的——protagonist 决定动画主体scene 决定背景风格emotion 决定色调和配乐倾向animationSuggestion 直接输入到后续的 3.3 图生视频服务作为 prompt 增强。三、多模态请求体构建3.1 OpenAI 兼容格式多模态请求遵循 OpenAI Chat Completions 格式核心是允许content字段为数组同时包含文本片段和图片片段interfaceModelContentPart{type:string;// text 或 image_urltext?:string;// type 为 text 时使用image_url?:ModelImageUrl;// type 为 image_url 时使用}interfaceModelMessage{role:string;// system 或 usercontent:string|ModelContentPart[];// 字符串或多模态数组}interfaceModelRequestBody{model:string;temperature:number;messages:ModelMessage[];}3.2 buildRequestBody组装结构化 PromptbuildRequestBody方法负责将 Base64 图片和结构化 Prompt 组装成请求体privatestaticbuildRequestBody(imageBase64:string,mimeType:string):ModelRequestBody{constschemaPrompt:string请识别这张儿童手绘图片中的主要角色、场景、情绪和适合生成动画的动作建议。只返回 JSON不要 Markdown。字段必须为protagonist、scene、emotion、animationSuggestion、summary、elements。elements 是数组最多 6 项每项包含 name 和 0-100 的 confidence。中文作答。;constsystemMessage:ModelMessage{role:system,content:你是儿童绘画智能识别助手结果要适合在儿童创作应用里展示表达温和、简洁。};consttextPart:ModelContentPart{type:text,text:schemaPrompt};constimageUrl:ModelImageUrl{url:data:mimeType;base64,imageBase64};constimagePart:ModelContentPart{type:image_url,image_url:imageUrl};constuserMessage:ModelMessage{role:user,content:[textPart,imagePart]};return{model:MODEL_NAME,// 多模态模型temperature:0.2,// 低温度确保输出稳定messages:[systemMessage,userMessage]};}3.3 Prompt 工程设计要点Prompt 是整个识别服务的灵魂。设计时有几个关键决策① system message 设定角色你是儿童绘画智能识别助手结果要适合在儿童创作应用里展示表达温和、简洁。这告诉模型用儿童友好的语气输出不要用冷冰冰的技术术语。② 明确的 JSON Schema 约束只返回 JSON不要 Markdown。字段必须为protagonist、scene、emotion……直接指定字段名和类型避免模型输出多余的 Markdown 包裹或无关字段。③ 置信度范围限定每项包含 name 和 0-100 的 confidence明确数值范围降低模型输出异常值的概率。④ temperature 0.2低温度值让模型输出更确定、更可重复适合结构化提取任务。如果是创意生成任务如文案创作通常会设到 0.7~0.9。四、图片预处理压缩与 Base64在 3.4 图片压缩篇中已经详细介绍了压缩技术这里重点关注几个与识别服务直接相关的常量constMAX_IMAGE_BYTES:number12*1024*1024;// 最大原始大小 12MBconstRECOGNITION_IMAGE_TARGET_BYTES:number520*1024;// 压缩目标 520KBconstRECOGNITION_IMAGE_MAX_EDGE:number1024;// 最大边长 1024pxprepareRecognitionImage方法是压缩链路的入口privatestaticasyncprepareRecognitionImage(imageUri:string):PromisePreparedImagePayload{constsourceBufferImageRecognitionService.readImageAsArrayBuffer(imageUri);try{constcompressedBufferawaitImageRecognitionService.compressImageBuffer(sourceBuffer);return{base64:ImageRecognitionService.arrayBufferToBase64(compressedBuffer),mimeType:image/jpeg};}catch(error){// 压缩失败时回退到原图return{base64:ImageRecognitionService.arrayBufferToBase64(sourceBuffer),mimeType:ImageRecognitionService.getMimeType(imageUri)};}}注意这里的兜底策略如果图片压缩失败比如图片太小无需压缩不会让整个识别流程崩溃而是直接使用原图。这个模式在后续的降级处理中还会反复出现。五、网络请求与异常处理recognizeDrawing是服务的核心入口方法staticasyncrecognizeDrawing(imageUri:string):PromiseDrawingRecognitionResult{if(imageUri){returnImageRecognitionService.getFallbackResult();}// Step 1: 准备图片压缩 Base64letpayload:PreparedImagePayload{base64:,mimeType:image/jpeg};try{payloadawaitImageRecognitionService.prepareRecognitionImage(imageUri);}catch(error){thrownewError(图片读取失败ImageRecognitionService.getErrorMessage(errorasError));}// Step 2: 创建 HTTP 请求constrequesthttp.createHttp();try{constheaders:ModelHeaders{Content-Type:application/json,Authorization:Bearer MODEL_API_KEY};constoptions:http.HttpRequestOptions{method:http.RequestMethod.POST,expectDataType:http.HttpDataType.STRING,connectTimeout:30000,readTimeout:60000,header:headers,extraData:JSON.stringify(ImageRecognitionService.buildRequestBody(payload.base64,payload.mimeType))};constresponseawaitrequest.request(MODEL_API_URL,options);constresponseTextresponse.result.toString();if(response.responseCode200||response.responseCode300){thrownewError(模型接口返回异常response.responseCode.toString());}constresponseBodyJSON.parse(responseText)asModelResponseBody;if(responseBody.errorresponseBody.error.message){thrownewError(responseBody.error.message);}// Step 3: 解析并规范化结果returnImageRecognitionService.normalizeModelResult(responseBody.choices[0].message.content);}finally{request.destroy();}}三重异常防护层级异常类型处理方式图片读取文件不存在、权限不足、大小超限抛出明确错误信息HTTP 请求网络超时、非 2xx 状态码抛出带状态码的错误响应解析JSON 格式错误、字段缺失抛出解析失败信息每一层都确保错误信息足够具体方便上层调用方决定是提示用户重试、降级展示默认结果还是跳转到备用流程。六、结果规范化与降级处理这是整个服务中最见工程功底的部分——AI 模型返回的内容永远不可信必须做严格的校验和兜底。6.1 stripJsonFence清理 Markdown 包裹虽然 Prompt 要求只返回 JSON不要 Markdown但模型有时还是会输出代码块。stripJsonFence负责清理privatestaticstripJsonFence(content:string):string{consttrimmedcontent.trim();if(!trimmed.startsWith()){returntrimmed;}constfirstLineEndtrimmed.indexOf(\n);constlastFenceStarttrimmed.lastIndexOf();if(firstLineEnd0lastFenceStartfirstLineEnd){returntrimmed.substring(firstLineEnd1,lastFenceStart).trim();}returntrimmed;}逻辑很简单如果字符串以开头找到第一个换行去掉第一行的语言标记和最后一个提取中间内容。这段代码处理了类似这样的输入json {protagonist: 小恐龙, ...} 6.2 clampConfidence置信度边界限定privatestaticclampConfidence(value:number|undefined):number{if(valueundefined||Number.isNaN(value)){return86;// 默认置信度}returnMath.max(0,Math.min(100,Math.round(value)));}既防止了模型返回负值或超过 100 的异常值也为缺失值提供了合理的默认值86。6.3 pickText字段级兜底privatestaticpickText(value:string|undefined,fallback:string):string{if(valuevalue.trim()!){returnvalue.trim();}returnfallback;}对每个文本字段独立做空值检查。如果 AI 没有返回某个字段比如emotion缺失就用默认值替代而不是显示空字符串。6.4 normalizeModelResult全量规范化privatestaticnormalizeModelResult(content:string):DrawingRecognitionResult{constjsonTextImageRecognitionService.stripJsonFence(content);constparsedJSON.parse(jsonText)asModelResultBody;constfallbackImageRecognitionService.getFallbackResult();constelements:RecognizedElement[][];if(parsed.elements){for(leti0;iparsed.elements.lengthi6;i){constitemparsed.elements[i];if(!item.name||item.name.trim()){continue;// 跳过无名称的元素}elements.push({name:item.name,confidence:ImageRecognitionService.clampConfidence(item.confidence),color:ELEMENT_COLORS[i%ELEMENT_COLORS.length]// 轮换颜色});}}return{protagonist:ImageRecognitionService.pickText(parsed.protagonist,fallback.protagonist),scene:ImageRecognitionService.pickText(parsed.scene,fallback.scene),emotion:ImageRecognitionService.pickText(parsed.emotion,fallback.emotion),animationSuggestion:ImageRecognitionService.pickText(parsed.animationSuggestion,fallback.animationSuggestion),summary:ImageRecognitionService.pickText(parsed.summary,fallback.summary),elements:elements.length0?elements:fallback.elements};}关键设计原则每字段独立兜底不因为一个字段异常就丢弃全部结果元素白名单跳过name为空的元素项而不是直接报错颜色轮换ELEMENT_COLORS提供 6 种预设色确保 UI 展示有视觉区分度整体降级如果elements数组最终为空使用默认元素列表6.5 getFallbackResult有温度的默认值constDEFAULT_RESULT:DrawingRecognitionResult{protagonist:小恐龙,scene:草地和太阳,emotion:快乐探险,animationSuggestion:跳跃、摇尾、看向太阳,summary:识别到儿童手绘中的主角、自然场景和明亮情绪适合生成轻快的探险动画草稿。,elements:[{name:小恐龙,confidence:96,color:#D8F7EA},{name:太阳,confidence:91,color:#FFF0DD},{name:草地,confidence:88,color:#EAF8F0},{name:树木,confidence:84,color:#F1EDFF}]};默认值不是随便填的。它们构成了一个完整的、可用的动画场景——一只小恐龙在草地上晒太阳。即使用户的网络完全不可用或者 AI 服务宕机应用也可以直接用这些默认值启动动画生成流程用户甚至可能察觉不到异常。七、完整调用链路7.1 业务层调用在PhotoRecognitionComponent中识别服务被调用的典型场景是用户拍照/选择图片后点击生成动画用户拍照/选图 │ ▼ capturedImageUri 保存成功 │ ▼ generateFromPhoto() │ ▼ navigateToGeneration() │ ▼ RecognitionWaitingPage等待页 │ ├── startGeneration() → AIGenerationService.generateVideo() │ 内部可先调用 ImageRecognitionService.recognizeDrawing │ └── 完成后 pushUrl → RecognitionResultPage7.2 结果展示在RecognitionResultPage.ets中识别结果通过路由参数传递并展示// aboutToAppear 中解析识别结果if(paramsparams.recognitionResult){try{this.recognitionResultJSON.parse(params.recognitionResult)asDrawingRecognitionResult;}catch(error){this.recognitionResultImageRecognitionService.getFallbackResult();}}在 UI 层通过buildResultItems方法将结构化数据转换为展示项privatebuildResultItems():ResultItem[]{constfirstConfidencethis.recognitionResult.elements.length0?this.recognitionResult.elements[0].confidence:96;constsecondConfidencethis.recognitionResult.elements.length1?this.recognitionResult.elements[1].confidence:91;// ...return[{name:主角this.recognitionResult.protagonist,confidence:firstConfidence},{name:场景this.recognitionResult.scene,confidence:secondConfidence},{name:情绪this.recognitionResult.emotion,confidence:thirdConfidence},{name:动画建议this.recognitionResult.animationSuggestion,confidence:fourthConfidence}];}每个识别维度都展示为一个卡片行包含名称和置信度百分比用品牌紫色突出显示。ResultPage 根据videoUri是否为空决定展示视频播放视图还是识别结果视图。┌──────────────────────────────┐ │ 识别结果 │ │ 画作识别已完成 │ ├──────────────────────────────┤ │ │ │ [ 画作预览图 ] │ │ │ │ ┌────────────────────────┐ │ │ │ 动画草稿已准备 │ │ │ │ 识别到儿童手绘中的... │ │ │ ├────────────────────────┤ │ │ │ 主角小恐龙 96% │ │ │ │ 场景草地和太阳 91% │ │ │ │ 情绪快乐探险 88% │ │ │ │ 动画建议跳跃... 86% │ │ │ └────────────────────────┘ │ │ │ │ [ 查看动画视频 ] │ └──────────────────────────────┘八、服务层架构设计要点ImageRecognitionService采用纯静态方法设计模式所有方法都是static这么做有几个好处特性说明无需实例化直接ImageRecognitionService.recognizeDrawing()调用无状态每次调用独立天然线程安全易于测试可以 mock HTTP 层单独测试解析逻辑轻量不依赖 Context不与组件生命周期耦合接口模型ModelRequestBody、ModelResponseBody等全部声明为私有interface对外只暴露DrawingRecognitionResult和RecognizedElement。这遵循了信息隐藏原则——调用方不需要知道 GPT-4o-mini 的请求格式细节。九、与 3.4 图片压缩的协作关系本文假设读者已经掌握了 3.4 篇的内容。这里梳理一下两个服务之间的协作点ImageRecognitionService.recognizeDrawing(imageUri) │ ├── readImageAsArrayBuffer() ← fileIo 读取文件 │ ├── compressImageBuffer() ← image.Packer 压缩520KB 目标 │ │ image.PixelMap 缩放1024px 最大边长 │ │ JPEG quality 68 / 52 两级策略 │ ├── arrayBufferToBase64() ← util.Base64Helper 编码 │ ├── buildRequestBody() ← 组装多模态请求 │ ├── http POST → GPT-4o-mini API ← kit.NetworkKit 网络请求 │ └── normalizeModelResult() ← 解析、校验、降级图片压缩将原始图片可能 10MB压缩到约 520KB 的 JPEG既满足 GPT-4o-mini 的输入限制又大幅缩短传输时间。Base64 编码将二进制图片转为字符串嵌入 JSON 请求体中。十、异常场景覆盖检查场景触发条件行为空 URIimageUri 直接返回 fallback 结果图片读取失败fileIo 异常抛出图片读取失败图片超 12MBstat.size 超限抛出明确提示压缩失败image.Packer 异常使用未压缩原图HTTP 超时30s 连接 / 60s 读取抛网络异常非 2xx 响应responseCode 异常抛出状态码 响应片段AI 返回错误responseBody.error抛出 error.messageJSON 解析失败JSON.parse 异常抛出不是约定 JSON字段缺失protagonist 等为空pickText 使用默认值elements 为空无有效元素使用默认元素列表这张表展示了工业级 AI 集成服务应有的防御深度。上线前可以对每个场景编写测试用例确保降级行为符合预期。总结GPT-4o-mini 图像识别是画伴梦工厂AI 服务链中承上启下的一环。本文从多模态请求构建、Prompt 工程设计、结果规范化到降级策略完整拆解了这个服务知识点实现多模态请求textimage_url双 part 结构含 Base64 图片Prompt 工程system/user 双消息显式 JSON Schema 约束结果解析stripJsonFence → JSON.parse → normalizeModelResult字段级兜底pickText / clampConfidence 独立处理每个字段整体降级getFallbackResult 返回有意义的默认场景防御式编程图片读取失败 → 原图回退AI 异常 → 默认结果下一篇预告第 3.6 篇 JSON 序列化与反序列化——我们将深入对比 AIGenerationService 与 ImageRecognitionService 中不同的 JSON 处理策略学习如何定义健壮的 interface 数据模型。参考源码本文所有代码均来自项目文件products/default/src/main/ets/services/ImageRecognitionService.ets— 完整的 GPT-4o-mini 图像识别服务实现products/default/src/main/ets/pages/RecognitionResultPage.ets— 识别结果展示页面products/default/src/main/ets/components/CreationComponents.ets— PhotoRecognitionComponent 中触发识别的业务逻辑