浅解Letterbox算法
什么是Letterbox算法?
Letterbox是一种在计算机视觉(尤其是在YOLO、SSD等目标检测模型中)广泛使用的图像预处理技术。 它的核心目标是将不同尺寸、不同长宽比的原始图片,转换成符合模型输入要求的固定尺寸,同时保持图片原有的长宽比不变,避免图像内容因拉伸或压缩而失真。
这个名字来源于电影术语“信箱模式”(Letterboxing),即在宽屏电影在标准屏幕上播放时,为了保持画面比例,会在屏幕的上下方添加黑边。Letterbox算法做的正是类似的操作。
为什么需要Letterbox?
-
模型需要固定尺寸的输入:大多数深度学习模型,其结构决定了它只能处理固定尺寸的输入图像(例如 640x640 像素)。
-
避免图像失真:如果直接将一张矩形图片强制缩放成正方形,图片中的物体会被压扁或拉长,导致其几何形状发生扭曲。 这种失真会严重影响模型识别物体的准确性。
-
保留完整信息:与直接裁剪图像中心区域来适应尺寸不同,Letterbox通过填充的方式保留了原始图像的全部信息,避免了因裁剪而丢失图像边缘物体的风险。
Letterbox如何实现
假设原始尺寸为
目标尺寸为
步骤如下:
-
计算缩放比例:
为了让整张图片都能被放入目标尺寸的框内,需要计算一个统一的缩放比例。这个比例由原始尺寸与目标尺寸的高度比和宽度比中较小的那一个决定。
-
等比例缩放图像:
使用上一步计算出的缩放比例,对原始图像进行等比例缩放,得到新的尺寸
,其中:
-
计算需要填充的尺寸:
计算缩放后的图像与目标尺寸之间的差值,这个差值就是需要填充“灰边”的区域大小。
-
将计算出来的
、
平均分配到图像的两侧(左/右)或(上/下),并用固定的颜色进行填充。
逆向变换
当模型在经过Letterbox处理的图像上完成目标检测后,它输出的边界框坐标是相对于这个带有灰边的图像的。为了在原始图像上正确地标出物体位置,必须将这些坐标进行逆向变换——即减去填充的边距,再除以缩放比例,以还原到原始图像的坐标系中。
由于计算过程中可能存在浮点数精度误差,或者模型预测的边界框稍微超出了图像内容区域,还原后的坐标可能会超出原始图像的边界,最好做一个“裁剪”或“约束”处理,确保所有坐标值都在有效范围内。
例程
#include <opencv2/opencv.hpp>/*** @brief 对图像进行Letterbox预处理* * @param src 原始图像* @param dst [输出] 处理后的图像* @param new_shape 目标形状(模型输入尺寸)* @param ratio [输出] 记录实际使用的缩放比例* @param dw [输出] 记录宽度方向的单边padding* @param dh [输出] 记录高度方向的单边padding* @param color 填充区域的颜色* @param scaleup 是否允许上采样*/
inline void letterbox(const cv::Mat& src, cv::Mat& dst, const cv::Size& new_shape, float& ratio, int& dw, int& dh,const cv::Scalar& color = cv::Scalar(114, 114, 114), bool scaleup = true) {// 1. 计算缩放比例ratio = std::min((float)new_shape.width / src.cols, (float)new_shape.height / src.rows);if (!scaleup) {ratio = std::min(ratio, 1.0f);}// 2. 计算缩放后的尺寸int new_w = static_cast<int>(round(src.cols * ratio));int new_h = static_cast<int>(round(src.rows * ratio));// 3. 计算需要填充的paddingdw = (new_shape.width - new_w) / 2;dh = (new_shape.height - new_h) / 2;// 4 创建一个最终尺寸的画布,并填充颜色dst.create(new_shape, src.type());dst.setTo(color);cv::Rect roi(dw, dh, new_w, new_h);cv::Mat roi_target = dst(roi);cv::resize(src, roi_target, roi_target.size(), 0, 0, cv::INTER_LINEAR);
}/*** @brief 将letterbox处理后图像上的坐标反算回原图坐标* * @param box_on_letterboxed 在letterbox图像上的检测框 (cv::Rect)* @param ratio letterbox处理时使用的缩放比例* @param dw letterbox处理时宽度方向的padding* @param dh letterbox处理时高度方向的padding* @param original_shape 原始图像的尺寸 cv::Size(width, height)* @return cv::Rect 在原始图像上的对应检测框*/
inline cv::Rect map_coordinates_back(const cv::Rect& box, float ratio, int dw, int dh, const cv::Size& original_shape) {// orig_coord = (letterbox_coord - padding) / ratioint x1 = static_cast<int>((box.x - dw) / ratio);int y1 = static_cast<int>((box.y - dh) / ratio);int x2 = static_cast<int>((box.x + box.width - dw) / ratio);int y2 = static_cast<int>((box.y + box.height - dh) / ratio);// 裁剪坐标x1 = std::max(0, std::min(x1, original_shape.width));y1 = std::max(0, std::min(y1, original_shape.height));x2 = std::max(0, std::min(x2, original_shape.width));y2 = std::max(0, std::min(y2, original_shape.height));return cv::Rect(x1, y1, x2 - x1, y2 - y1);
}int main() {// 测试图像cv::Mat image = cv::Mat::zeros(cv::Size(600, 400), CV_8UC3);cv::Rect object(150, 100, 200, 150);cv::rectangle(image, object, cv::Scalar(255, 0, 0), -1);// Letterboxcv::Size model_input_size(640, 640);// 接收letterbox结果cv::Mat letterboxed_image;float ratio;int dw, dh;// 信息通过引用被填充letterbox(image, letterboxed_image, model_input_size, ratio, dw, dh);// std::cout << "图像已处理为 " << model_input_size.width << "x" << model_input_size.height << " (Letterbox)。" << std::endl;// std::cout << " - 缩放比例 (ratio): " << ratio << std::endl;// std::cout << " - 宽度填充 (dw): " << dw << " pixels" << std::endl;// std::cout << " - 高度填充 (dh): " << dh << " pixels" << std::endl;// 模拟在Letterbox图像上进行目标检测int x_prime = static_cast<int>(object.x * ratio + dw);int y_prime = static_cast<int>(object.y * ratio + dh);int w_prime = static_cast<int>(object.width * ratio);int h_prime = static_cast<int>(object.height * ratio);cv::Rect box(x_prime, y_prime, w_prime, h_prime);// std::cout << "Letterbox图像上检测到物体,其坐标为: " << box << std::endl;cv::rectangle(letterboxed_image, box, cv::Scalar(0, 0, 255), 2);// 将检测框坐标反算回原始图像// 将之前获取的 ratio, dw, dh 等信息传入cv::Rect box_original = map_coordinates_back(box, ratio, dw, dh, image.size());// std::cout << "将检测框坐标反算回原始图像,得到坐标: " << box_original << std::endl;// std::cout << "原始物体坐标为: " << object << " (用于对比)" << std::endl;cv::rectangle(image, box_original, cv::Scalar(0, 255, 0), 2);// 显示结果cv::imshow("Original Image", image);cv::imshow("Letterboxed Image", letterboxed_image);cv::waitKey(0);return 0;
}