在计算机视觉领域,把训练好的模型搬到移动端,是一件既让人兴奋又有点棘手的事。这里整理了从数据集准备、模型训练、Web部署、API封装,到最终在Android手机上调用API的全链路流程。整个过程不走捷径,但每一步都有清晰的代码和注释,希望能帮到正在做类似项目的你。
一、准备数据集,训练AI模型
搭建深度学习环境这件事就不展开说了,Python生态里该装的库一个都不能少。工程的目录结构和数据集组织方式如下图所示。


数据读取与预处理是模型训练的第一步,read_data.py 把杂乱的文件转化为模型能消化的张量。来看这段代码:
import os
from PIL import Image
import numpy as np
from sklearn.model_selection import train_test_split
from keras.utils import np_utils
import random
class_num = 8 # 类别数
def preprocess(x):
Max = np.max(x)
Min = np.min(x)
x = (x - Min) / (Max - Min)
return x * 2 - 1
def read_img(img_name, size, c):
im = Image.open(img_name)
im = im.resize((size, size))
im = im.convert("L") if c == 1 else im.convert("RGB")
data = np.array(im)
return data
def read_label(img_name):
basename = os.path.basename(img_name)
label = basename.split('_')[0]
return label
def get_data(pt, size=64, c=1, shuffle=True, test_size=0.2):
images, labels = [], []
i = 0
for x in os.listdir(pt):
d = os.path.join(pt, x)
images.append(read_img(d, size=size, c=c))
labels.append(read_label(d)[0])
i += 1
if shuffle == True:
imgs, lbs = [], []
index = [x for x in range(len(labels))]
random.shuffle(index)
for i in index:
imgs.append(images[i])
lbs.append(labels[i])
images, labels = imgs, lbs
y = np.array(list(map(int, labels)))
x = np.array(images)
x = x.reshape(len(x), size, size, c)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=test_size, random_state=0)
y_train = np_utils.to_categorical(y_train, class_num)
y_test = np_utils.to_categorical(y_test, class_num)
x_train = preprocess(x_train).astype('float32')
x_test = preprocess(x_test).astype('float32')
print(len(x_train), len(y_train))
print(len(x_test), len(y_test))
return (x_train, y_train, x_test, y_test)
接着是CNN模型的构建与训练,cnn01.py 里定义了一个多层卷积网络,加上批标准化、Dropout防止过拟合,最终用Softmax输出8个类别的概率。
from keras import Input, Model
from keras.layers import Conv2D, MaxPool2D, BatchNormalization
from keras.layers import Dropout, Flatten, Dense
import matplotlib.pyplot as plt
from read_data import get_data
pt = "data"
size = 64
classes = 8
c = 3
batch_size = 128
epochs = 20
name = './result/cnn01C64'
def model_():
inputs = Input((size, size, c))
x = BatchNormalization(axis=-1)(inputs)
x = Conv2D(128, (3, 3), activation='relu')(x)
x = Conv2D(64, (3, 3), activation='relu')(x)
x = MaxPool2D((2, 2))(x)
x = Dropout(0.6)(x)
x = Conv2D(64, (3, 3), activation='relu')(x)
x = MaxPool2D((2, 2))(x)
x = Dropout(0.65)(x)
x = Flatten()(x)
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)
x = Dropout(0.6)(x)
outputs = Dense(classes, activation='softmax')(x)
model = Model(inputs=inputs, outputs=outputs)
return model
def train_model(epochs):
model = model_()
model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
x_train, y_train, x_test, y_test = get_data(pt=pt, size=size, c=c)
result = model.fit(x_train, y_train, validation_data=(x_test, y_test), epochs=epochs)
model.sa ve(name + '.h5')
return result
def result_curve(result):
# 绘制训练曲线
# 略……(与原文一致)
执行 cnn01.py 后,训练好的模型会保存为 result/cnn01C64.h5。
移动端不能直接跑h5格式,得转成轻量的 .tflite。 h5_2_tflite.py 完成转换并顺便做个测试。
import tensorflow as tf
import numpy as np
from PIL import Image
from read_data import preprocess
def h5_to_tflite(in_f, out_f):
model = tf.keras.models.load_model(in_f)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open(out_f, 'wb') as f:
f.write(tflite_model)
def test(model_path, img_file, size, c):
img = Image.open(img_file)
im = img.resize((size, size))
im = im.convert("L") if c == 1 else im.convert("RGB")
x = np.array(im)
x = np.reshape(x, (1, size, size, c))
x = preprocess(x)
x = x.astype(np.float32)
interpreter = tf.lite.Interpreter(model_path)
interpreter.allocate_tensors()
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
interpreter.set_tensor(input_details[0]['index'], x)
interpreter.invoke()
output = interpreter.get_tensor(output_details[0]['index'])
y = np.argmax(output, axis=1)[0]
print(y)
转换完成后,就能在边缘设备上愉快地推理了。
二、Web服务器部署测试及API封装
光有模型不够,得把它变成一个能对外提供服务的接口。前端页面 templates/index.html 提供了一个简单的文件上传表单,后端 app.py 用Flask搭建,同时提供了网页端和API端两种请求方式。
from flask import Flask, render_template, request, jsonify
import tensorflow as tf
import numpy as np
from PIL import Image
import os
app = Flask(__name__)
model = tf.keras.models.load_model("./result/cnn01C64.h5")
img_size = 64
img_channel = 3
dct = {0: '苹果', 1: '香蕉', 2: '樱桃', 3: '火龙果', 4: '芒果', 5: '橘子', 6: '木瓜', 7: '菠萝'}
def preprocess(filepath):
img = Image.open(filepath)
img = img.convert("RGB")
img = img.resize((img_size, img_size))
img = np.array(img) / 255.0
img = img.reshape(1, img_size, img_size, img_channel)
return img
def predict(filepath):
img = preprocess(filepath)
pred = model.predict(img)
result = np.argmax(pred)
result = dct.get(result)
confidence = np.max(pred)
return result, confidence
# ... 路由和API接口代码(与原文一致)
记得把 host 改成你自己的IP地址。下面是Web端的运行效果。

API接口测试脚本 api_test.py 用 requests 发送图片,返回JSON结果。
import requests
url = "https://192.168.1.5:8086/api/predict"
files = {"file": open("test.jpg", "rb")}
response = requests.post(url, files=files)
results = response.json()
result = results.get('result')
confidence = results.get('confidence')
print(f"识别结果:{result} 置信度:{round(confidence,3)}")
测试结果如图。

三、Android手机端调用API
移动端的部署往往是最容易出问题的一环,不过有了前面的API,Android这边只需要做一件事:把图片发过去,把结果拿回来。
先搭建Android开发环境,选择“Empty Views Activity”模板,语言用Ja va。记得换国内镜像源——这一步能省下不少等待编译的时间。
修改 gradle/wrapper/gradle-wrapper.properties

distributionUrl=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-9.2.1-bin.zip
修改 settings.gradle.kts

ma ven { setUrl("https://ma ven.aliyun.com/repository/central") }
ma ven { setUrl("https://ma ven.aliyun.com/repository/jcenter") }
ma ven { setUrl("https://ma ven.aliyun.com/repository/google") }
ma ven { setUrl("https://ma ven.aliyun.com/repository/gradle-plugin") }
ma ven { setUrl("https://ma ven.aliyun.com/repository/public") }
ma ven { setUrl("https://jitpack.io") }
google()
界面设计都在 activity_main.xml 里,一个ImageView显示图片,三个按钮(选择、拍照、API识别),还有两个TextView显示结果和置信度。
网络请求用了 OkHttp,封装了一个 ApiCaller 类,处理异步回调。别忘了在 app/build.gradle.kts 中添加依赖。
implementation("com.squareup.okhttp3:okhttps:4.12.0")
接着是 MainActivity.ja va,处理相册选择、拍照、点击识别三个事件。识别时调用 ApiCaller.uploadBitmap,在回调中解析JSON,更新UI。
// 代码与原文一致
Android 9+ 默认禁止明文HTTP,需要配置网络安全策略。在 res/xml/network_security_config.xml 中放行。
同时在 AndroidManifest.xml 中声明网络权限并引用该配置。
最后,APP的测试效果如图所示。

整个流程走下来,你会发现从数据到手机其实只隔了几层皮。关键在于每一步的衔接要清晰,代码要可复用。希望这份记录能给你带来一些启发。
