本项目包含 CNN 模型训练、模型推理服务、讯飞 API 调用、FRP 内网穿透配置等核心模块。 点击下方标签切换查看不同模块的代码。
# model.py - CNN 果蔬分类模型定义 import torch import torch.nn as nn import torchvision.models as models class FruitVegCNN(nn.Module): """基于 ResNet18 迁移学习的果蔬分类模型""" def __init__(self, num_classes=36, pretrained=True): super(FruitVegCNN, self).__init__() # 加载预训练 ResNet18 self.backbone = models.resnet18(pretrained=pretrained) # 冻结特征提取层 for param in self.backbone.parameters(): param.requires_grad = False # 替换全连接层 num_features = self.backbone.fc.in_features self.backbone.fc = nn.Sequential( nn.Linear(num_features, 512), nn.ReLU(), nn.Dropout(0.5), nn.Linear(512, num_classes) ) def forward(self, x): return self.backbone(x) def get_model(num_classes=36, device='cuda'): model = FruitVegCNN(num_classes=num_classes) return model.to(device)
# train.py - 模型训练脚本 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader import torchvision.transforms as transforms import torchvision.datasets as datasets from model import get_model # 数据增强与预处理 train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) # 加载数据集 train_dataset = datasets.ImageFolder('data/train', transform=train_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) num_classes = len(train_dataset.classes) # 初始化模型 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = get_model(num_classes=num_classes, device=device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.backbone.fc.parameters(), lr=0.001) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) # 训练循环 num_epochs = 20 for epoch in range(num_epochs): model.train() running_loss = 0.0 correct = 0 total = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() scheduler.step() acc = 100 * correct / total print(f'Epoch [{epoch+1}/{num_epochs}] Loss: {running_loss/len(train_loader):.4f} Acc: {acc:.2f}%') # 保存模型 torch.save(model.state_dict(), 'fruit_veg_model.pth') print('模型训练完成,已保存至 fruit_veg_model.pth')
# app.py - 本地模型推理服务 (Flask) from flask import Flask, request, jsonify from flask_cors import CORS import torch import torchvision.transforms as transforms from PIL import Image import io from model import get_model app = Flask(__name__) CORS(app) # 类别名称映射 CLASS_NAMES = [ '苹果', '香蕉', '橙子', '葡萄', '西瓜', '草莓', '菠萝', '芒果', '西红柿', '黄瓜', '胡萝卜', '土豆', '青椒', '茄子', '白菜' ] # 加载模型 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = get_model(num_classes=len(CLASS_NAMES), device=device) model.load_state_dict(torch.load('fruit_veg_model.pth', map_location=device)) model.eval() # 图像预处理 transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) @app.route('/predict', methods=['POST']) def predict(): if 'image' not in request.files: return jsonify({'success': False, 'message': '未上传图片'}), 400 file = request.files['image'] img = Image.open(io.BytesIO(file.read())).convert('RGB') img_tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(img_tensor) probabilities = torch.softmax(outputs, dim=1) confidence, predicted = probabilities.max(1) result = CLASS_NAMES[predicted.item()] return jsonify({ 'success': True, 'result': result, 'confidence': confidence.item() }) @app.route('/health') def health(): return jsonify({'status': 'ok', 'model': 'loaded'}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)
# xunfei_api.py - 讯飞星火图像识别 API 调用 import requests import base64 import json import time import hmac import hashlib from urllib.parse import urlencode # 讯飞 API 配置(从环境变量读取,避免硬编码) APP_ID = os.environ.get('XUNFEI_APP_ID') API_KEY = os.environ.get('XUNFEI_API_KEY') API_SECRET = os.environ.get('XUNFEI_API_SECRET') class XunfeiRecognizer: """讯飞星火图像识别客户端""" def __init__(self): self.app_id = APP_ID self.api_key = API_KEY self.api_secret = API_SECRET self.base_url = "https://spark-api.cn-huabei-1.xf-yun.com/v2.1/image" def _generate_auth_url(self): """生成鉴权 URL""" from datetime import datetime from email.utils import formatdate from time import mktime host = "spark-api.cn-huabei-1.xf-yun.com" path = "/v2.1/image" now = datetime.now() date = formatdate(timeval=mktime(now.timetuple()), localtime=False, usegmt=True) signature_origin = f"host: {host}\ndate: {date}\nPOST {path} HTTP/1.1" signature_sha = hmac.new( self.api_secret.encode('utf-8'), signature_origin.encode('utf-8'), digestmod=hashlib.sha256 ).digest() signature = base64.b64encode(signature_sha).decode() authorization_origin = ( f'api_key="{self.api_key}", algorithm="hmac-sha256", ' f'headers="host date request-line", signature="{signature}"' ) authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode() params = urlencode({"authorization": authorization, "date": date, "host": host}) return f"{self.base_url}?{params}" def recognize(self, image_bytes): """识别果蔬图片""" img_base64 = base64.b64encode(image_bytes).decode('utf-8') payload = { "header": { "app_id": self.app_id, "uid": "fruit_veg_recognizer" }, "parameter": { "chat": { "domain": "image", "temperature": 0.5, "max_tokens": 512 } }, "payload": { "message": { "text": [ {"role": "user", "content": "请识别这张图片中的果蔬种类,只返回名称"}, {"role": "user", "content": img_base64} ] } } } url = self._generate_auth_url() response = requests.post(url, json=payload, timeout=30) result = response.json() if result.get("header", {}).get("code") == 0: answer = result["payload"]["message"]["text"][0]["content"] return {"success": True, "result": answer.strip()} else: return {"success": False, "message": result.get("header", {}).get("message", "识别失败")}
# opencv_processing.py - OpenCV 经典算法实现 import cv2 import numpy as np def face_detection(img, scale_factor=1.1, min_neighbors=5, min_size=30): """人脸检测(Haar 级联分类器)""" result = img.copy() gray = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY) face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) faces = face_cascade.detectMultiScale( gray, scaleFactor=scale_factor, minNeighbors=min_neighbors, minSize=(min_size, min_size) ) for (x, y, w, h) in faces: cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2) return result, len(faces) def image_blur(img, blur_type='gaussian', kernel_size=5, sigma=0): """图像模糊:高斯/中值/双边""" if kernel_size % 2 == 0: kernel_size += 1 if blur_type == 'gaussian': return cv2.GaussianBlur(img, (kernel_size, kernel_size), sigmaX=sigma) elif blur_type == 'median': return cv2.medianBlur(img, kernel_size) elif blur_type == 'bilateral': return cv2.bilateralFilter(img, d=kernel_size, sigmaColor=75, sigmaSpace=75) def contour_detection(img, threshold1=50, threshold2=150, min_area=100): """轮廓检测(Canny + findContours)""" result = img.copy() gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) edged = cv2.Canny(gray, threshold1, threshold2) contours, _ = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) valid = [c for c in contours if cv2.contourArea(c) > min_area] cv2.drawContours(result, valid, -1, (0, 255, 0), 2) return result, len(valid) def edge_detection(img, method='canny', threshold1=50, threshold2=150, ksize=3): """边缘检测:Canny/Sobel/Laplacian""" gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if method == 'canny': return cv2.cvtColor(cv2.Canny(gray, threshold1, threshold2), cv2.COLOR_GRAY2BGR) elif method == 'sobel': sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=ksize) sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=ksize) sobel = np.uint8(np.sqrt(sobelx**2 + sobely**2) / np.sqrt(sobelx**2 + sobely**2).max() * 255) return cv2.cvtColor(sobel, cv2.COLOR_GRAY2BGR) elif method == 'laplacian': lap = cv2.Laplacian(gray, cv2.CV_64F, ksize=ksize) return cv2.cvtColor(np.uint8(np.absolute(lap)), cv2.COLOR_GRAY2BGR)
# app.py - OpenCV Flask API 服务(端口 5001) from flask import Flask, request, jsonify from flask_cors import CORS import opencv_processing as cv_proc app = Flask(__name__) CORS(app) @app.route('/api/health', methods=['GET']) def health_check(): return jsonify({'success': True, 'service': 'opencv-service', 'status': 'online'}) @app.route('/api/opencv/face-detect', methods=['POST']) def face_detect(): img = cv_proc.decode_image(request.files['image']) scale = float(request.form.get('scale_factor', 1.1)) neighbors = int(request.form.get('min_neighbors', 5)) min_size = int(request.form.get('min_size', 30)) result, count = cv_proc.face_detection(img, scale, neighbors, min_size) return jsonify({ 'success': True, 'face_count': count, 'image': cv_proc.encode_image(result) }) # 同理实现 /api/opencv/blur、/api/opencv/contour、/api/opencv/edge if __name__ == '__main__': app.run(host='0.0.0.0', port=5001, debug=False)
// 实时摄像头人脸检测 - 核心代码(纯前端,基于 face-api.js) // 1. 加载模型(从 CDN 加载预训练权重) const MODEL_URL = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/weights'; async function loadModels() { await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL); await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL); await faceapi.nets.faceExpressionNet.loadFromUri(MODEL_URL); await faceapi.nets.ageGenderNet.loadFromUri(MODEL_URL); } // 2. 启动摄像头 async function startCamera() { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 1280 } }, audio: false }); video.srcObject = stream; video.onloadedmetadata = () => { const displaySize = { width: video.videoWidth, height: video.videoHeight }; faceapi.matchDimensions(canvas, displaySize); detectionLoop(video, canvas, displaySize); }; } // 3. 实时检测循环 async function detectionLoop(video, canvas, displaySize) { if (!isRunning) return; const options = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.4 }); // 同时检测人脸、关键点、表情、年龄性别 const detections = await faceapi.detectAllFaces(video, options) .withFaceLandmarks() .withFaceExpressions() .withAgeAndGender(); const resized = faceapi.resizeResults(detections, displaySize); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); // 4. 绘制人脸框和关键点 faceapi.draw.drawDetections(canvas, resized); faceapi.draw.drawFaceLandmarks(canvas, resized); // 5. 绘制表情、年龄、性别标签 resized.forEach(det => { const exp = Object.entries(det.expressions) .sort((a,b) => b[1]-a[1])[0]; const label = `${(det.detection.score*100).toFixed(0)}% | ${exp[0]} | ${det.gender} ${Math.round(det.age)}岁`; new faceapi.draw.DrawBox(det.detection.box, { label }).draw(canvas); }); // 继续下一帧 setTimeout(() => detectionLoop(video, canvas, displaySize), 0); }
# text_analyzer.py - NLP 文本分析核心模块 import jieba import jieba.analyse import jieba.posseg as pseg from snownlp import SnowNLP from collections import Counter def get_word_frequency(text, top_n=50): """词频统计""" words = jieba.lcut(text) filtered = [w for w in words if len(w) > 1 and w not in STOP_WORDS] return Counter(filtered).most_common(top_n) def extract_keywords(text, top_n=20): """关键词提取(TF-IDF 和 TextRank)""" tfidf = jieba.analyse.extract_tags(text, topK=top_n, withWeight=True) textrank = jieba.analyse.textrank(text, topK=top_n, withWeight=True) return {"tfidf": tfidf, "textrank": textrank} def sentiment_analysis(text): """情感分析(按句子和段落)""" sentences = re.split(r'[。!?;\n]', text) sentence_sentiments = [] for sent in sentences: if len(sent) > 1: s = SnowNLP(sent) sentence_sentiments.append({ "sentiment": round(s.sentiments, 4), "label": "积极" if s.sentiments > 0.6 else ("消极" if s.sentiments < 0.4 else "中性") }) overall = SnowNLP(text).sentiments return {"overall": overall, "sentences": sentence_sentiments} def named_entity_recognition(text): """命名实体识别(基于jieba词性标注)""" words = pseg.cut(text) entities = {"person": [], "location": [], "organization": [], "time": []} entity_map = {"nr": "person", "ns": "location", "nt": "organization", "t": "time"} for word, flag in words: if flag in entity_map and word not in entities[entity_map[flag]]: entities[entity_map[flag]].append(word) return entities def text_statistics(text): """文本统计信息""" chinese_chars = len(re.findall(r'[\u4e00-\u9fa5]', text)) sentences = max(len(re.split(r'[。!?;]', text)) - 1, 1) words = jieba.lcut(text) unique_words = len(set([w for w in words if len(w) > 1])) total_words = len([w for w in words if len(w) > 1]) return { "chinese_chars": chinese_chars, "sentences": sentences, "avg_sentence_length": round(chinese_chars / sentences, 1), "lexical_diversity": round(unique_words / total_words * 100, 1), "read_time": round(chinese_chars / 300, 1) }
# app.py - NLP Flask API 服务(端口 5002) from flask import Flask, request, jsonify from flask_cors import CORS import text_analyzer import sample_texts app = Flask(__name__) CORS(app) @app.route('/api/nlp/analyze', methods=['POST']) def analyze(): data = request.get_json() text = data['text'].strip() if len(text) > 50000: text = text[:50000] result = text_analyzer.analyze_text(text) return jsonify({'success': True, 'data': result}) @app.route('/api/nlp/samples', methods=['GET']) def get_samples(): return jsonify({'success': True, 'samples': sample_texts.get_sample_list()}) @app.route('/api/nlp/samples/' , methods=['GET']) def get_sample(sample_id): sample = sample_texts.get_sample_by_id(sample_id) return jsonify({'success': True, 'sample': sample}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5002, debug=False)
# frpc.toml - FRP 客户端配置(本地 Ubuntu 主机) # 将本地 5000 端口的模型服务通过 FRP 暴露到公网 serverAddr = "39.106.104.13" # 阿里云服务器公网 IP serverPort = 7000 # FRP 服务端端口 # 鉴权(可选,建议配置) auth.method = "token" auth.token = "your_frp_token_here" # 日志配置 log.to = "./frpc.log" log.level = "info" log.maxDays = 7 # 代理配置:将本地模型服务暴露到公网 [[proxies]] name = "fruit-veg-model" type = "tcp" localIP = "127.0.0.1" localPort = 5000 # 本地 Flask 服务端口 remotePort = 6000 # 公网访问端口(阿里云服务器上) # 可选:通过自定义域名访问(需要 Nginx 配合) # [[proxies]] # name = "fruit-veg-web" # type = "http" # localIP = "127.0.0.1" # localPort = 5000 # customDomains = ["model.ztz101.top"]
# frps.toml - FRP 服务端配置(阿里云服务器) # 绑定端口(客户端连接端口) bindPort = 7000 # 鉴权(与客户端一致) auth.method = "token" auth.token = "your_frp_token_here" # 允许客户端映射的端口范围 allowPorts = [ { start = 6000, end = 6010 } ] # Dashboard(Web 管理界面,可选) webServer.addr = "0.0.0.0" webServer.port = 7500 webServer.user = "admin" webServer.password = "your_dashboard_password" # 日志配置 log.to = "./frps.log" log.level = "info" log.maxDays = 7 # 最大连接数限制 maxPoolCount = 5 # 心跳配置 heartbeatTimeout = 90
ztzjl/
├── frontend/ # 前端静态文件(部署到阿里云 Nginx)
│ ├── index.html # 首页/简历
│ ├── projects.html # 项目展示
│ ├── code.html # 代码展示
│ ├── demo.html # 果蔬识别演示
│ ├── opencv.html # OpenCV 实践
│ ├── realtime.html # 实时人脸检测
│ ├── nlp.html # NLP 文本分析
│ ├── ip.html # 访问记录
│ ├── css/style.css
│ └── js/main.js
├── backend/ # Node.js 后端服务(部署到阿里云)
│ ├── server.js # Express 主入口
│ ├── package.json
│ ├── routes/
│ │ ├── recognize.js # 识别接口(讯飞API/CNN转发)
│ │ └── visits.js # IP 访问记录
│ └── data/visits.json # 访问数据存储
├── python-service/ # Python OpenCV 服务(部署到阿里云)
│ ├── app.py # Flask API 主入口
│ ├── opencv_processing.py # 算法实现
│ ├── requirements.txt
│ └── Dockerfile
├── nlp-service/ # NLP 文本分析服务(部署到阿里云)
│ ├── app.py # Flask API 主入口
│ ├── text_analyzer.py # 文本分析核心
│ ├── sample_texts.py # 示例文本
│ ├── requirements.txt
│ └── Dockerfile
├── deploy/ # 部署配置
│ ├── nginx.conf # Nginx 配置
│ ├── frps.toml # FRP 服务端配置
│ └── deploy.md # 部署文档
├── docker-compose.yml # Docker 编排配置
└── 果蔬识别系统/ # 本地模型代码(留在本地主机)
├── model.py
├── train.py
├── app.py
└── frpc.toml