Android与Python图像缩放结果不一致问题排查
问题:Python与Android端图像缩放结果不一致导致车道检测模型输出差异
我有一个基于ENet架构的车道检测模型,已转换为TFLite模型。输入图像需缩放到(80, 160, 3)尺寸,但Python(PC端)使用skimage.transform.resize缩放,Android端通过Dart FFI调用OpenCV的C++ resize接口,二者均转为RGB格式、使用线性插值,但缩放后的数值仍存在显著差异,且确认差异仅来自缩放环节。
相关代码
1. Android端C++代码(dart/opencv.cpp)
float* resizeAndConvertToFloat32(char* inputImagePath, int* outSize) { Mat img = imread(inputImagePath); if(img.empty()) { platform_log("Error: Image could not be read"); *outSize = 0; return nullptr; } cvtColor(img, img, COLOR_BGR2RGB); Mat resizedImg; resize(img, resizedImg, Size(160, 80), 0, 0, INTER_LINEAR); Mat floatImg; resizedImg.convertTo(floatImg, CV_32F); int totalSize = floatImg.rows * floatImg.cols * floatImg.channels(); *outSize = totalSize; float* result = (float*)malloc(totalSize * sizeof(float)); if(floatImg.isContinuous()) { memcpy(result, floatImg.data, totalSize * sizeof(float)); } else { int rowSize = floatImg.cols * floatImg.channels(); for(int i = 0; i < floatImg.rows; i++) { memcpy(result + i * rowSize, floatImg.ptr<float>(i), rowSize * sizeof(float)); } } platform_log("Image resized to 160x80x3 and converted to float32 with preserve_range=True. Total elements: %d", totalSize); return result; }
2. Python端代码
import numpy as np import cv2 import tensorflow as tf from skimage.transform import resize def load_tflite_model(model_path): interpreter = tf.lite.Interpreter("/content/enet_model.tflite") interpreter.allocate_tensors() print("Loaded TensorFlow Lite model successfully.") return interpreter def predict_tflite(interpreter, input_data): input_details = interpreter.get_input_details() output_details = interpreter.get_output_details() print("Input data:", input_data) interpreter.set_tensor(input_details[0]['index'], input_data) interpreter.invoke() output_data = interpreter.get_tensor(output_details[0]['index'])*255 return output_data def road_lines(image, interpreter, lanes): h, w = image.shape[:2] image = image.copy() image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) small_img = resize(image, (80, 160), preserve_range=True) small_img = np.array(small_img, dtype=np.float32) small_img = small_img[None, :, :, :] prediction = predict_tflite(interpreter, small_img)[0] lanes.recent_fit.append(prediction) if len(lanes.recent_fit) > 5: lanes.recent_fit = lanes.recent_fit[1:] lanes.avg_fit = np.mean(np.array(lanes.recent_fit), axis=0) lane_avg_uint8 = np.clip(lanes.avg_fit, 0, 255).astype(np.uint8) left_boundary = [] right_boundary = [] threshold_boundary = 128 for y in range(lane_avg_uint8.shape[0]): row = lane_avg_uint8[y] indices = np.where(row > threshold_boundary)[0] if indices.size > 0: left_boundary.append((indices[0], y)) right_boundary.append((indices[-1], y)) scale_x = w / 160.0 scale_y = h / 80.0 left_boundary_scaled = [(int(x * scale_x), int(y * scale_y)) for (x, y) in left_boundary] right_boundary_scaled = [(int(x * scale_x), int(y * scale_y)) for (x, y) in right_boundary] if len(left_boundary_scaled) > 1: cv2.polylines(image, [np.array(left_boundary_scaled, dtype=np.int32)], isClosed=False, color=(0, 255, 0), thickness=5) if len(right_boundary_scaled) > 1: cv2.polylines(image, [np.array(right_boundary_scaled, dtype=np.int32)], isClosed=False, color=(0, 255, 0), thickness=5) print("Lane detection completed.") return image
3. Android端Dart FFI代码(dart/ffi.dart)
import 'dart:ffi'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'dart:io' show Platform, File; import 'package:tflite_flutter/tflite_flutter.dart'; class OpenCVProcessor { late final DynamicLibrary _nativeLib; late final Pointer<Float> Function(Pointer<Utf8>, Pointer<Int32>) _resizeAndConvert; late final void Function(Pointer<Float>) _freeFloatArray; static final OpenCVProcessor _instance = OpenCVProcessor._internal(); factory OpenCVProcessor() { return _instance; } OpenCVProcessor._internal() { _loadLibrary(); } void _loadLibrary() { _nativeLib = Platform.isAndroid ? DynamicLibrary.open('libmy_functions.so') : DynamicLibrary.process(); _resizeAndConvert = _nativeLib .lookup< NativeFunction< Pointer<Float> Function(Pointer<Utf8>, Pointer<Int32>)>>('resizeAndConvertToFloat32') .asFunction<Pointer<Float> Function(Pointer<Utf8>, Pointer<Int32>)>(); _freeFloatArray = _nativeLib .lookup<NativeFunction<Void Function(Pointer<Float>)>>('freeFloatArray') .asFunction<void Function(Pointer<Float>)>(); } Future<Float32List?> processImage(String imagePath) async { final imagePathPointer = imagePath.toNativeUtf8(); final outSizePointer = calloc<Int32>(); try { final resultPointer = _resizeAndConvert(imagePathPointer, outSizePointer); if (resultPointer == nullptr || outSizePointer.value <= 0) { print('Resim işleme hatası: Boş veri döndü'); return null; } final totalElements = outSizePointer.value; print('Toplam eleman sayısı: $totalElements'); final list = resultPointer.asTypedList(totalElements); _freeFloatArray(resultPointer); return list; } catch (e) { print('Resim işleme hatası: $e'); return null; } finally { calloc.free(imagePathPointer); calloc.free(outSizePointer); } } Future<List?> imageFileToFloat32List2(File imageFile) async { final float32List = await processImage(imageFile.path); if (float32List == null) { return null; } return float32List.reshape([ 80, 160, 3]); } }
原因分析与解决方案
1. 核心差异来源
skimage.transform.resize和OpenCV的resize在线性插值的计算细节上存在本质差异:
- scikit-image的线性插值默认启用
anti_aliasing=True,缩放前会对图像做高斯模糊处理以避免混叠;OpenCV的INTER_LINEAR无默认抗锯齿逻辑。 - 两者对边界像素的插值处理逻辑不同,scikit-image使用反射式边界填充,OpenCV默认使用常量填充,导致边缘区域数值差异明显。
2. 统一缩放逻辑的具体方案
方案一:Python端改用OpenCV缩放,对齐Android端逻辑
将Python代码中的skimage.transform.resize替换为OpenCV的resize,确保插值、色彩空间转换逻辑完全一致:
# 替换原small_img = resize(...)部分 small_img = cv2.resize(image, (160, 80), interpolation=cv2.INTER_LINEAR) small_img = np.array(small_img, dtype=np.float32) small_img = small_img[None, :, :, :]
方案二:Android端修改OpenCV缩放逻辑,对齐scikit-image
若需保留scikit-image的处理逻辑,需在OpenCV中添加抗锯齿和边界处理:
// 在resize前添加高斯模糊,模拟scikit-image的anti_aliasing Mat blurred; GaussianBlur(img, blurred, Size(3, 3), 0); // 使用反射式边界填充,对齐scikit-image的边界处理 resize(blurred, resizedImg, Size(160, 80), 0, 0, INTER_LINEAR, BORDER_REFLECT_101);
3. 验证一致性
修改后,取同一张测试图,分别输出两端缩放后的浮点数组,比较数值差异(允许1e-6以内的浮点精度误差),确认差异消失后再输入模型验证检测结果。
内容的提问来源于stack exchange,提问作者İsmail Kerem Gül
相关产品推荐
相关产品推荐

