PHP项目中的图像分类与ResNet实现
在PHP项目中实现基于ResNet的图像分类,通常有几种可行的方案,让我详细介绍各种方法:

使用PHP扩展 + 预训练模型
安装PHP-ML扩展与ONNX Runtime
// 使用ONNX Runtime PHP扩展
composer require onnxruntime/onnxruntime
// 加载ResNet模型进行推理
use ONNX\Runtime;
$runtime = new Runtime();
$model = $runtime->loadModel('resnet50.onnx');
// 图像预处理
function preprocessImage($imagePath) {
$image = imagecreatefromjpeg($imagePath);
$resized = imagescale($image, 224, 224);
// 转换为RGB数组
$pixels = [];
for ($y = 0; $y < 224; $y++) {
for ($x = 0; $x < 224; $x++) {
$rgb = imagecolorat($resized, $x, $y);
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
// ImageNet标准化
$pixels[] = ($r / 255 - 0.485) / 0.229;
$pixels[] = ($g / 255 - 0.456) / 0.224;
$pixels[] = ($b / 255 - 0.406) / 0.225;
}
}
return $pixels;
}
// 执行分类
$input = preprocessImage('test.jpg');
$result = $model->predict([$input]);
$classId = array_search(max($result), $result);
echo "预测类别ID: " . $classId;
使用Python微服务架构(推荐)
这是最实用的方案,将模型推理部署为独立服务:
Python端 (Flask服务)
# app.py
from flask import Flask, request, jsonify
from PIL import Image
import torch
import torchvision.transforms as transforms
import torchvision.models as models
import numpy as np
import io
app = Flask(__name__)
# 加载预训练ResNet模型
model = models.resnet50(pretrained=True)
model.eval()
# 图像预处理
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 加载ImageNet类别标签
with open('imagenet_classes.txt') as f:
labels = [line.strip() for line in f.readlines()]
@app.route('/classify', methods=['POST'])
def classify_image():
if 'image' not in request.files:
return jsonify({'error': 'No image uploaded'}), 400
file = request.files['image']
image = Image.open(io.BytesIO(file.read())).convert('RGB')
# 预处理
input_tensor = transform(image).unsqueeze(0)
# 推理
with torch.no_grad():
outputs = model(input_tensor)
_, predicted = outputs.max(1)
probabilities = torch.nn.functional.softmax(outputs[0], dim=0)
# 获取Top-5结果
top5_prob, top5_indices = torch.topk(probabilities, 5)
results = []
for i in range(5):
idx = top5_indices[i].item()
results.append({
'class': labels[idx],
'class_id': idx,
'probability': top5_prob[i].item()
})
return jsonify({'predictions': results})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
PHP端调用
<?php
// PHP调用Python微服务
class ImageClassifier {
private $serviceUrl;
public function __construct($serviceUrl = 'http://localhost:5000') {
$this->serviceUrl = $serviceUrl;
}
public function classify($imagePath) {
$imageData = file_get_contents($imagePath);
$ch = curl_init($this->serviceUrl . '/classify');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'image' => new CURLFile($imagePath)
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
}
// 使用示例
$classifier = new ImageClassifier();
$result = $classifier->classify('cat.jpg');
echo "图像分类结果:\n";
foreach ($result['predictions'] as $prediction) {
echo sprintf(
"%s (%.2f%%)\n",
$prediction['class'],
$prediction['probability'] * 100
);
}
使用TensorFlow Serving + PHP
导出ResNet模型
# 导出为SavedModel格式
import tensorflow as tf
import tensorflow_hub as hub
model = tf.keras.Sequential([
hub.KerasLayer("https://tfhub.dev/google/imagenet/resnet_v2_50/classification/5"),
])
# 保存模型
tf.saved_model.save(model, "resnet_saved_model")
Docker部署TensorFlow Serving
# Dockerfile FROM tensorflow/serving COPY resnet_saved_model /models/resnet/1 ENV MODEL_NAME=resnet
PHP客户端
<?php
// 使用gRPC或HTTP调用TensorFlow Serving
// HTTP REST方式
function classifyWithTFServing($imagePath) {
$image = base64_encode(file_get_contents($imagePath));
$requestData = [
'instances' => [
['b64' => $image]
]
];
$ch = curl_init('http://localhost:8501/v1/models/resnet:predict');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
完整的PHP项目示例
<?php
// ImageClassifier.php - 完整的图像分类类
class ImageClassifier {
private $modelPath;
private $labels;
private $pythonPath;
private $tempDir;
public function __construct($config = []) {
$this->modelPath = $config['model_path'] ?? '/models/resnet50.onnx';
$this->labels = $this->loadLabels($config['labels_path'] ?? 'imagenet_labels.txt');
$this->pythonPath = $config['python_path'] ?? 'python3';
$this->tempDir = $config['temp_dir'] ?? sys_get_temp_dir();
}
public function classify($imagePath, $topK = 5) {
// 验证图像
if (!file_exists($imagePath)) {
throw new Exception("图像文件不存在: $imagePath");
}
// 创建临时Python脚本
$script = $this->createPythonScript($imagePath, $topK);
$scriptPath = $this->tempDir . '/classify_' . uniqid() . '.py';
file_put_contents($scriptPath, $script);
// 执行Python推理
$command = sprintf(
'%s %s 2>&1',
escapeshellcmd($this->pythonPath),
escapeshellarg($scriptPath)
);
$output = shell_exec($command);
unlink($scriptPath);
// 解析结果
$result = json_decode($output, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("解析推理结果失败: " . json_last_error_msg());
}
return $result;
}
private function createPythonScript($imagePath, $topK) {
return <<<PYTHON
import json
import sys
from PIL import Image
import torch
import torchvision.transforms as transforms
import torchvision.models as models
# 加载模型
model = models.resnet50(pretrained=True)
model.eval()
# 图像预处理
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 加载图像
image = Image.open('$imagePath').convert('RGB')
input_tensor = transform(image).unsqueeze(0)
# 推理
with torch.no_grad():
outputs = model(input_tensor)
probabilities = torch.nn.functional.softmax(outputs[0], dim=0)
top_probs, top_indices = torch.topk(probabilities, $topK)
# 加载标签
with open('{$this->labels}', 'r') as f:
labels = [line.strip() for line in f.readlines()]
# 构建结果
results = []
for i in range($topK):
idx = top_indices[i].item()
results.append({
'class': labels[idx],
'class_id': idx,
'probability': top_probs[i].item()
})
print(json.dumps(results))
PYTHON;
}
private function loadLabels($path) {
if (!file_exists($path)) {
// 返回默认标签或下载
return $this->downloadLabels();
}
return $path;
}
private function downloadLabels() {
$url = "https://raw.githubusercontent.com/anishathalye/imagenet-simple-labels/master/imagenet-simple-labels.json";
$path = $this->tempDir . '/imagenet_labels.json';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$data = curl_exec($ch);
curl_close($ch);
file_put_contents($path, $data);
return $path;
}
}
// 使用示例
try {
$classifier = new ImageClassifier();
$results = $classifier->classify('uploads/cat.jpg', 3);
echo "<h2>图像分类结果</h2>";
echo "<table border='1'>";
echo "<tr><th>类别</th><th>置信度</th></tr>";
foreach ($results as $result) {
echo sprintf(
"<tr><td>%s</td><td>%.2f%%</td></tr>",
htmlspecialchars($result['class']),
$result['probability'] * 100
);
}
echo "</table>";
} catch (Exception $e) {
echo "错误: " . $e->getMessage();
}
性能优化建议
- 缓存模型加载:避免每次请求都重新加载模型
- 批量处理:合并多个图像请求进行批量推理
- 使用GPU:如果有GPU资源,确保模型在GPU上运行
- 图像预处理缓存:对相同尺寸的图像缓存预处理结果
- 异步处理:使用消息队列处理图像分类请求
部署建议
# docker-compose.yml
version: '3.8'
services:
php-app:
build: .
ports:
- "8080:80"
volumes:
- ./app:/var/www/html
depends_on:
- classifier-service
classifier-service:
build: ./classifier
ports:
- "5000:5000"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
这种微服务架构是最推荐的方案,它既保留了PHP的优势(Web处理、简单部署),又能充分利用Python的深度学习生态。