You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 14:53:10