鸿蒙+Flutter跨平台开发:用图像分割技术做一款口红试色APP的完整实践
做跨平台开发这些年,我最大的感受是:真正的痛点从来不是“能不能跑”,而是“跑起来之后体验到底行不行”。尤其是当目标平台变成鸿蒙的时候,情况就更微妙了。鸿蒙生态刚猛起步,Flutter官方对HarmonyOS NEXT的支持还在快速迭代中,很多社区方案带着“能编译过就算成功”的侥幸心态。但如果你要做的是一款口红试色APP——一个非常吃图像处理、吃实时渲染、吃交互帧率的应用——那就不能只停留在“能用”的层面。
这篇文章是我自己从零搭建一款基于鸿蒙+Flutter的口红试色APP的全过程记录。项目核心是:用Flutter承载业务和UI,通过鸿蒙原生层接入图像分割能力,实现摄像头或相册照片中的嘴唇区域提取,再叠加不同口红色号的数字渲染效果,最终在HarmonyOS NEXT设备上以接近原生的流畅度跑起来。无论你是想入门鸿蒙跨平台开发,还是想尝试图像分割在移动端的落地,这篇内容都可以直接作为参考。
1. 整体设计与技术选型思路
1.1 为什么是鸿蒙+Flutter,而不是纯鸿蒙开发
先说结论:鸿蒙原生开发(ArkTS+ArkUI)完全能写出口红试色这种应用,但如果你和我一样,手里已经有一套Flutter业务代码,或者团队主力技能在Dart侧,那采用Flutter做跨平台壳、鸿蒙做能力底座,是现阶段性价比最高的方案。
鸿蒙NEXT从2024年开始逐步去掉了AOSP兼容层,这套系统已经是微内核加自研鸿蒙内核的独立生态了。很多做Flutter开发的朋友第一反应是“那Flutter还能跑吗?”。答案是能,而且官方和社区的适配速度比想象中快。目前Flutter SDK已经针对鸿蒙出了独立的分支版本,通过OpenHarmony的ACE容器集成Flutter引擎,把Dart代码跑在鸿蒙应用里,同时保留鸿蒙原生的平台通道。
这种混合方案的好处有三层:
第一层,UI层跨平台复用。我用Flutter写好口红试色页、色号列表、试色结果展示这些交互界面,将来如果要出Android或者iOS版本,UI代码能直接平移过去,不必重写。
第二层,底层能力走鸿蒙原生。图像分割模型推理、相机数据采集、相册选择这些和系统强相关的模块,通过鸿蒙原生能力实现,再由Flutter的MethodChannel或EventChannel接入。这样既保证了性能,又不需要在Dart层硬造轮子。
第三层,团队技术栈平滑迁移。Flutter工程师可以专注于业务,鸿蒙工程师专注于平台层,两边通过定义好的协议对接,合作边界清晰。
当然,纯鸿蒙方案也有它的优势,比如系统能力调用更直接、应用体积更小。但跨平台永远是一个“取最大公约数”的游戏,对于需要同时覆盖多端的小团队来说,Flutter那套“一次编写,多端运行”的效率优势还是很难放弃的。
1.2 图像分割技术选型:为什么放弃云端API
口红试色的核心环节是找到嘴唇区域。这里有两个技术路线:一是云端API,把用户相册照片上传到服务器,用服务端的语义分割模型做完推理再返回结果;二是端侧推理,在手机上直接跑一个轻量级分割模型。
我一开始真考虑过云端方案,毕竟服务端模型精度高,调起来也方便。但仔细想过之后放弃了这个思路,原因很现实:
延迟问题。口红试色讲究实时反馈,用户对着摄像头抿嘴的时候,如果每一帧都要上传、推理、返回,即使网络很好也要几百毫秒的延迟,体验会非常拖沓。想要流畅得像镜子一样,就必须端侧实时处理。
隐私问题。用户的脸部照片属于高度敏感信息。让用户把自己的素颜照传到云端,对很多用户来说心理门槛太高。在本地完成全部处理,既能保证隐私,又省去合规方面的很多麻烦。
离线可用。试色场景经常发生在逛街、约会现场,网络环境不可控。端侧推理完全离线运行,这一点对用户体验的保障是决定性的。
端侧方案听起来美好,限制也不少,最突出的就是模型体积和推理速度。我选用的是基于PaddleSeg套件蒸馏出来的轻量级人像分割模型,输入分辨率设为256x256,模型体积压到了5MB以内。这个模型原本是做人像分割的,人像里的嘴唇区域虽然只是一个小局部,但通过针对性微调,最终在嘴唇分割任务上也能达到不错的IoU(交并比)。如果你的场景只拍嘴巴特写,也可以直接选用专门训练的口唇分割模型,精度会更理想。
1.3 试色渲染方案:混合模式与色号映射
分割模型输出的是一个单通道的Mask图,每个像素的值在0到1之间,表示该像素属于嘴唇区域的概率。拿到这张灰度图之后,怎么把口红颜色“涂”上去,这是口红试色体验的灵魂所在。
业界最常用的做法是Alpha Blending,也就是把口红色号作为前景色,用Mask作为Alpha通道,和原图做混合。
但这里有个细节:如果直接拿Mask当透明度用,出来的效果会像贴了一层半透明色片,边缘假、光泽丢失、唇纹也不明显,看起来非常“塑料”。
我采用的是“正片叠底+颜色叠加”的组合渲染策略。思路是:先拿一个较深的色号版本和原图做正片叠底(Multiply),模拟口红附着在唇部后对原有唇色的压暗效果;再拿原始色号做一次线性减淡(Screen或Linear Dodge),恢复亮部和镜面光泽。两步结果按Mask加权合成,出来的试色效果就有了一些真实的体积感,而不是扁平的色块。
色号映射方面,每种口红颜色在程序里表示为一个RGB三元组。但就算是同一个颜色,在不同肤色、不同原始唇色上呈现的结果也完全不同。这就需要在渲染时对原始唇色做一定的提取和校正。我的做法是:取Mask区域内原始像素的平均色,和标准唇色做一个偏移计算。偏移量决定了口红色号叠加时的基准强弱,最终效果会更接近“涂在你自己嘴上”的真实感。
2. 鸿蒙端环境搭建与Flutter适配
2.1 开发环境准备:SDK版本与工具链
鸿蒙+Flutter这套组合最让人头疼的就是环境配置,版本不匹配会带来一堆匪夷所思的编译错误。我花了一整天反复试验,最终调通下来的组合是:
DevEco Studio 5.0.0及以上版本。HarmonyOS NEXT的应用开发IDE,负责创建鸿蒙工程骨架、配置系统权限、打包生成HAP。
Flutter鸿蒙版SDK。这里注意,不是官网那个标准Flutter SDK,而是社区维护的ohos分支,目前OpenHarmony SIG组在积极推进适配。用flutter create --platforms ohos创建的工程会同时生成android、ios和ohos三个平台目录。
配置过程并不复杂,但在两个地方特别容易踩坑。
第一个坑是环境变量。鸿蒙版的Flutter SDK会有独立的FLUTTER_STORAGE_BASE_URL配置需求,因为部分依赖包托管在镜像仓库。如果不配或者配错,执行flutter pub get时会卡在下载依赖这一步,报各种奇怪的网络错误。
第二个坑是签名配置。鸿蒙应用构建HAP包时必须在build-profile.json5里配置签名信息。调试阶段可以勾选DevEco Studio的Automatically generate signature,它会自动生成调试证书。但如果你要用Flutter命令行直接构建,就得手动指定签名文件路径,否则编译能过,装不上设备。
2.2 Flutter引擎在鸿蒙上的启动机制
在鸿蒙上跑Flutter,和Android/iOS有一个本质区别:鸿蒙不是通过原生View直接嵌入FlutterViewController的。鸿蒙的ACE容器框架提供了XComponent组件作为原生UI的承载容器,Flutter引擎的渲染输出最终会被推送到这个XComponent上。
这个机制带来的直接影响是:你在鸿蒙原生侧写页面时,可以创建任意多个FlutterView,每个FlutterView内部持有一个FlutterEngine实例。对于口红试色这种相对简单的单页面应用,一个FlutterEngine就够了,但如果你的应用有多个独立页面,每个页面都需要保持Dart侧状态,那就要考虑多个Engine的复用策略,否则内存开销会非常恐怖。
引擎启动的大致流程是:
鸿蒙的EntryAbility在onWindowStageCreate回调里创建一个FlutterView,调用其attachToFlutterEngine方法,传入预先把Dart entrypoint加载好的FlutterEngine。如果希望加快启动速度,可以在应用Application初始化阶段就预热引擎,把MainActivity里最耗时的Dart isolate初始化提前完成。
实测下来,预热引擎能让冷启动时间减少30%左右。这个优化对用户体感非常明显,因为口红试色是一个典型的“打开就想立刻用”的工具型应用场景。
3. 图像分割模型的接入与推理实现
3.1 模型格式转换与工程集成
我训练的PaddleSeg模型是.pdmodel和.pdiparams两件套,要在鸿蒙侧做推理,得先转成ONNX格式,再转成鸿蒙MindSpore Lite支持的.ms格式。整个转换链路其实挺顺畅的:
第一步,Paddle转ONNX。用paddle2onnx工具一行命令搞定,输入是训练好的模型,输出一个model.onnx文件。转换时需要注意opset版本,MindSpore Lite对太新的opset支持可能不全,我最后锁定了opset 11。
第二步,ONNX转MindSpore Lite。用华为官方的converter_lite工具,在命令行里指定输入模型路径和输出格式,转换时有个很重要的小技能:多次调用converter_lite之前,先检查模型里是否有自定义算子。我的模型里有一个很冷门的Flatten实现方式,直接转换会报错,最后手动给ONNX图加了一个Reshape节点才绕过去。
第三步,把.ms模型文件放到鸿蒙工程的resources/rawfile目录下,工程构建时它会被自动打进HAP包里。运行时通过ResourceManager读取模型内容,再加载给MindSpore Lite执行。
这一套流程看着简单,实际上每一层都有版本坑。我的建议是:转换工具链的版本尽量都选2024年以后的新版本,尤其是MindSpore Lite,新版本对动态shape的支持和算子覆盖率都提升了很多。
3.2 图像预处理与后处理
模型推理前,图像要经过一个标准化的预处理流程。
我采集到的图像可能来自摄像头,也可能来自相册,尺寸五花八门。第一步是等比缩放,让最长边对齐到256像素。注意必须等比缩放,如果直接拉伸成正方形,人的脸会被拉变形,后面分割出来的嘴唇位置全乱套。等比缩放之后,剩余部分用边缘像素填充,凑成256x256的输入张量。第二步是归一化,按PaddleSeg模型训练时的均值和标准差做减除操作。第三步是通道重排,把HWC的内存布局转成CHW,这一步在Dart层做非常麻烦,我最后把数据直接传给了鸿蒙native层,用C++完成。
后处理相对简单:模型输出的张量是1x2x256x256,取其中嘴唇类别那一通道,做一次softmax得到每个像素属于嘴唇的概率。然后缩放到原图尺寸,做一个5x5的高斯模糊去掉锯齿边缘。这个模糊操作特别关键,直接决定最终试色看起来是“融进嘴唇”还是“贴了张纸”。
3.3 推理性能调优实测
华为的设备上,MindSpore Lite的性能整体是不错的,但想让帧率达到30fps以上,还是要做几件小事。
第一件,线程数设成4。MindSpore Lite的推理配置里有个numThreads参数,默认是2,在性能模式下改成4,推理耗时能下降差不多一半。改成8不一定更快,反而可能因为线程调度开销得不偿失。
第二件,开启FP16混合精度。如果你的目标设备芯片支持FP16,可以在转换模型时用--fp16 1参数,模型体积直接砍半,推理速度也能提升30%以上,精度损失对于口红试色这种应用场景完全在可接受范围内。
第三件,把推理放到独立线程。Dart侧通过compute或者Isolate发起推理请求,不让推理阻塞UI线程。在鸿蒙侧,我用的是taskpool来实现,这也是HarmonyOS NEXT主推的并发方案,替代了旧版本的@Worker线程。
实测数据(麒麟9000系列芯片):
- 模型加载耗时:约180ms(只做一次,启动阶段完成)
- 预处理耗时:约8ms
- 单帧推理耗时:约25ms
- 后处理+渲染耗时:约12ms
整体算下来单帧全链路约45ms,稳定在20fps出头。对于口红试色这种镜子型应用,20fps是能接受的体验下限,如果能再做一档分辨率优化,比如输入降到192x192,帧率还能再往上走。
4. 双端通道与数据流设计
4.1 MethodChannel与EventChannel的鸿蒙适配
Flutter和鸿蒙原生通信的方式和Android差不多,都基于平台通道机制,但鸿蒙的实现类名和API有自己的一套。
MethodChannel用于一次性的调用,比如“获取相册图片”、“执行分割推理”、“设置当前色号”。Dart侧调用的方法名,会通过通道传递给鸿蒙侧,鸿蒙侧通过MethodChannel的setMethodCallHandler注册处理函数,完成调用后返回结果给Dart。
EventChannel用于持续的数据流,比如摄像头采集的连续帧。这个通道在鸿蒙上的实现需要实现EventChannel.StreamHandler接口,通过EventSink对象把图像数据逐帧推送出去。这里有一个特别值得注意的点:EventChannel的数据推送频率要自己做节流,否则Dart侧的处理速度跟不上,事件积压会导致内存飙升和明显的延迟增大。
我的方案是:摄像头帧率保持30fps采集,但封装层只允许每50ms推送一帧给Dart侧。这样既保证了操作时的实时反馈,又不会让Dart侧承受过重的处理压力。
4.2 图像数据在双端之间的高效传递
这可能是整个项目里最值得说的一块。
如果你在Dart侧拿到一张CameraImage,想把它传给鸿蒙侧去推理,最直觉的做法是转成Uint8List,通过MethodChannel传过去。但这样有个大问题:每帧图像要经历一次完整的通道序列化,Dart和鸿蒙之间复制一整块内存。摄像头分辨率稍微高一点,一帧就是几MB的数据,每秒30帧就是100MB级别的传输量,通道根本吃不消,而且CPU占用会直线飙升。
我的解决办法是:让摄像头数据全程留在鸿蒙侧,不让它进Dart层。
具体流程是:鸿蒙原生侧在启动摄像头预览后,每一帧图像数据直接在native层完成预处理、推理、后处理,只把最终的分割Mask(一张很小的灰度图)通过EventChannel推给Dart侧。Dart侧拿到Mask后,只负责用Flutter的图形API做口红颜色的混合渲染。这样通道里传的数据量从每帧几MB骤降到每帧256x256x1字节,也就是几十KB,压力瞬间降了几个数量级。
有几个技术细节需要特别说明:
- 鸿蒙摄像头模块的帧回调里拿到的是
PixelMap或者Image对象,需要先转成pixelBuffer才能被推理引擎读取,转换操作在NativeWindow上完成,不走Dart层。 - 图像的方向处理是在native层完成的,因为摄像头传感器出来的数据是旋转过的,前置摄像头还有镜像问题,这些都需要在拿到原始帧后立刻处理,不能等到Dart侧再旋转。
- 预热一个内存池,避免每帧都申请新的byte数组,减少GC压力。
4.3 Dart层状态管理与UI联动
整个应用的业务状态我用Flutter的Provider管理,页面主要有三个:相机试色页、相册试色页、色号列表页。三页共享同一个TryOnState对象,它内部维护当前选中的色号、当前分割Mask、渲染参数等核心业务状态。
Dart侧收到鸿蒙传来的Mask后,要做的事其实不多:
第一,把Mask编码成ui.Image,这一步调decodeImageFromPixels接口,把灰度字节数组转成一个可绘制图像对象。
第二,拿到ui.Image后,用Canvas.drawImage配合BlendMode系列混合模式把口红颜色层画上去。这里我会画两层:先画一个压暗层,再画一个提亮层,分别对应正片叠底和线性减淡的效果。
第三,两个层画完后,整个结果返回给页面层,页面用一个RawImage或者CustomPaint展示给用户。
Dart侧的这三次操作虽然看着简单,但每次都涉及Canvas的绘制过程,所以一定要做好帧率控制。如果用户拖动色号列表时试色预览能跟上,那体验就是及格的。如果掉帧明显,优先检查是不是Mask转ui.Image时重复解码导致的开销过大,可以考虑复用缓存。
5. 摄像头与相册场景的完整实现细节
5.1 摄像头实时试色流程
实时试色是口红试色APP最核心的使用场景,用户举着手机前置摄像头,屏幕上实时显示自己涂上口红的效果。
实现过程中,我踩过的几个大坑值得拿出来单独说。
第一个坑:权限申请时机。鸿蒙的权限系统和Android一样是运行时权限,但申请弹窗的样式和用户引导逻辑不太一样。我一开始在页面初始化时就直接申请相机权限,结果在一些老版本系统上弹窗还没出现,摄像头预览就已经尝试打开了,直接报CameraDeviceException。正确的做法是,在页面onPageShow回调里先检查权限状态,通过了再初始化相机。
第二个坑:摄像头启动参数。前置摄像头的默认输出方向通常和人脸方向差90度,需要在API里设置正确的sensorOrientation校正。鸿蒙CameraKit的CameraOutputCapability提供了previewProfile列表,要多试几个分辨率和帧率组合,找到设备和算法都能接受的那一档。
第三个坑:实时性和画质的平衡。如果直接用满分辨率采集,FirstFrame会非常慢,而且高分辨率意味着模型输入帧数反而上不去,因为预处理和推理都在等待更大的数据量。我最终选择了720p优先策略,也就是默认采集1280x720@30fps,但推理只使用中心的256x256区域。这样既能保证手机上看预览画质足够清晰,又不牺牲分割帧率。
第四个坑:画面预览和渲染结果的方向错位。如果你直接把分割后的Mask贴回原图,就会发现嘴唇的位置在原图里是偏的。这个问题90%出在坐标系转换上——摄像头拿到的原始帧坐标系、预览画面的坐标系、图像分割模型的坐标系,三者因为旋转和裁剪处理不同,往往不是同一个。我的解决方式是:让native层在回传Mask时,同时回传一串变换矩阵参数(旋转角度、缩放比例、中心点偏移),Dart层拿到后统一映射。这个方法虽然多做了一点数学计算,但完美解决了方向错位问题。
5.2 相册照片试色流程
相册场景里,目标照片是从系统相册选出的,可能是一张自拍,也可能是一张他拍。分割模型在这种场景下表现并不稳定,尤其是当人脸在照片里占比很小、或者作为装饰出现时,模型经常会漏检或者误检。
为了提高成功率,我在相册场景里加了一个“人脸检测”的前置步骤。鸿蒙系统自带的TextRecognition和人脸检测能力可以快速判断照片里是否有人脸、人脸的主要区域在哪。如果检测到人脸,我会先把人脸区域裁剪出来,等比放大后送入分割模型,这样嘴唇占输入图像的比例明显变大,分割结果会稳很多。如果检测不到人脸,就直接提示用户“未检测到人脸”,跳过分割流程。
人脸检测的实时性在这个场景下不那么重要(毕竟用户是看一张静态图),精度才是关键。我测试过几种方案,最终选用了MindSpore Lite的人脸检测模型,在普通人脸角度下召回率超过95%。如果因为遮挡导致检测失败,还会给用户提供一个手动圈选嘴唇区域的手动校正入口。
5.3 口红效果精细调节:透明度、光泽与边缘过渡
用户试用色号时,除了颜色本身,还会关注三个调节项:透明度、光泽、边缘羽化。
透明度对应的是色号的叠加强度。不同人口红的使用习惯不一样,有人喜欢淡淡一层,有人喜欢浓郁饱和。透明度参数实际上就是混合公式里的权重系数,在Dart渲染层做mix操作时调整即可。我在实现时把这个参数暴露成一个滑杆,用户可以实时拖动看效果变化。
光泽度对应的是高光层的强度。嘴唇区域的鼻尖高光和唇珠位置的反光,是口红试色看起来真实与否的关键。我在分割Mask的基础上做了二次提取:取Mask区域内灰度值Top 10%的像素,作为高光候选区,再按比例叠加白色或浅色。这个效果参数叫“光泽感”,调大了像镜面唇釉,调小了像雾面哑光。
边缘羽化是一个常常被忽视的小细节。如果直接用原Mask做渲染,试色的边缘会有一圈硬边,显得很假。我在后处理阶段对Mask做了高斯模糊,但同时保留了一部分Alpha阈值,让边缘过渡既柔和又不完全模糊。实测羽化半径在1.5像素左右效果最佳,太大会显得“没涂匀”。
6. 常见问题与排查技巧实录
6.1 编译期问题
问题一:Flutter Engine在鸿蒙上编译时提示NDK版本不匹配
这个问题的根源是OpenHarmony SDK里自带的NDK版本和Flutter引擎编译时所使用的NDK版本不一致。解决方案很直接:去下载对应版本的NDK,然后在local.properties或build-profile.json5中指定NDK路径。不要试图让不同版本的NDK共存,该删就删,该覆盖就覆盖。
问题二:鸿蒙工程里无法识别Flutter的pubspec.yaml依赖
这通常是工程结构没配对。鸿蒙工程里ohos目录是独立的模块,Flutter插件生成的ohos代码没有被settings.gradle正确包含。检查一下settings.gradle里有没有引入Flutter插件的目录,以及在鸿蒙工程的oh-package.json5里是否声明了flutter相关的依赖。
问题三:构建产物中看不到libflutter.so
这个文件是Flutter引擎在鸿蒙上的核心动态库,如果HAP包里没有它,应用一启动就会闪退。检查构建日志,确认是否执行了flutter build hap而不是直接点DevEco Studio的Build按钮。两者最大的区别是,前者会先编译Dart代码并打包引擎和资产,后者只编译鸿蒙壳工程。
6.2 运行期问题
问题一:摄像头画面能打开,但分割Mask一直输出全0
这基本可以确认是图像数据没喂进模型。排查路径分为两步:先确认native层有没有真正拿到摄像头帧数据,有些设备在低光环境下帧率会骤降,30帧变成了1帧,看起来就是“画面卡住但Mask全黑”;再确认输入数据的通道顺序,摄像头出来的Buffer是NV21,而模型期望的是RGB,这一步不做转换,推理结果必然是乱码或全零。
问题二:画面帧率很稳,但嘴唇区域和手指点击位置对不上
这是坐标系没校正的问题。摄像头预览、全屏渲染、模型输入三者使用的坐标系不统一,常见的差异来源是设备屏幕的比例和摄像头输出比例不一致,导致画面做了裁切或拉伸。排查时先固定设备横竖屏,再把三套坐标统一成同一个参考系,然后逐一核对。
问题三:Dart侧收到Mask后渲染,发现画面出现严重重影
重影问题一般出在纹理同步上。鸿蒙的XComponent和Flutter纹理之间共享内存时,如果同步隔离没做好,正在上传的纹理数据会被下一次摄像头新帧的数据覆盖。我的解决方式是在native层给每帧加上序号,Dart侧只用序号最大的那帧,正在渲染的旧帧数据不要被新帧冲掉。
6.3 性能优化核心方法速查
应用刚跑起来时,我用DevEco Studio自带的Profiler工具抓了一遍性能报告,发现CPU占用率和内存占用都偏高。结合分析和反复试验,最终沉淀出下面这几个核心优化点:
模型预热。App冷启动时,在闪屏页的间隙里就初始化MindSpore Lite的推理会话,把模型加载时间隐藏起来。正式进入试色页时,直接可以开始推理。
内存复用。摄像头帧处理的所有buffer都来自预先分配的内存池,整个过程不new任何大对象,避免了反复的内存分配和GC。
降低无用渲染。当用户没有改变色号且没有移动设备时,Mask结果是不变的,此时不需要重新推理,直接复用上一帧的结果,CPU占用可以降到几乎为0。
避免Dart侧解码大图。相册场景的高清照片如果直接交给Flutter渲染,会同时吃满CPU和内存。我的做法是:照片在鸿蒙侧就完成缩放,最长边不超过1280像素,再传给Dart渲染。
可选的极速模式。在设置里提供一个“极速模式”开关,开启后模型输入分辨率降到192x192,帧率能从20fps提升到30fps以上。代价是边缘精度会有轻微下降,但考虑到大部分用户不会贴着脸看细节,这个性价比极高。
7. 写在最后的一些心得体会
做这个口红试色APP的过程,最大的收获并不是学会了某几项具体技术,而是对“跨平台”这个词有了更深的理解。跨平台从来不只是“同一套代码跑多个系统”,它更考验的是,你对每个平台的底层机制有没有够深的认识。
鸿蒙和Flutter的组合,现在正在从“能跑”走向“好跑”的阶段。我在这篇文章里记录的很多细节,比如XComponent和Flutter引擎的衔接、MindSpore Lite的算子兼容问题、原生摄像头数据如何高效传给Dart层,都是社区里还没有特别系统沉淀的内容。希望这篇实践笔记能给正在啃鸿蒙+Flutter这块硬骨头的朋友一些实质性的帮助。
最后分享一个我踩得最重的坑:图像分割模型的效果,直接决定口红试色APP的生死。我第一版用的模型在公开数据集上指标非常漂亮,但在真实自拍场景下频繁翻车,原因就是训练数据里缺少了各种复杂光线环境下的嘴唇图像。后来我在真实环境中采集了上千张覆盖室内外、晨晚、逆光顺光的人脸照片,重新微调了模型之后,整个应用的可用度直线提升。在做这类偏视觉的应用时,算法效果本身的重要性,永远排在工程架构之上。这个次序搞反了,你做出来的产品会始终透着一股“技术demo”的塑料感。