去年帮水产养殖客户做了一套鱼类识别系统,从一开始“自定义CNN模型准确率85%、识别一张图要2秒”,到最后优化成“基于ResNet50迁移学习准确率98.5%、0.3秒/张实时识别”,前后踩了10多个坑——比如数据集标注混乱导致模型过拟合、前后端跨域调试卡了3天、服务器部署时模型加载内存溢出。这篇文章把整个开发流程拆解开,从需求分析到技术选型,再到CNN模型实战、前后端联调和工业级部署,每一步都带可复用的代码和踩坑记录,你跟着做也能搭出自己的鱼类识别系统。
一、先搞懂:为什么要做鱼类识别系统?核心需求是什么?
别上来就堆技术,先明确业务场景——我做的这套系统主要用于水产养殖病害预防和渔业资源调查,核心需求有3个:
实时识别:养殖人员用手机拍鱼,1秒内返回种类(比如“草鱼”“鲫鱼”)和健康状态(是否有红斑病征兆);高准确率:常见淡水鱼识别准确率≥98%,避免认错导致用药错误;易操作:前端界面简单,养殖户不用懂技术也能上手,后端支持批量上传图片批量识别。
明确需求后,再选技术栈才不会盲目——比如实时性要求高,就不能用太复杂的模型;要支持手机操作,前端就得做响应式;批量识别需要后端做异步处理。
二、技术栈选型:为什么选Python+TensorFlow+Vue3+Django?
不是为了堆技术,而是每个选型都对应业务需求:
| 技术模块 | 选型 | 选型理由 |
|---|---|---|
| 深度学习框架 | TensorFlow 2.15 | 相比PyTorch,部署更成熟(支持TensorRT加速),适合工业级落地;Keras API上手快 |
| 图像识别算法 | CNN(ResNet50迁移学习) | 自定义CNN准确率不够,ResNet50预训练模型能快速提升精度,还能减少数据量需求 |
| 后端API | Django 4.2 + Django REST Framework | 快速开发RESTful API,自带Admin后台,方便客户管理识别记录 |
| 前端界面 | Vue3 + Vant 4 | 轻量化,支持响应式(手机/电脑都能用),Vant有现成的上传/列表组件,省时间 |
| 数据库 | MySQL 8.0 | 存储识别记录(图片路径、识别结果、时间),关系型数据库适合结构化数据 |
| 部署工具 | Docker + Nginx | 容器化部署,避免环境依赖问题;Nginx做反向代理,支持高并发 |
新手别乱换技术栈,比如用Flask代替Django没问题,但要自己搭Admin后台;用PyTorch代替TensorFlow也可以,但部署时要多做一步模型转换(转ONNX)。
三、核心实战1:CNN深度学习模型开发(鱼类识别的“大脑”)
这部分是系统的核心,也是最容易踩坑的地方。我会从数据集准备到模型训练、优化,一步步讲实战细节。
3.1 数据集准备:标注比模型更重要(踩坑1:标注混乱导致模型过拟合)
一开始用网上爬的“鱼类数据集”,标注混乱(比如同一张草鱼图标成“鲫鱼”),训练出的模型准确率只有82%。后来自己标注了3000张图(10种常见淡水鱼,每种300张),准确率直接提到95%。
数据集规格:
类别:草鱼、鲫鱼、鲤鱼、鲢鱼、鳙鱼、鲈鱼、黑鱼、黄颡鱼、泥鳅、鳝鱼(10类);数量:每类300张,共3000张;预处理:统一 resize 到 224×224(ResNet50输入要求),做数据增强(旋转±15°、水平翻转、亮度±20%)。
数据增强代码(TensorFlow Keras):
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# 数据增强配置(只在训练集用,验证集不用)
train_datagen = ImageDataGenerator(
rescale=1./255, # 归一化到0-1
rotation_range=15, # 随机旋转±15°
width_shift_range=0.1, # 水平偏移
height_shift_range=0.1, # 垂直偏移
horizontal_flip=True, # 水平翻转
brightness_range=[0.8, 1.2] # 亮度调整
)
# 验证集只做归一化
val_datagen = ImageDataGenerator(rescale=1./255)
# 加载数据集(按文件夹分类,文件夹名就是类别名)
train_generator = train_datagen.flow_from_directory(
'dataset/train', # 训练集路径
target_size=(224, 224),
batch_size=32,
class_mode='categorical' # 多分类,用categorical_crossentropy
)
val_generator = val_datagen.flow_from_directory(
'dataset/val', # 验证集路径(占总数据20%)
target_size=(224, 224),
batch_size=32,
class_mode='categorical'
)
# 查看类别映射(关键!后续识别要用到)
class_indices = train_generator.class_indices # {0: '草鱼', 1: '鲫鱼', ...}
# 反转映射:id→类别名
id_to_class = {v: k for k, v in class_indices.items()}
# 保存映射到文件(后端要用)
import json
with open('id_to_class.json', 'w', encoding='utf-8') as f:
json.dump(id_to_class, f)
3.2 模型训练:用ResNet50迁移学习(踩坑2:自定义CNN训练难收敛)
一开始自己搭CNN(3个卷积层+2个全连接层),训练100轮后准确率才85%,还出现过拟合。换成ResNet50预训练模型,冻结前50层,只训练顶层全连接层,50轮就到98.5%。
模型搭建代码:
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout
# 加载ResNet50预训练模型(排除顶层全连接层)
base_model = ResNet50(
weights='imagenet', # 加载ImageNet预训练权重
include_top=False, # 不包含顶层分类层
input_shape=(224, 224, 3)
)
# 冻结基础模型(先不训练,只训练新增层)
for layer in base_model.layers[:-10]: # 只解冻最后10层,平衡精度和训练速度
layer.trainable = False
# 新增顶层分类层
x = base_model.output
x = GlobalAveragePooling2D()(x) # 全局平均池化,减少参数
x = Dense(1024, activation='relu')(x)
x = Dropout(0.5)(x) # Dropout防止过拟合
predictions = Dense(10, activation='softmax')(x) # 10类,softmax输出概率
# 构建完整模型
model = Model(inputs=base_model.input, outputs=predictions)
# 编译模型
model.compile(
optimizer='adam', # Adam优化器,学习率默认0.001
loss='categorical_crossentropy', # 多分类损失
metrics=['accuracy'] # 监控准确率
)
# 训练模型(用早停法防止过拟合)
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
# 早停:验证集准确率3轮不提升就停止
early_stopping = EarlyStopping(monitor='val_accuracy', patience=3, restore_best_weights=True)
# 保存最优模型
model_checkpoint = ModelCheckpoint('fish_model.h5', monitor='val_accuracy', save_best_only=True)
# 开始训练
history = model.fit(
train_generator,
epochs=50,
validation_data=val_generator,
callbacks=[early_stopping, model_checkpoint]
)
# 评估模型(在测试集上)
test_generator = val_datagen.flow_from_directory(
'dataset/test',
target_size=(224, 224),
batch_size=32,
class_mode='categorical',
shuffle=False
)
loss, accuracy = model.evaluate(test_generator)
print(f'测试集准确率:{accuracy:.4f}') # 输出:测试集准确率:0.9850
3.3 模型优化:从“能跑”到“工业级”(踩坑3:模型太大加载慢)
训练好的有90MB,后端加载要3秒,手机调用接口延迟高。用TensorFlow Lite做模型量化,压缩到22MB,加载时间缩短到0.5秒,准确率只降了0.3%。
fish_model.h5
模型量化代码:
import tensorflow as tf
# 加载训练好的模型
model = tf.keras.models.load_model('fish_model.h5')
# 转换为TFLite模型(动态量化)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT] # 动态量化
tflite_model = converter.convert()
# 保存量化模型
with open('fish_model.tflite', 'wb') as f:
f.write(tflite_model)
# 测试量化模型准确率(确保下降不多)
interpreter = tf.lite.Interpreter(model_path='fish_model.tflite')
interpreter.allocate_tensors()
# 获取输入输出张量
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# 测试单张图片
import cv2
import numpy as np
def predict_image(image_path):
# 预处理图片
img = cv2.imread(image_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转RGB(OpenCV默认BGR)
img = cv2.resize(img, (224, 224))
img = img / 255.0
img = np.expand_dims(img, axis=0) # 加batch维度
# 量化输入(关键!量化模型要对应输入类型)
input_data = np.array(img, dtype=np.float32)
interpreter.set_tensor(input_details[0]['index'], input_data)
# 推理
interpreter.invoke()
output_data = interpreter.get_tensor(output_details[0]['index'])
pred_class_id = np.argmax(output_data)
pred_score = output_data[0][pred_class_id]
# 加载类别映射
with open('id_to_class.json', 'r', encoding='utf-8') as f:
id_to_class = json.load(f)
pred_class = id_to_class[str(pred_class_id)]
return pred_class, pred_score
# 测试
pred_class, pred_score = predict_image('test_fish.jpg')
print(f'识别结果:{pred_class},置信度:{pred_score:.4f}') # 输出:识别结果:草鱼,置信度:0.9923
四、核心实战2:Django后端开发(搭建识别API)
后端主要做3件事:接收前端上传的图片、调用TFLite模型预测、返回结果并保存记录。
4.1 项目初始化与配置
# 创建Django项目
django-admin startproject fish_recognition
cd fish_recognition
# 创建app
python manage.py startapp api
# 安装依赖
pip install django djangorestframework pillow tensorflow-cpu # CPU版TensorFlow,部署轻量
配置
settings.py(关键部分):
settings.py
INSTALLED_APPS = [
# ... 其他默认app
'rest_framework',
'api',
]
# 配置媒体文件路径(存储上传的图片)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# REST Framework配置
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_THROTTLE_CLASSES': [ # 接口限流,防止滥用
'rest_framework.throttling.AnonRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/minute', # 匿名用户每分钟100次请求
}
}
4.2 模型加载与预测工具类(
api/utils.py)
api/utils.py
把模型加载逻辑封装成工具类,避免每次请求都重新加载(踩坑4:重复加载模型导致内存溢出):
import tensorflow as tf
import json
import numpy as np
import cv2
from django.conf import settings
import os
# 单例模式加载模型(全局只加载一次)
class FishModelSingleton:
_instance = None
_interpreter = None
_id_to_class = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
# 加载TFLite模型
model_path = os.path.join(settings.BASE_DIR, 'models', 'fish_model.tflite')
cls._interpreter = tf.lite.Interpreter(model_path=model_path)
cls._interpreter.allocate_tensors()
# 加载类别映射
class_map_path = os.path.join(settings.BASE_DIR, 'models', 'id_to_class.json')
with open(class_map_path, 'r', encoding='utf-8') as f:
cls._id_to_class = json.load(f)
return cls._instance
def predict(self, image_path):
# 图片预处理(和训练时一致)
img = cv2.imread(image_path)
if img is None:
raise ValueError("图片无法读取")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (224, 224))
img = img / 255.0
img = np.expand_dims(img, axis=0).astype(np.float32)
# 推理
input_details = self._interpreter.get_input_details()
output_details = self._interpreter.get_output_details()
self._interpreter.set_tensor(input_details[0]['index'], img)
self._interpreter.invoke()
output_data = self._interpreter.get_tensor(output_details[0]['index'])
# 解析结果
pred_class_id = str(np.argmax(output_data))
pred_class = self._id_to_class[pred_class_id]
pred_score = float(output_data[0][np.argmax(output_data)])
return {
'class': pred_class,
'confidence': round(pred_score, 4)
}
# 初始化模型实例
model_instance = FishModelSingleton.get_instance()
4.3 识别API开发(
api/views.py)
api/views.py
写两个接口:单图识别、批量识别(支持多图上传):
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .utils import model_instance
from django.core.files.storage import FileSystemStorage
from django.conf import settings
import os
import uuid
# 单图识别API
class FishRecognitionView(APIView):
def post(self, request):
# 检查是否有图片上传
if 'image' not in request.FILES:
return Response(
{'error': '请上传图片'},
status=status.HTTP_400_BAD_REQUEST
)
# 保存上传的图片(用UUID命名,避免重复)
image_file = request.FILES['image']
fs = FileSystemStorage()
filename = f"{uuid.uuid4()}.{image_file.name.split('.')[-1]}"
image_path = fs.save(os.path.join('uploads', filename), image_file)
full_image_path = os.path.join(settings.MEDIA_ROOT, image_path)
try:
# 调用模型预测
result = model_instance.predict(full_image_path)
# 保存识别记录到数据库(后续实现)
# ...
return Response({
'success': True,
'result': result,
'image_url': f"{settings.MEDIA_URL}{image_path}"
})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 批量识别API(支持多图上传)
class BatchFishRecognitionView(APIView):
def post(self, request):
if 'images' not in request.FILES:
return Response(
{'error': '请上传图片列表'},
status=status.HTTP_400_BAD_REQUEST
)
# 处理多图上传
image_files = request.FILES.getlist('images')
results = []
for image_file in image_files:
filename = f"{uuid.uuid4()}.{image_file.name.split('.')[-1]}"
fs = FileSystemStorage()
image_path = fs.save(os.path.join('uploads', filename), image_file)
full_image_path = os.path.join(settings.MEDIA_ROOT, image_path)
try:
result = model_instance.predict(full_image_path)
results.append({
'image_url': f"{settings.MEDIA_URL}{image_path}",
'result': result
})
except Exception as e:
results.append({
'image_url': f"{settings.MEDIA_URL}{image_path}",
'error': str(e)
})
return Response({
'success': True,
'count': len(results),
'results': results
})
4.4 配置URL(
fish_recognition/urls.py)
fish_recognition/urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls.static import static
from django.conf import settings
from api.views import FishRecognitionView, BatchFishRecognitionView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/recognize/', FishRecognitionView.as_view(), name='fish_recognize'),
path('api/batch-recognize/', BatchFishRecognitionView.as_view(), name='batch_recognize'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # 媒体文件URL
4.5 测试API(用Postman)
启动Django服务:;Postman发送POST请求到
python manage.py runserver 0.0.0.0:8000,form-data上传
http://localhost:8000/api/recognize/字段;响应结果示例:
image
{
"success": true,
"result": {
"class": "草鱼",
"confidence": 0.9923
},
"image_url": "/media/uploads/5f8a7b9c-1234-5678-90ab-cdef12345678.jpg"
}
五、核心实战3:Vue3前端开发(用户交互界面)
前端做3个核心功能:图片上传(支持拍照/本地选择)、识别结果展示、历史记录查询。用Vue3+Vant4(移动端友好)开发。
5.1 项目初始化
# 创建Vue项目
npm create vue@latest fish-recognition-frontend
cd fish-recognition-frontend
npm install
# 安装Vant4
npm i vant
5.2 配置Vant(
main.js)
main.js
import { createApp } from 'vue'
import App from './App.vue'
import { Uploader, Button, Card, List, Cell, Toast } from 'vant'
import 'vant/lib/index.css'
const app = createApp(App)
app.use(Uploader).use(Button).use(Card).use(List).use(Cell).use(Toast)
app.mount('#app')
5.3 核心页面:识别页(
views/Recognition.vue)
views/Recognition.vue
实现图片上传、调用API、展示结果:
<template>
<div class="recognition-page">
<!-- 标题 -->
<div class="page-title">鱼类识别系统</div>
<!-- 图片上传 -->
<van-uploader
v-model="fileList"
multiple
accept="image/*"
:before-read="beforeRead"
:after-read="afterRead"
max-count="5"
upload-text="点击上传图片(最多5张)"
class="uploader"
/>
<!-- 识别结果 -->
<div class="result-container" v-if="results.length > 0">
<div class="result-title">识别结果</div>
<van-card
v-for="(item, index) in results"
:key="index"
class="result-card"
>
<img :src="item.imageUrl">{ item.result.class }}</div>
<div class="result-confidence">置信度:{{ (item.result.confidence * 100).toFixed(2) }}%</div>
</div>
<div class="result-error" v-else>
识别失败:{{ item.error }}
</div>
</van-card>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Toast } from 'vant'
import axios from 'axios'
// 后端API地址(本地开发用localhost,部署后改服务器地址)
const API_BASE_URL = 'http://localhost:8000/api'
// 上传文件列表
const fileList = ref([])
// 识别结果
const results = ref([])
// 图片上传前校验(只允许图片)
const beforeRead = (file) => {
const isImage = file.type.startsWith('image/')
if (!isImage) {
Toast('请上传图片文件')
return false
}
return true
}
// 图片上传后处理(调用API识别)
const afterRead = async (files) => {
// 处理单图/多图
const fileList = Array.isArray(files) ? files : [files]
if (fileList.length === 0) return
// 显示加载中
Toast.loading({
message: '识别中...',
duration: 0,
forbidClick: true
})
try {
// 构建FormData
const formData = new FormData()
fileList.forEach(file => {
formData.append('images', file.file) // 批量识别用images字段
})
// 调用批量识别API
const response = await axios.post(
`${API_BASE_URL}/batch-recognize/`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
// 处理结果
const { results: apiResults } = response.data
results.value = apiResults.map(item => ({
imageUrl: item.image_url.startsWith('http') ? item.image_url : `${API_BASE_URL.replace('/api', '')}${item.image_url}`,
success: !item.error,
result: item.result,
error: item.error
}))
Toast.success(`识别完成,共${apiResults.length}张图片`)
} catch (error) {
Toast.fail('识别失败,请重试')
console.error('识别错误:', error)
} finally {
// 关闭加载
Toast.clear()
// 清空上传列表
fileList.value = []
}
}
</script>
<style scoped>
.recognition-page {
padding: 16px;
background-color: #f5f5f5;
min-height: 100vh;
}
.page-title {
font-size: 20px;
font-weight: bold;
text-align: center;
margin-bottom: 24px;
color: #333;
}
.uploader {
margin-bottom: 24px;
}
.result-container {
margin-top: 24px;
}
.result-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.result-card {
margin-bottom: 16px;
}
.result-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 8px;
}
.result-content {
font-size: 14px;
color: #333;
}
.result-class {
margin-bottom: 4px;
}
.result-confidence {
color: #666;
}
.result-error {
font-size: 14px;
color: #ff4444;
}
</style>
六、核心实战4:系统部署(Docker+Nginx工业级落地)
本地跑通后,要部署到服务器才能给客户用。用Docker打包,避免环境依赖问题;Nginx做反向代理,处理静态文件和跨域。
6.1 后端Dockerfile(
Dockerfile.backend)
Dockerfile.backend
# 基础镜像:Python 3.10-slim
FROM python:3.10-slim
# 设置工作目录
WORKDIR /app
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制项目文件
COPY . .
# 创建媒体文件目录
RUN mkdir -p /app/media/uploads /app/models
# 暴露端口
EXPOSE 8000
# 启动命令(用gunicorn代替runserver,支持高并发)
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "fish_recognition.wsgi:application"]
6.2 前端Dockerfile(
Dockerfile.frontend)
Dockerfile.frontend
# 构建阶段
FROM node:16-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 生产阶段
FROM nginx:alpine as production-stage
# 复制构建产物到Nginx静态目录
COPY --from=build-stage /app/dist /usr/share/nginx/html
# 复制Nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
6.3 Nginx配置(
nginx.conf)
nginx.conf
处理前端静态文件、反向代理后端API、解决跨域:
server {
listen 80;
server_name 你的服务器IP或域名; # 替换成实际地址
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # 解决Vue路由刷新404
}
# 后端API反向代理
location /api/ {
proxy_pass http://backend:8000/api/; # backend是Docker Compose中的服务名
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 媒体文件(上传的图片)
location /media/ {
proxy_pass http://backend:8000/media/;
proxy_set_header Host $host;
}
# 跨域配置
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
6.4 Docker Compose(
docker-compose.yml)
docker-compose.yml
一键启动后端、前端、Nginx:
version: '3.8'
services:
backend:
build:
context: ./backend # 后端项目目录
dockerfile: Dockerfile.backend
volumes:
- ./backend/media:/app/media # 挂载媒体文件目录
- ./backend/models:/app/models # 挂载模型目录
restart: always
networks:
- fish-net
frontend:
build:
context: ./frontend # 前端项目目录
dockerfile: Dockerfile.frontend
restart: always
networks:
- fish-net
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- backend
- frontend
restart: always
networks:
- fish-net
networks:
fish-net:
driver: bridge
6.5 部署步骤
服务器安装Docker和Docker Compose;把后端、前端项目和配置文件传到服务器,目录结构如下:
fish-recognition/
├── backend/ # 后端Django项目
│ ├── Dockerfile.backend
│ ├── requirements.txt
│ └── ...
├── frontend/ # 前端Vue项目
│ ├── Dockerfile.frontend
│ └── ...
├── nginx.conf
└── docker-compose.yml
上传模型文件(、
fish_model.tflite)到
id_to_class.json;启动服务:
backend/models/;访问:浏览器打开
docker-compose up -d,就能用前端界面上传图片识别。
http://服务器IP
七、实战踩坑总结(避免你走弯路)
数据集标注混乱:一开始用网上的数据集,准确率上不去,后来自己标注3000张图才解决——数据集质量比数量更重要;模型过拟合:自定义CNN层数太少,加Dropout层、用早停法、数据增强,三管齐下才解决;模型加载内存溢出:每次请求都加载模型,用单例模式全局只加载一次,内存从1.5G降到300M;前端跨域问题:本地开发时前端调后端API报跨域,在Django加CORS中间件(),部署后用Nginx配置跨域;服务器部署静态文件404:Django的MEDIA文件没挂载到容器外,重新配置Docker volumes,确保数据持久化。
django-cors-headers
八、系统扩展方向(给你更多启发)
多端适配:现在是Web端,可扩展成微信小程序、APP(用Flutter),方便养殖户在鱼塘边用;病害识别:目前只识别种类,可扩展识别鱼的健康状态(比如红斑病、烂鳃病),需要标注病害图片;边缘部署:把模型部署到边缘设备(比如树莓派),在没有网络的鱼塘现场识别;数据统计:后端加数据分析模块,统计每月识别最多的鱼类、病害发生率,给客户提供养殖建议。
总结
这套鱼类识别系统从开发到部署,前后花了1个月,核心不是堆技术,而是从业务需求出发,用合适的技术解决实际问题——比如客户需要实时识别,就用TFLite量化模型;需要手机操作,就用Vue3+Vant做响应式前端。
如果你也想做类似的图像识别系统(比如植物识别、垃圾分类),完全可以套用这套流程:数据集准备→CNN模型训练→前后端开发→Docker部署。遇到具体问题时,别着急换技术栈,先排查细节(比如模型过拟合可能是数据增强不够,不是模型选错了)。