端侧照片模糊检测系统:多类别分类(清晰 / 运动模糊 / 失焦模糊 / 高斯模糊),使用 ONNX Runtime 进行推理。
| 项目 | 说明 |
|---|---|
| Backbone | MobileNetV3-Large (~5.4M 参数) |
| 输入 | 224×224 RGB 图像,ImageNet 归一化 |
| 输出 | 4 类 logits(推理端执行 softmax) |
| 准确率 | 验证集 98.84%,测试集 97.80% (TTA) |
| 模型格式 | ONNX (12MB) |
| ID | 类别 | 说明 |
|---|---|---|
| 0 | defocus_blur | 失焦模糊(对焦不准) |
| 1 | gaussian_blur | 高斯模糊(整体柔和) |
| 2 | motion_blur | 运动模糊(相机/物体移动) |
| 3 | sharp | 清晰图像 |
⚠️ 类别顺序按torchvision.datasets.ImageFolder字母排序,与模型输出索引对应。不要按业务展示顺序重排。
cd training
# 安装依赖
pip install -r requirements.txt
# 准备数据集(合成模糊图像,每张源图生成 15 个变体)
python data/prepare_dataset.py --source data/sharp_images --output data/datasets --variants 15
# 训练
python train.py --data-dir data/datasets --epochs 100
# 评估
python evaluate.py --model output/blur_detector_best.pth --data-dir data/datasets
# 导出 ONNX 模型
python export.py --model output/blur_detector_best.pth --output-dir ../models# 安装 ONNX Runtime
brew install onnxruntime
# 构建
cmake -B build -DONNXRUNTIME_ROOT=/opt/homebrew/opt/onnxruntime
cmake --build build
# 运行(backend 默认是 onnx)
./build/blur_detection --model models/blur_detector.onnx --input photo.jpg
# 跑单元测试
ctest --test-dir build --output-on-failure# 单图检测
./build/blur_detection --model models/blur_detector.onnx --input photo.jpg
# 批量检测
./build/blur_detection --model models/blur_detector.onnx --input ./photos/
# JSON 输出
./build/blur_detection --model models/blur_detector.onnx --input photo.jpg --json输出示例:
photo.jpg: motion_blur (confidence: 96.16%) [0.013, 0.013, 0.962, 0.013]
概率数组顺序为 [defocus_blur, gaussian_blur, motion_blur, sharp]。
使用 onnxruntime-swift 官方库:
import onnxruntime_objc
class BlurDetector {
private var session: ORTSession?
private let classes = ["defocus_blur", "gaussian_blur", "motion_blur", "sharp"]
init(modelPath: String) throws {
let env = try ORTEnv(loggingLevel: .warning)
let options = try ORTSessionOptions()
options.setIntraOpNumThreads(4)
session = try ORTSession(env: env, modelPath: modelPath, sessionOptions: options)
}
func detect(image: CGImage) throws -> (className: String, confidence: Float, probabilities: [Float]) {
// 1. 预处理: resize 到 224x224, 转换为 CHW float32, ImageNet 归一化
let inputData = preprocessImage(image)
// 2. 创建输入 tensor
let inputTensor = try ORTValue(
tensorData: NSMutableData(data: inputData),
elementType: .float,
shape: [1, 3, 224, 224]
)
// 3. 推理
let outputs = try session?.run(
withInputs: ["input": inputTensor],
outputNames: ["output"]
)
// 4. 获取 logits
let outputTensor = outputs?["output"]
let outputData = try outputTensor?.tensorData() as Data?
let logits = outputData?.withUnsafeBytes {
Array($0.bindMemory(to: Float.self))
} ?? []
// 5. Softmax + argmax
let maxVal = logits.max() ?? 0
let expValues = logits.map { exp($0 - maxVal) }
let sum = expValues.reduce(0, +)
let probs = expValues.map { $0 / sum }
let maxIndex = probs.enumerated().max(by: { $0.element < $1.element })?.offset ?? 0
return (classes[maxIndex], probs[maxIndex], probs)
}
private func preprocessImage(_ image: CGImage) -> Data {
let width = 224
let height = 224
// 1. 绘制到 224x224 RGB 画布
guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
let context = CGContext(
data: nil,
width: width, height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue |
CGImageAlphaInfo.noneSkipFirst.rawValue
) else {
return Data()
}
context.interpolationQuality = .high
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let pixelData = context.data else { return Data() }
let pixels = pixelData.bindMemory(to: UInt8.self, capacity: width * height * 4)
// 2. BGRA -> RGB, HWC -> CHW + ImageNet 归一化
let mean: [Float] = [0.485, 0.456, 0.406]
let std: [Float] = [0.229, 0.224, 0.225]
let channelSize = width * height
var floatData = [Float](repeating: 0, count: 3 * channelSize)
for i in 0..<channelSize {
let offset = i * 4 // BGRA 布局
let r = Float(pixels[offset + 2]) / 255.0
let g = Float(pixels[offset + 1]) / 255.0
let b = Float(pixels[offset + 0]) / 255.0
floatData[0 * channelSize + i] = (r - mean[0]) / std[0]
floatData[1 * channelSize + i] = (g - mean[1]) / std[1]
floatData[2 * channelSize + i] = (b - mean[2]) / std[2]
}
return Data(bytes: floatData, count: floatData.count * MemoryLayout<Float>.stride)
}
}
// 使用
let detector = try BlurDetector(modelPath: "blur_detector.onnx")
let result = try detector.detect(image: uiImage.cgImage!)
print("检测结果: \(result.className), 置信度: \(result.confidence)")Podfile:
pod 'onnxruntime-objc', '~> 1.16'Swift 中 import onnxruntime_objc,Objective-C 中 #import <onnxruntime_objc/onnxruntime_objc.h>。
Swift Package Manager:
dependencies: [
.package(url: "https://github.com/microsoft/onnxruntime.git", from: "1.16.0")
]#import <onnxruntime_objc/onnxruntime_objc.h>
#import <math.h>
@interface BlurDetector : NSObject
- (instancetype)initWithModelPath:(NSString *)modelPath error:(NSError **)error;
- (NSDictionary *)detectWithImage:(CGImageRef)image error:(NSError **)error;
@end
@implementation BlurDetector {
ORTSession *_session;
NSArray<NSString *> *_classes;
}
- (instancetype)initWithModelPath:(NSString *)modelPath error:(NSError **)error {
self = [super init];
if (self) {
_classes = @[@"defocus_blur", @"gaussian_blur", @"motion_blur", @"sharp"];
ORTEnv *env = [[ORTEnv alloc] initWithLoggingLevel:ORTLoggingLevelWarning error:error];
ORTSessionOptions *options = [[ORTSessionOptions alloc] init];
[options setIntraOpNumThreads:4 error:error];
_session = [[ORTSession alloc] initWithEnv:env modelPath:modelPath sessionOptions:options error:error];
}
return self;
}
- (NSDictionary *)detectWithImage:(CGImageRef)image error:(NSError **)error {
// 1. 预处理图像
NSData *inputData = [self preprocessImage:image];
// 2. 创建输入 tensor
ORTValue *inputTensor = [[ORTValue alloc] initWithTensorData:[NSMutableData dataWithData:inputData]
elementType:ORTTensorElementDataTypeFloat
shape:@[@1, @3, @224, @224]
error:error];
// 3. 推理
NSDictionary<NSString *, ORTValue *> *outputs = [_session runWithInputs:@{@"input": inputTensor}
outputNames:@[@"output"]
error:error];
// 4. 解析 logits 并执行 softmax
ORTValue *outputTensor = outputs[@"output"];
NSData *outputData = [outputTensor tensorDataWithError:error];
const float *logits = (const float *)outputData.bytes;
float maxVal = logits[0];
for (int i = 1; i < 4; i++) {
if (logits[i] > maxVal) {
maxVal = logits[i];
}
}
float probs[4];
float sum = 0.0f;
for (int i = 0; i < 4; i++) {
probs[i] = expf(logits[i] - maxVal);
sum += probs[i];
}
for (int i = 0; i < 4; i++) {
probs[i] /= sum;
}
// 5. 找到最大概率
int maxIdx = 0;
float maxProb = probs[0];
for (int i = 1; i < 4; i++) {
if (probs[i] > maxProb) {
maxProb = probs[i];
maxIdx = i;
}
}
return @{
@"className": _classes[maxIdx],
@"confidence": @(maxProb),
@"probabilities": @[@(probs[0]), @(probs[1]), @(probs[2]), @(probs[3])]
};
}
- (NSData *)preprocessImage:(CGImageRef)image {
const int width = 224;
const int height = 224;
// 1. 绘制到 224x224 RGB 画布
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
CGContextRef context = CGBitmapContextCreate(
NULL, width, height, 8, width * 4,
colorSpace,
kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst
);
CGColorSpaceRelease(colorSpace);
if (!context) return [NSData data];
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
const uint8_t *pixels = CGBitmapContextGetData(context);
if (!pixels) {
CGContextRelease(context);
return [NSData data];
}
// 2. BGRA -> RGB, HWC -> CHW + ImageNet 归一化
const float mean[3] = {0.485f, 0.456f, 0.406f};
const float std[3] = {0.229f, 0.224f, 0.225f};
const int channelSize = width * height;
NSMutableData *outputData = [NSMutableData dataWithLength:3 * channelSize * sizeof(float)];
float *floatBuffer = (float *)outputData.mutableBytes;
for (int i = 0; i < channelSize; i++) {
int offset = i * 4; // BGRA 布局
float r = pixels[offset + 2] / 255.0f;
float g = pixels[offset + 1] / 255.0f;
float b = pixels[offset + 0] / 255.0f;
floatBuffer[0 * channelSize + i] = (r - mean[0]) / std[0];
floatBuffer[1 * channelSize + i] = (g - mean[1]) / std[1];
floatBuffer[2 * channelSize + i] = (b - mean[2]) / std[2];
}
CGContextRelease(context);
return outputData;
}
@end使用 onnxruntime-android 官方库:
build.gradle:
dependencies {
implementation 'com.microsoft.onnxruntime:onnxruntime-android:1.16.0'
}BlurDetector.kt:
import ai.onnxruntime.*
import android.graphics.Bitmap
import java.nio.FloatBuffer
class BlurDetector(context: Context, modelPath: String) {
private val env = OrtEnvironment.getEnvironment()
private val session: OrtSession
private val classes = arrayOf("defocus_blur", "gaussian_blur", "motion_blur", "sharp")
init {
val options = OrtSession.SessionOptions()
options.setIntraOpNumThreads(4)
val modelBytes = context.assets.open(modelPath).readBytes()
session = env.createSession(modelBytes, options)
}
fun detect(bitmap: Bitmap): Pair<String, Float> {
// 1. 预处理
val inputTensor = preprocessImage(bitmap)
// 2. 推理
val results = session.run(mapOf("input" to inputTensor))
// 3. 获取输出
val output = (results[0].value as Array<FloatArray>)[0]
// 4. Softmax
val maxVal = output.max()
val expValues = output.map { Math.exp((it - maxVal).toDouble()).toFloat() }
val sum = expValues.sum()
val probs = expValues.map { it / sum }
// 5. Argmax
val maxIndex = probs.indices.maxByOrNull { probs[it] } ?: 0
return Pair(classes[maxIndex], probs[maxIndex])
}
private fun preprocessImage(bitmap: Bitmap): OnnxTensor {
val resized = Bitmap.createScaledBitmap(bitmap, 224, 224, true)
val floatBuffer = FloatBuffer.allocate(1 * 3 * 224 * 224)
val mean = floatArrayOf(0.485f, 0.456f, 0.406f)
val std = floatArrayOf(0.229f, 0.224f, 0.225f)
// HWC -> CHW, 归一化
for (c in 0 until 3) {
for (y in 0 until 224) {
for (x in 0 until 224) {
val pixel = resized.getPixel(x, y)
val value = when (c) {
0 -> (android.graphics.Color.red(pixel) / 255.0f - mean[c]) / std[c]
1 -> (android.graphics.Color.green(pixel) / 255.0f - mean[c]) / std[c]
else -> (android.graphics.Color.blue(pixel) / 255.0f - mean[c]) / std[c]
}
floatBuffer.put(value)
}
}
}
floatBuffer.rewind()
return OnnxTensor.createTensor(env, floatBuffer, longArrayOf(1, 3, 224, 224))
}
fun close() {
session.close()
env.close()
}
}
// 使用
val detector = BlurDetector(context, "blur_detector.onnx")
val (className, confidence) = detector.detect(bitmap)
println("检测结果: $className, 置信度: $confidence")
detector.close()将模型放入 assets/ 目录:
app/src/main/assets/blur_detector.onnx
使用 onnxruntime 插件:
pubspec.yaml:
dependencies:
onnxruntime: ^1.4.0
image: ^4.1.0blur_detector.dart:
import 'dart:typed_data';
import 'dart:io';
import 'dart:math' as math;
import 'package:onnxruntime/onnxruntime.dart';
import 'package:image/image.dart' as img;
class BlurDetector {
late OrtSession _session;
final List<String> classes = [
'defocus_blur', 'gaussian_blur', 'motion_blur', 'sharp'
];
Future<void> init(String modelPath) async {
final sessionOptions = OrtSessionOptions();
sessionOptions.setIntraOpNumThreads(4);
final session = OrtSession.fromFile(
await File(modelPath).readAsBytes(),
sessionOptions,
);
_session = session;
}
Future<Map<String, dynamic>> detect(Uint8List imageBytes) async {
// 1. 解码图像
final image = img.decodeImage(imageBytes)!;
// 2. 预处理
final inputData = _preprocessImage(image);
// 3. 创建输入 tensor
final inputTensor = OrtValueTensor.createTensorWithDataList(
inputData,
[1, 3, 224, 224],
);
// 4. 推理
final inputs = {'input': inputTensor};
final outputs = await _session.runAsync(OrtRunOptions(), inputs);
// 5. 获取结果
final outputTensor = outputs[0];
final outputData = outputTensor.value as List<double>;
// 6. Softmax
final maxVal = outputData.reduce((a, b) => a > b ? a : b);
final expValues = outputData.map((v) => math.exp(v - maxVal)).toList();
final sum = expValues.reduce((a, b) => a + b);
final probs = expValues.map((v) => v / sum).toList();
// 7. Argmax
final maxIndex = probs.indexOf(probs.reduce((a, b) => a > b ? a : b));
inputTensor.release();
outputTensor.release();
return {
'className': classes[maxIndex],
'confidence': probs[maxIndex],
'probabilities': probs,
};
}
Float32List _preprocessImage(img.Image image) {
// Resize 到 224x224
final resized = img.copyResize(image, width: 224, height: 224);
final inputData = Float32List(1 * 3 * 224 * 224);
final mean = [0.485, 0.456, 0.406];
final std = [0.229, 0.224, 0.225];
int index = 0;
for (int c = 0; c < 3; c++) {
for (int y = 0; y < 224; y++) {
for (int x = 0; x < 224; x++) {
final pixel = resized.getPixel(x, y);
final value = switch (c) {
0 => (pixel.r / 255.0 - mean[c]) / std[c],
1 => (pixel.g / 255.0 - mean[c]) / std[c],
_ => (pixel.b / 255.0 - mean[c]) / std[c],
};
inputData[index++] = value;
}
}
}
return inputData;
}
void dispose() {
_session.release();
}
}
// 使用
final detector = BlurDetector();
await detector.init('assets/blur_detector.onnx');
final result = await detector.detect(imageBytes);
print('检测结果: ${result['className']}, 置信度: ${result['confidence']}');
detector.dispose();import onnxruntime as ort
import numpy as np
from PIL import Image
class BlurDetector:
def __init__(self, model_path: str):
self.session = ort.InferenceSession(model_path)
self.classes = ['defocus_blur', 'gaussian_blur', 'motion_blur', 'sharp']
self.mean = np.array([0.485, 0.456, 0.406])
self.std = np.array([0.229, 0.224, 0.225])
def detect(self, image_path: str) -> dict:
# 加载并预处理
image = Image.open(image_path).convert('RGB')
image = image.resize((224, 224))
img_np = np.array(image).astype(np.float32) / 255.0
img_np = (img_np - self.mean) / self.std
# HWC -> CHW, 添加 batch 维度
input_data = np.transpose(img_np, (2, 0, 1))[np.newaxis, ...]
# 推理
outputs = self.session.run(None, {'input': input_data})
probs = self._softmax(outputs[0][0])
# 结果
max_idx = np.argmax(probs)
return {
'className': self.classes[max_idx],
'confidence': float(probs[max_idx]),
'probabilities': probs.tolist()
}
def _softmax(self, x):
e_x = np.exp(x - np.max(x))
return e_x / e_x.sum()
# 使用
detector = BlurDetector('models/blur_detector.onnx')
result = detector.detect('photo.jpg')
print(f"检测结果: {result['className']}, 置信度: {result['confidence']:.2%}")使用 onnxruntime-web:
import * as ort from 'onnxruntime-web';
class BlurDetector {
constructor() {
this.classes = ['defocus_blur', 'gaussian_blur', 'motion_blur', 'sharp'];
this.mean = [0.485, 0.456, 0.406];
this.std = [0.229, 0.224, 0.225];
}
async init(modelPath) {
this.session = await ort.InferenceSession.create(modelPath);
}
async detect(imageElement) {
// 1. 预处理
const inputData = this.preprocessImage(imageElement);
// 2. 创建 tensor
const inputTensor = new ort.Tensor('float32', inputData, [1, 3, 224, 224]);
// 3. 推理
const results = await this.session.run({ input: inputTensor });
const output = results.output.data;
// 4. Softmax
const probs = this.softmax(Array.from(output));
// 5. Argmax
const maxIdx = probs.indexOf(Math.max(...probs));
return {
className: this.classes[maxIdx],
confidence: probs[maxIdx],
probabilities: probs
};
}
preprocessImage(imageElement) {
const canvas = document.createElement('canvas');
canvas.width = 224;
canvas.height = 224;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageElement, 0, 0, 224, 224);
const imageData = ctx.getImageData(0, 0, 224, 224);
const data = imageData.data;
const inputData = new Float32Array(3 * 224 * 224);
for (let c = 0; c < 3; c++) {
for (let y = 0; y < 224; y++) {
for (let x = 0; x < 224; x++) {
const pixelIdx = (y * 224 + x) * 4;
const value = (data[pixelIdx + c] / 255.0 - this.mean[c]) / this.std[c];
inputData[c * 224 * 224 + y * 224 + x] = value;
}
}
}
return inputData;
}
softmax(arr) {
const maxVal = Math.max(...arr);
const exps = arr.map(v => Math.exp(v - maxVal));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map(v => v / sum);
}
}
// 使用
const detector = new BlurDetector();
await detector.init('blur_detector.onnx');
const result = await detector.detect(document.getElementById('photo'));
console.log(`检测结果: ${result.className}, 置信度: ${(result.confidence * 100).toFixed(2)}%`);使用 react-native-onnx 或通过 Native Module 调用原生 ONNX Runtime。
所有平台的预处理必须一致:
- Resize: 将图像缩放到 224×224
- 色彩空间: 转换为 RGB(去除 Alpha 通道)
- 数据布局: HWC → CHW(通道优先)
- 归一化:
(pixel / 255.0 - mean) / std- mean =
[0.485, 0.456, 0.406] - std =
[0.229, 0.224, 0.225]
- mean =
- 数据类型: float32
- 输入形状:
[1, 3, 224, 224] - 输出处理: ONNX 输出是 logits,推理端对 4 个输出值执行一次 softmax,再取 argmax
├── CMakeLists.txt # C++ 构建配置
├── models/
│ └── blur_detector.onnx # 训练好的模型
├── training/ # Python 训练管线
│ ├── data/
│ │ ├── blur_kernels.py # 模糊核生成
│ │ └── prepare_dataset.py # 数据集准备
│ ├── model/
│ │ ├── config.py # 训练配置
│ │ └── network.py # MobileNetV3 模型
│ ├── train.py # 训练入口
│ ├── evaluate.py # 评估脚本
│ └── export.py # 导出模型
├── include/blur_detection/ # C++ 头文件
├── src/ # C++ 源文件
├── apps/main.cpp # CLI 工具
└── tests/ # 单元测试
- PyTorch >= 2.0
- torchvision >= 0.15
- onnx >= 1.14
- ONNX Runtime (
brew install onnxruntime) - CMake >= 3.16
- C++17 编译器
MIT