项目核心代码

本项目包含 CNN 模型训练、模型推理服务、讯飞 API 调用、FRP 内网穿透配置等核心模块。 点击下方标签切换查看不同模块的代码。

CNN 模型定义
模型训练
推理服务 (Flask)
讯飞 API 调用
OpenCV 算法实现
OpenCV API 服务
实时人脸检测
NLP 文本分析
NLP API 服务
FRP 客户端配置
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