本科阶段做车辆氛围灯项目时,我想解决的不是“在手机上选一个颜色”,而是让灯光能跟着用户喜欢的画面变化。用户可以从相册、相机或系统壁纸中选图,应用提取其中的主色,再把这些颜色分配到车内 LED 灯带上。

我把它做成了一套 Android 应用:Kotlin 负责业务逻辑,Jetpack Compose 负责界面,颜色提取在手机本地完成,最后通过 Art-Net 协议直接控制灯光设备。

从一张图片到一条灯带

整个流程可以概括为:

flowchart LR
    A["相册 / 相机 / 系统壁纸"] --> B["图片缩放与取色"]
    B --> C["Palette 或 K-Means"]
    C --> D["亮度、饱和度与互补色处理"]
    D --> E["LED 颜色插值"]
    E --> F["Art-Net DMX 数据包"]
    F --> G["车内 LED 控制器"]

图片输入有三种。相册适合提前准备主题图,相机可以现场拍摄,系统壁纸则让灯光自动延续手机当前的视觉风格。图片加载后先缩小尺寸,避免直接在原图上做聚类拖慢界面。

我提供了两套取色方法。第一套使用 Android Palette,从图片色块中筛掉过暗、饱和度过低的颜色,再按出现频率选择主色。如果画面接近单色,还可以补一组互补色,让灯带不至于全部挤在一个色相里。

第二套是 K-Means。图片会先降采样到大约 100 x 100,像素转换成 RGB 点,再进行有限轮次的聚类。这里不追求图像分析上的极致精度,目标是在手机上快速得到肉眼合理的几组颜色。用户可以在设置中切换算法,也可以调整颜色数量、最低亮度和最低饱和度。

Android 端怎样组织状态

应用采用 MVVM。Compose 页面负责展示和交互,AmbientLightViewModel 维护图片、提取结果、连接状态和全部设置,SettingsRepository 通过 DataStore 保存语言、算法、LED 数量、阈值和 Art-Net 地址。

1
2
3
4
5
Compose Screens

AmbientLightViewModel + StateFlow

Color Extractors / SettingsRepository / ArtNetController

这样做有一个直接好处:设置页修改算法或阈值后,ViewModel 可以重新计算当前图片,而不需要页面自己保存一份颜色状态。相机、相册和壁纸最后也都进入同一个 onImageSelected 流程。

界面使用 Jetpack Compose 和 Material 3,主页面包含图片预览、提取色板、LED 渐变预览、图库、相机和壁纸入口。设置页处理中文/英文、算法、颜色数量、过滤阈值、LED 数量以及目标设备 IP。保存设置时统一提交,避免拖动多个滑块时持续写入 DataStore 和重复计算。

Art-Net 控制不是普通网络请求

取出几种颜色后,我按 LED 在灯带上的位置做分段插值,把离散色板变成连续渐变。每颗 RGB 灯占三个 DMX 通道,数据再封装成 ArtDmx UDP 包发送到端口 6454

ArtNetController 会预先写好固定包头并复用缓冲区,序列号在 1-255 之间循环。DMX 数据长度需要是偶数,长度字段使用大端序;Art-Net 操作码本身又是小端序。这些细节如果写错,手机端看起来发送成功,控制器却不会响应。

为了避免每次状态变化都创建 Socket 或堆积 UDP 包,我让 DatagramSocket 在连接期间复用,并把更新频率限制在约 30 FPS。新颜色到来时可以取消尚未执行的更新,只发送较新的结果。对于氛围灯来说,这个速度已经足够平滑。

我在实现中遇到的边界

Android 权限和硬件协议是这个项目里最容易被忽略的两部分。相机需要运行时权限和 FileProvider,读取系统壁纸在不同 Android 版本上也有兼容差异。网络连通只说明 UDP Socket 可用,不代表 Art-Net 设备真的接收了灯光数据,因此连接状态还需要更明确的设备反馈。

另一个限制来自单个 DMX Universe。一个 Universe 最多 512 个通道,而 RGB 灯每颗占三个通道,所以单 Universe 实际只能完整容纳 170 颗 RGB 灯。应用设置允许更大的 LED 数量,后续应该在输入端限制,或者增加多 Universe 分包,不能只扩大数组。

测试方面,我为颜色提取、设置存储和 Art-Net 控制准备了基础用例,但真实灯光效果仍然需要在不同图片、不同灯珠数量和实际控制器上做集成测试。UDP 无连接的特性决定了“函数没有抛异常”并不等于硬件工作正常。

回头看这个项目

AmbientCarLight 把 Android、图像处理和硬件控制连到了一起。它不是单纯的取色 Demo:图片输入、算法选择、状态持久化、灯带插值和 Art-Net 发包都在同一条用户流程里。

这次实现让我更熟悉 Compose 的状态组织,也第一次认真处理实时灯光协议里的包结构、更新频率和硬件边界。以后再做类似项目,我会更早加入设备发现、Universe 配置和真实回执,把“能发数据”推进到“能确认整套设备正在按预期运行”。