图像翻正以及原地(不开辟新空间)顺时旋转90度问题

本文介绍一种不开辟新空间实现图像顺时针旋转90度的方法,并针对OpenCV中图像数据结构特性进行深入探讨,包括算法实现及潜在问题的解决。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

将图像原地顺时针旋转90度,不开辟新空间。此题看似平易(题目简短),仔细研究发现着实不容易。经过一番探索后,终于找到了正确的算法,但是当使用opencv实现时,有碰到了困难而且费了一番周折才找到问题所在。

首先,解决这个问题,先简化成原地90度旋转一M×N的矩阵A(注意不是N×N方阵)。对于2×3的矩阵A = {1,2,3;4,5,6},其目标为矩阵B = {4,1;5,2;6,3}。因为是原地旋转,这里A和B应指向同一大小为6的内存空间。

这里有这样一个重要的导出公式,就是B[i][j] = A[M-1-j][i],读者可自己推敲,是后面算法的基石。
比如用Python实现:

import numpy as np
A = np.linspace(1,6,6)
A = A.reshape(2,3)
print(A)

H = A.shape[0]#行的数量
W = A.shape[1]#列的数量
print("H:%d W:%d"%(H,W))
B = np.zeros([W,H])

for i in range(W):
    for j in range(H):
        B[i][j] = A[H-j-1][i]

print(B)


这里写图片描述
好了,这样如果B指向的是一块新开辟内存,问题就简单了,只要遍历B的空间按照上面公式取A元素赋值即可。但是,对于原地旋转就面临一个问题,给B[i][j]赋值时,该位置的原先值被覆盖,如何处理?直观的解决方案是交换元素,即将B[i][j]和A[M-1-j][i]两处元素交换。但是新问题出来了,如果简单采用此交换策略问题如下:
注意:A和B指向同一大小为6的内存空间,首地址为同一叫做S,即一维化结果,则B[i][j] = S[i*M+j],A[i][j] = S[i*N+j]。

S S+5
1 2 3 4 5 6
B[0][0] <-> A[1][0] (内存空间中元素1和4交换位置)

S S+5
4 2 31 5 6
B[0][1] <-> A[0][0] (内存空间中元素2应和原先的元素1交换,但是该位置现在已经不是1了)

所以,需要有一种新策略,如果发现待交换位置的元素已经被更换了,需要继续查找到该元素被换到哪里了。新算法核心伪代码如下:(参考)
这里写图片描述

采用Python实现,只能是方阵:

import numpy as np
A = np.linspace(1, 9, 9)
A = A.reshape(3, 3)
print(A)

H = A.shape[0]  # 行的数量
W = A.shape[1]  # 列的数量
print("H:%d W:%d" % (H, W))

for i in range(W):
    for j in range(H):
        # B[i][j] 前的元素均已正确
        I = H - j - 1
        J = i
        while ((i*H+j)>(I*W+J)):#待替换元素在B[i][j]位置后面,因为我们只想要交换一次,第二次就变了
            p = I*W+J
            tmp_i = int(p / H)#取整,获取B[i][j]在A中的位置i
            tmp_j = int(p % H)#取余,获取B[i][j]在A中的位置j
            I = H-tmp_j-1
            J = tmp_i
        A[i][j],A[I][J]=A[I][J],A[i][j]

print(A)

这里写图片描述
上述代码可以实现任意矩阵原地90度顺时旋转,按理说彩色图像只不过是3通道每个位置由3个元素组成而已,用上述算法应该同样能够搞定,采用opencv写出了如下程序:

#include <cv.h>
#include <highgui.h>

void swap(char& a, char& b){
    char c = a;
    a = b;
    b = c;
}

int main() {
    IplImage* img = cvLoadImage("test.jpg");
    cvShowImage("src", img);
    int w = img->width;
    int h = img->height;
    for (int i = 0; i < w; i++) 
    {
        for (int j = 0; j < h; j++) 
        {
            int I = h - 1 - j;
            int J = i;
            while ((i*h + j) > (I*w + J))
            {
                int p = I*w + J;
                int tmp_i = p / h;
                int tmp_j = p % h;
                I = h - 1 - tmp_j;
                J = tmp_i;
            } 
            swap(*(img->imageData + i*h*3 + j*3 + 0), *(img->imageData + I*w*3 + J*3 + 0));
            swap(*(img->imageData + i*h*3 + j*3 + 1), *(img->imageData + I*w*3 + J*3 + 1));
            swap(*(img->imageData + i*h*3 + j*3 + 2), *(img->imageData + I*w*3 + J*3 + 2));
        }
    }

    img->width = h;
    img->height = w;
    img->widthStep = h*3;
    cvShowImage("dst", img);
    cvWaitKey();
    cvReleaseImage(&img);
    return 0;
}

opencv2.4.9版本为:

// Demon.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  



void swap(char& a, char& b){
    char c = a;
    a = b;
    b = c;
}

int _tmain(int argc, _TCHAR* argv[])
{
    IplImage* img = cvLoadImage("F:\\2345Do\\2\\index29_400.jpg");
    cvShowImage("src", img);
    int w = img->width;
    int h = img->height;
    for (int i = 0; i < w; i++){
        for (int j = 0; j < h; j++){
            int I = h - 1 - j;
            int J = i;
            while ((i*h + j)>(I*w + J)){
                int p = I*w + J;
                int tmp_i = p / h;
                int tmp_j = p%h;
                I = h - 1 - tmp_j;
                J = tmp_i;
            }
            swap(*(img->imageData + i*h * 3 + j * 3 + 0), *(img->imageData + I*w * 3 + J * 3 + 0));
            swap(*(img->imageData + i*h * 3 + j * 3 + 1), *(img->imageData + I*w * 3 + J * 3 + 1));
            swap(*(img->imageData + i*h * 3 + j * 3 + 2), *(img->imageData + I*w * 3 + J * 3 + 2));
        }
    }
    img->width = h;
    img->height = w;
    img->widthStep = h * 3;
    cvShowImage("dst", img);
    cvWaitKey();
    cvReleaseImage(&img);
    return 0;
}

测试几幅图片,得到了满意的效果,一例如下
这里写图片描述

然而,心满意足之时,忽然出现了意外,个别图出现了如下效果:
这里写图片描述

这里让人非常沮丧,算法难道错了么?仔细思考后发现算法是没有漏洞的,那问题出在哪里呢?经过一番思考,终于找到这个潜在的问题。

请仔细看上面的C代码,你会发现笔者假定了一件事,就是认为
img->widthStep = img->width * img->nChannels
,这其实就是潜在的隐患!我们通常直觉认为图像imageData这个一维数组的存储就是每行的w×3个字节连续存储h段,然而实际上是每行widthStep个字节连续存储h段,但widthStep并不总是等于w×3,往往会多出几个字节。因为在32位机上,opencv分配字节要按4字节对齐。所以如上问题图w = 706,本来应分配2118个字节,实际上却是分配2120个字节(4的倍数),每行2个冗余字节。
这点在平时很难察觉到,但在原地旋转图片这个问题中就凸显出来,因为冗余字节的存在,旋转前后opencv图像需要空间可能不一样。例如,原图700×401,图像字节大小为700×401×3,而选转90度后401×700,图像字节大小变为404×700×3,说明原空间大小是不足以表示新图的。

综上,如果真的碰到原地旋转图像90度这种问题的话,两种解决方法:
1. 仍用opencv1.x的数据结构IplImge,那先将图片就近resize到宽和高为4的倍数的值。
2. 采用opencv2.x的数据结构cv::Mat,就不会出现上述问题,下面是使用opencv2的示例程序。

#include <opencv2/opencv.hpp>

void swap(unsigned char& a, unsigned char& b){
    char c = a;
    a = b;
    b = c;
}

int main() {
    cv::Mat img = cv::imread("test.jpg");
    int w = img.cols;
    int h = img.rows;
    for (int i = 0; i < w; i++) 
    {
        for (int j = 0; j < h; j++) 
        {
            int I = h - 1 - j;
            int J = i;
            while ((i*h + j) > (I*w + J))
            {
                int p = I*w + J;
                int tmp_i = p / h;
                int tmp_j = p % h;
                I = h - 1 - tmp_j;
                J = tmp_i;
            } 
            swap(*(img.data + i*h*3 + j*3 + 0), *(img.data + I*w*3 + J*3 + 0));
            swap(*(img.data + i*h*3 + j*3 + 1), *(img.data + I*w*3 + J*3 + 1));
            swap(*(img.data + i*h*3 + j*3 + 2), *(img.data + I*w*3 + J*3 + 2));
        }
    }

    img.cols = h;
    img.rows = w;
    img.step = h*3;

    cv::imshow("img", img);
    cv::waitKey();
    return 0;
}

我的代码:

// Demon.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <opencv2\opencv.hpp>



void swap(unsigned char& a, unsigned char& b){//为什么要用unsigned?
    char c = a;
    a = b;
    b = c;
}

int _tmain(int argc, _TCHAR* argv[])
{
    cv::Mat img = cv::imread("F:\\2345Do\\2\\index29_400.jpg");
    cv::imshow("img", img);

    int w = img.cols;
    int h = img.rows;
    for (int i = 0; i < w; i++){
        for (int j = 0; j < h; j++){
            int I = h - 1 - j;
            int J = i;
            while ((i*h + j)>(I*w + J)){
                int p = I*w + J;
                int tmp_i = p / h;
                int tmp_j = p%h;
                I = h - 1 - tmp_j;
                J = tmp_i;
            }
            swap(*(img.data + i*h * 3 + j * 3 + 0), *(img.data + I*w * 3 + J * 3 + 0));
            swap(*(img.data + i*h * 3 + j * 3 + 1), *(img.data + I*w * 3 + J * 3 + 1));
            swap(*(img.data + i*h * 3 + j * 3 + 2), *(img.data + I*w * 3 + J * 3 + 2));
        }
    }
    img.cols = h;
    img.rows = w;
    img.step = h * 3;
    cv::imshow("rotimg",img);
    cv::waitKey();
    return 0;
}

这里写图片描述

参考

补充自opencv图像原地(不开辟新空间)顺时旋转90度 - Sean.W的专栏 - 优快云博客
http://blog.youkuaiyun.com/seanwang_25/article/details/43273593

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何以问天涯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值