皮肤癌是全球范围内高发的恶性肿瘤之一,早期发现患者的治愈率可以超过九成。随着移动端机器学习框架的成熟,在Android设备上直接运行皮肤病变分类模型已经成为可能。本文将从模型准备、应用集成、测试验证三个层面,完整讲解如何在Android项目中实现并测试一个皮肤癌检测功能。

一、模型的选择、训练与转换为TFLite格式
实现皮肤癌检测的第一步是获得一个可用的分类模型。目前公开可用的数据集主要有ISIC存档数据集和HAM10000数据集,其中HAM10000包含一万多张经过皮肤镜 confirmation 的病变图像,涵盖了黑色素瘤、基底细胞癌、痣等多种类别,非常适合作为训练数据。对于自定义模型训练,通常的做法是基于MobileNetV3或EfficientNet等轻量级骨干网络进行迁移学习,这样既能保证一定的识别准确率,又能控制模型的体积和推理耗时,方便在移动设备上流畅运行。
训练完成后,需要将模型转换为TensorFlow Lite格式。转换过程中建议开启量化,常见的做法是训练后动态范围量化或全整数量化。全整数量化需要提供代表性数据集,用于校准激活值的范围,量化后模型体积通常可以缩小到原来的四分之一,推理速度在低端设备上提升尤为明显。以下是Python端的转换示例:
import tensorflow as tf
# 加载训练好的Keras模型
model = tf.keras.models.load_model("skin_cancer_model.h5")
def representative_dataset():
# 使用代表性图像数据做量化校准
for _ in range(100):
data = tf.random.normal([1, 224, 224, 3])
yield [tf.cast(data, tf.float32)]
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8
tflite_model = converter.convert()
with open("skin_cancer_int8.tflite", "wb") as f:
f.write(tflite_model)转换完成后,一定要用Python端的TFLite解释器对量化模型做一轮离线验证,对比量化前后的准确率损失。如果量化导致准确率下降超过两个百分点,可以考虑改用float16量化,或者延长量化校准的数据量。模型文件最终需要放到Android工程的assets目录下,注意assets中单个文件的读取需要通过文件名完整路径,打包时Gradle默认不会压缩tflite文件,但为了保险起见建议在build.gradle中将aaptOptions的noCompress参数明确设置为tflite,避免某些设备上加载失败。
二、在Android应用中集成相机采集与模型推理
模型准备好之后,就进入Android端的集成环节。整个数据流可以概括为:相机采集图像、裁剪对齐到模型输入尺寸、归一化预处理、送入解释器推理、解析输出概率。相机部分推荐使用CameraX库,它的ImageAnalysis用例可以以流的方式逐帧回调图像数据,配合ImageProxy拿到YUV格式的帧后转换为RGB位图。需要注意CameraX默认输出的旋转角度与设备方向有关,必须根据rotationDegrees做旋转校正,否则竖屏拍摄时模型拿到的图像是横着的,识别结果会明显异常。
图像预处理是移动端推理最容易出错的一环。不同模型对输入的要求不同,有的要求像素值归一化到0到1的浮点区间,量化模型则可能要求0到255的uint8值,均值和标准差的减除方式也各不相同。这些参数必须与训练时的预处理保持完全一致,否则即使模型本身质量很高,推理结果也会不可靠。下面是核心的推理代码:
public class SkinCancerClassifier {
private final Interpreter interpreter;
private final int inputSize = 224;
public SkinCancerClassifier(Context context) throws IOException {
// 从assets加载tflite模型
ByteBuffer modelBuffer = loadModelFile(context, "skin_cancer_int8.tflite");
Interpreter.Options options = new Interpreter.Options();
options.setNumThreads(4);
// 优先尝试NNAPI加速,失败时回退CPU
options.setUseNNAPI(true);
interpreter = new Interpreter(modelBuffer, options);
}
public float[] classify(Bitmap bitmap) {
// 裁剪中心区域并缩放到模型输入尺寸
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true);
ByteBuffer input = ByteBuffer.allocateDirect(inputSize * inputSize * 3);
input.order(ByteOrder.nativeOrder());
int[] pixels = new int[inputSize * inputSize];
scaled.getPixels(pixels, 0, inputSize, 0, 0, inputSize, inputSize);
for (int pixel : pixels) {
input.put((byte) Color.red(pixel));
input.put((byte) Color.green(pixel));
input.put((byte) Color.blue(pixel));
}
// 输出为各类别的概率数组
ByteBuffer output = ByteBuffer.allocateDirect(7);
output.order(ByteOrder.nativeOrder());
interpreter.run(input, output);
float[] result = new float[7];
output.rewind();
for (int i = 0; i < 7; i++) {
result[i] = (output.get() & 0xFF) / 255f;
}
return result;
}
private ByteBuffer loadModelFile(Context context, String name) throws IOException {
AssetFileDescriptor fd = context.getAssets().openFd(name);
FileInputStream fis = new FileInputStream(fd.getFileDescriptor());
return ByteBuffer.wrap(readAllBytes(fis));
}
}工程上还有几个值得注意的细节。第一,推理应放在后台线程执行,CameraX的ImageAnalysis本身支持设置后台执行器,主线程只负责接收结果刷新UI。第二,解释器实例是重量级对象,创建耗时且占用内存,应当在应用生命周期内复用,而不是每帧新建。第三,皮肤病变检测对光照非常敏感,建议在UI上引导用户在光线充足的环境下拍摄,并在预处理阶段加入亮度检测,过暗或过曝的图像直接提示重拍,这在实际体验中比让模型硬算一个不可靠的结果要好得多。
三、测试策略:从单元测试到真实设备验证
医疗类功能对可靠性要求极高,测试环节必须系统化。首先是模型本身的离线评估,用HAM10000的测试集切分,计算每个类别的精确率、召回率和混淆矩阵。对于癌症筛查场景,召回率比精确率更重要,因为漏诊的代价远大于误报。如果黑色素瘤类别的召回率偏低,可以通过训练时的类别加权、数据增强(旋转、翻转、亮度扰动)来改善。这一步在Python端完成,不进入Android工程,但它决定了整个应用的上限。
其次是Android端的单元测试。把图像预处理和输出解析逻辑抽成独立的纯函数类,用JUnit直接测试:给定一张确定尺寸和颜色的Bitmap,验证生成的ByteBuffer内容是否符合预期;给定一段模拟的输出字节,验证解析出的概率数组正确。这样可以在不依赖设备相机的情况下覆盖大部分逻辑。仪器化测试则可以使用Espresso配合CameraX的注入机制,或者直接准备一组固定的测试图片,绕过相机环节调用分类器,断言恶性样本的黑色素瘤概率高于设定阈值、良性样本低于阈值。示例代码如下:
@RunWith(AndroidJUnit4.class)
public class SkinCancerClassifierTest {
@Test
public void melanomaImage_shouldReturnHighRisk() throws Exception {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
SkinCancerClassifier classifier = new SkinCancerClassifier(context);
// 从测试资源中加载已标注的黑色素瘤样本
Bitmap sample = BitmapFactory.decodeStream(
context.getAssets().open("test_melanoma_01.jpg"));
float[] probs = classifier.classify(sample);
// 断言恶性类别概率超过阈值
assertTrue("黑色素瘤概率应高于0.5,实际为:" + probs[4], probs[4] > 0.5f);
}
}最后是真实设备层面的验证。不同芯片对NNAPI的支持程度差异很大,同一份量化模型在旗舰机和中端机上的推理耗时可能相差数倍,建议在加速方式上做好降级策略:NNAPI不可用时自动切回多线程CPU推理。测试时应覆盖至少高中低三档机型,记录每台的推理耗时和内存占用,确保低端机上单帧推理不超过300毫秒。此外还需要做弱网测试(如果结果需要上报云端复核)、权限拒绝场景测试(用户不授予相机权限时的引导流程)以及长时间连续使用的稳定性测试,观察内存是否持续增长。需要强调的是,无论测试多么完善,移动端皮肤癌检测只能作为初筛参考工具,应用内必须明确提示用户结果不构成医学诊断,发现异常病变应尽快就医,由皮肤科医生通过皮肤镜和专业检查做最终判断。