在 C++ 中旋转图像而不使用 OpenCV 函数
说明:-我试图在不使用 C++ 中的 OpenCV 函数的情况下旋转图像.旋转中心不必是图像的中心.它可能是一个不同的点(从图像中心偏移).到目前为止,我遵循各种来源进行图像插值,并且我知道
更新:-
在受到与此问题相关的许多答案以及下面最详尽、最有用和最慷慨的答案的启发后,我可以修复我的 OpenCV 代码以获得所需的结果.
修改后的代码:
//平凡常量constexpr 双圆周率 = 3.1415926535897932384626433832795;/*!* rief 函数生成变换矩阵* param angle 是用户输入的旋转角度* param pivot 是 x 轴和 y 轴的平移量* eturn 平移矩阵*/cv::Mat CreateTransMat(double angle, std::pair<int, int> &pivot) {角度 = Pi * 角度/180;返回(cv::Mat_(3, 3)<(1, 2) <<(0, 0)/trans_mat.at(0, 2),trans_mat.at<double>(0, 1)/trans_mat.at<double>(0, 2));}/*!* rief 基于旋转角度和平移变换图像的函数矩阵.当旋转和平移同时发生时,两个矩阵可以合并* param src 是源图像* param dest 是目标图像* param trans_mat 是变换(旋转/平移)矩阵*/void ImageTransform(const cv::Mat &src, const cv::Mat &trans_mat, cv::Mat &dest) {int src_rows = src.rows;int src_cols = src.cols;int dest_rows = dest.rows;int dest_cols = dest.cols;const cv::Mat inverse_mat = trans_mat.inv();//#pragma omp parallel for simdfor (int row = 0; row < dest_rows; row++) {//#pragma omp parallel for simdfor (int col = 0; col < dest_cols; col++) {cv::Mat src_pos = CoordTransform(inverse_mat,(cv::Mat_(3, 1) (src_pos.at(0, 0) + 0.5);const int y_actual = static_cast(src_pos.at(0, 1) + 0.5);如果 (x_actual >= 0 && x_actual < src_cols &&y_actual >= 0 &&y_实际(row, col) = src.at(y_actual, x_actual);别的dest.at(row, col) = cv::Vec3b(0, 0, 0);}}}/*!* rief 命令行参数输入的用户手册*/无效用法(){std::cout <<命令输入:-
"<<"./ImageTransform <图像><旋转角度>><<std::endl;}/*!* rief 主函数读取图像的用户输入位置,然后应用所需的转换(旋转/平移)*/int main(int argc, char *argv[]){自动启动 = std::chrono::steady_clock::now();if (argc == 0 || argc <3)用法();别的 {双学位 = std::stod(argv[2]);双角 = 度数 * CV_PI/180.;cv::Mat src_img = cv::imread(argv[1]);std::pairnull_trans = std::make_pair(0, 0);std::pair翻译_初始 =std::make_pair(src_img.cols/2 + 1, src_img.rows/2 + 1);std::pair翻译_最终 =std::make_pair(0, -src_img.rows/2 - 4);如果(!src_img.data){std::cout <<图像空"<<std::endl;简历::等待键(0);}简历:: imshow(来源",src_img);cv::Mat dest_img = cv::Mat(static_cast(2 * src_img.rows),static_cast(2 * src_img.cols),src_img.type());cv::Mat trans_mat1 = CreateTransMat(degree, translation_initial);ImageTransform(src_img, trans_mat1, dest_img);cv::imshow(临时", dest_img);简历::垫中间_img = dest_img;dest_img.release();dest_img = cv::Mat(src_img.rows, src_img.cols, src_img.type());cv::Mat trans_mat2 = CreateTransMat(0, translation_final);ImageTransform(interim_img, trans_mat2, dest_img);cv::imshow(最终图像", dest_img);简历::等待键(10);}自动结束 = std::chrono::steady_clock::now();自动差异 = 结束 - 开始;std::cout <<std::chrono::duration <double, std::milli>(diff).count() <<"毫秒"<<std::endl;}
输入图像
旋转图像
解决方案首先,我必须承认我同意 来自我最近写的另一个答案.(已使用 PPM 文件格式,因为它需要最少的文件 I/O 代码.)
接下来,我使用了linMath.h
(我用于 3D 转换的最小数学集合)为 2D 转换创建最小数学集合–linMath.h
:
#ifndef LIN_MATH_H#define LIN_MATH_H#include #include <cassert>#include <cmath>extern const double Pi;模板内联值 degToRad(值角度){返回 (VALUE)Pi * 角度/(VALUE)180;}模板内联值 radToDeg(VALUE 角度){返回 (VALUE)180 * 角度/(VALUE)Pi;}枚举 ArgNull { Null };模板struct Vec2T {typedef VALUE 值;值 x, y;//默认构造函数(使元素未初始化)Vec2T() { }Vec2T(ArgNull): x((Value)0), y((Value)0) { }Vec2T(值 x, 值 y): x(x), y(y) { }};typedef Vec2Tvec2f;typedef Vec2Tvec2;模板struct Vec3T {typedef VALUE 值;值 x, y, z;//默认构造函数(使元素未初始化)Vec3T() { }Vec3T(ArgNull): x((Value)0), y((Value)0), z((Value)0) { }Vec3T(x 值,y 值,z 值):x(x), y(y), z(z) { }Vec3T(const Vec2T &xy, Value z): x(xy.x), y(xy.y), z(z) { }显式运算符 Vec2T() const { return Vec2T(x, y);}const Vec2f xy() const { return Vec2f(x, y);}const Vec2f xz() const { return Vec2f(x, z);}const Vec2f yz() const { return Vec2f(y, z);}};typedef Vec3Tvec3f;typedef Vec3Tvec3;枚举 ArgInitIdent { InitIdent };枚举 ArgInitTrans { InitTrans };枚举 ArgInitRot { InitRot };枚举 ArgInitScale { InitScale };枚举 ArgInitFrame { InitFrame };模板结构 Mat3x3T {联合{价值补偿[3 * 3];结构{值_00、_01、_02;值_10、_11、_12;值_20、_21、_22;};};//默认构造函数(使元素未初始化)Mat3x3T() { }//构造函数以按元素构建矩阵Mat3x3T(价值_00,价值_01,价值_02,价值_10,价值_11,价值_12,价值_20、价值_21、价值_22):_00(_00), _01(_01), _02(_02),_10(_10), _11(_11), _12(_12),_20(_20)、_21(_21)、_22(_22){ }//构造单位矩阵的构造函数Mat3x3T(ArgInitIdent):_00((VALUE)1)、_01((VALUE)0)、_02((VALUE)0)、_10((VALUE)0)、_11((VALUE)1)、_12((VALUE)0)、_20((VALUE)0)、_21((VALUE)0)、_22((VALUE)1){ }//构造一个用于翻译的矩阵Mat3x3T(ArgInitTrans, const Vec2T &t):_00((VALUE)1)、_01((VALUE)0)、_02((VALUE)t.x)、_10((VALUE)0), _11((VALUE)1), _12((VALUE)t.y),_20((VALUE)0)、_21((VALUE)0)、_22((VALUE)1){ }//构造函数来构建旋转矩阵Mat3x3T(ArgInitRot, VALUE 角度):_00(std::cos(angle)), _01(-std::sin(angle)), _02((VALUE)0),_10(std::sin(angle))、_11(std::cos(angle))、_12((VALUE)0)、_20((VALUE)0)、_21((VALUE)0)、_22((VALUE)1){ }//构造函数来构建平移/旋转矩阵Mat3x3T(ArgInitFrame, const Vec2T &t, VALUE 角度):_00(std::cos(angle)), _01(-std::sin(angle)), _02((VALUE)t.x),_10(std::sin(angle))、_11(std::cos(angle))、_12((VALUE)t.y)、_20((VALUE)0)、_21((VALUE)0)、_22((VALUE)1){ }//构造函数以构建用于缩放的矩阵Mat3x3T(ArgInitScale, VALUE sx, VALUE sy):_00((VALUE)sx)、_01((VALUE)0)、_02((VALUE)0)、_10((VALUE)0)、_11((VALUE)sy)、_12((VALUE)0)、_20((VALUE)0)、_21((VALUE)0)、_22((VALUE)1){ }//允许使用 [][] 访问的运算符VALUE* 运算符 [] (int i){断言(i >= 0 && i < 3);返回补偿 + 3 * i;}//允许使用 [][] 访问的运算符const VALUE* 运算符 [] (int i) const{断言(i >= 0 && i < 3);返回补偿 + 3 * i;}//将矩阵与矩阵相乘 ->矩阵Mat3x3T 运算符 * (const Mat3x3T &mat) const{返回 Mat3x3T(_00 * mat._00 + _01 * mat._10 + _02 * mat._20,_00 * mat._01 + _01 * mat._11 + _02 * mat._21,_00 * mat._02 + _01 * mat._12 + _02 * mat._22,_10 * mat._00 + _11 * mat._10 + _12 * mat._20,_10 * mat._01 + _11 * mat._11 + _12 * mat._21,_10 * mat._02 + _11 * mat._12 + _12 * mat._22,_20 * mat._00 + _21 * mat._10 + _22 * mat._20,_20 * mat._01 + _21 * mat._11 + _22 * mat._21,_20 * mat._02 + _21 * mat._12 + _22 * mat._22);}//将矩阵与向量相乘 ->向量Vec3T运算符 * (const Vec3T &vec) const{返回 Vec3T(_00 * vec.x + _01 * vec.y + _02 * vec.z,_10 * vec.x + _11 * vec.y + _12 * vec.z,_20 * vec.x + _21 * vec.y + _22 * vec.z);}};typedef Mat3x3TMat3x3f;typedef Mat3x3T;Mat3x3;模板std::ostream&运算符<<(std::ostream &out, const Mat3x3T &m){回来<<m._00<<' ' <<m._01<<' ' <<m._02<<'
'<<m._10 <<' ' <<m._11 <<' ' <<m._12<<'
'<<m._20<<' ' <<m._21<<' ' <<m._22<<'
';}/* 计算矩阵的行列式.** det = |M|** mat ... 矩阵*/模板值行列式(const Mat3x3T&mat){返回 mat._00 * mat._11 * mat._22+ mat._01 * mat._12 * mat._20+ mat._02 * mat._10 * mat._21- mat._20 * mat._11 * mat._02- mat._21 * mat._12 * mat._00- mat._22 * mat._10 * mat._01;}/* 返回正则矩阵的逆矩阵.** mat 矩阵反转* eps epsilon 矩阵的规律性*/模板Mat3x3T倒置(const Mat3x3T&mat, VALUE eps = (VALUE)1E-10){断言(eps >=(值)0);//计算行列式并检查它是否不等于 0//(否则,矩阵是奇异的!)常量值 det = 行列式(垫);if (std::abs(det) (detInvPos * (mat._11 * mat._22 - mat._12 * mat._21),detInvNeg * (mat._01 * mat._22 - mat._02 * mat._21),detInvPos * (mat._01 * mat._12 - mat._02 * mat._11),detInvNeg * (mat._10 * mat._22 - mat._12 * mat._20),detInvPos * (mat._00 * mat._22 - mat._02 * mat._20),detInvNeg * (mat._00 * mat._12 - mat._02 * mat._10),detInvPos * (mat._10 * mat._21 - mat._11 * mat._20),detInvNeg * (mat._00 * mat._21 - mat._01 * mat._20),detInvPos * (mat._00 * mat._11 - mat._01 * mat._10));}#endif//LIN_MATH_H
以及linMath.cc
中Pi
的定义:
#include "linmath.h"const double Pi = 3.1415926535897932384626433832795;
有了所有可用的工具,我制作了示例应用程序 xformRGBImg.cc
:
#include #include <fstream>#include #include <字符串>#include "linMath.h"#include "image.h"#include "imagePPM.h"typedef unsigned int uint;结构错误{const std::string 文本;错误(常量字符*文本):文本(文本){}};const char* readArg(int &i, int argc, char **argv){++i;if (i >= argc) throw Error("缺少参数!");返回 argv[i];}uint readArgUInt(int &i, int argc, char **argv){const char *arg = readArg(i, argc, argv);字符 * 结束;const unsigned long value = strtoul(arg, &end, 0);if (arg == end || *end) throw Error("应为无符号整数值!");if ((uint)value != value) throw Error("无符号整数溢出!");返回(单位)值;}double readArgDouble(int &i, int argc, char **argv){const char *arg = readArg(i, argc, argv);字符 * 结束;const double value = strtod(arg, &end);if (arg == end || *end) throw Error("需要浮点值!");返回值;}std::pair调整大小(int &i,int argc,char **argv){const uint w = readArgUInt(i, argc, argv);const uint h = readArgUInt(i, argc, argv);返回 std::make_pair(w, h);}Mat3x3 翻译(int &i,int argc,char **argv){const double x = readArgDouble(i, argc, argv);const double y = readArgDouble(i, argc, argv);返回 Mat3x3(InitTrans, Vec2(x, y));}Mat3x3 旋转(int &i,int argc,char **argv){const double angle = readArgDouble(i, argc, argv);返回 Mat3x3(InitRot, degToRad(angle));}Mat3x3 比例(int &i,int argc,char **argv){const double x = readArgDouble(i, argc, argv);const double y = readArgDouble(i, argc, argv);返回 Mat3x3(InitScale, x, y);}Vec2 变换(const Mat3x3 &mat,const Vec2 &pos){const Vec3 pos_ = mat * Vec3(pos, 1.0);返回 Vec2(pos_.x/pos_.z, pos_.y/pos_.z);}空变换(const Image &imgSrc, const Mat3x3 &mat, Image &imgDst,int rgbFail = 0x808080){const Mat3x3 matInv = invert(mat);for (int y = 0; y < imgDst.h(); ++y) {for (int x = 0; x 大小输出(0, 0);Mat3x3 mat(InitIdent);for (int i = 3; i < argc; ++i) 试试 {const std::string cmd = argv[i];if (cmd == "resize") sizeOut = resize(i, argc, argv);else if (cmd == "translate") mat = translate(i, argc, argv) * mat;else if (cmd == "rotate") mat = rotate(i, argc, argv) * mat;else if (cmd == "scale") mat = scale(i, argc, argv) * mat;别的 {std::cerr <<"错误的命令!
";std::cout <<用法;返回 1;}} catch (const Error &error) {std::cerr <<$ 处的错误参数" <<我<<"
"<<错误文本<<'
';std::cout <<用法;返回 1;}//读取图像图片 imgSrc;{ std::ifstream fIn(inFile.c_str(), std::ios::binary);如果(!readPPM(fIn,imgSrc)){std::cerr <<阅读"<<文件中<<"'失败!
";返回 1;}}//设置输出图像大小如果(sizeOut.first * sizeOut.second == 0){sizeOut = std::make_pair(imgSrc.w(), imgSrc.h());}//变换图像图像 imgDst;imgDst.resize(sizeOut.first, sizeOut.second, 3 * sizeOut.second);变换(imgSrc,垫,imgDst);//写入图像{ std::ofstream fOut(outFile.c_str(), std::ios::binary);if (!writePPM(fOut, imgDst) || (fOut.close(), !fOut.good())) {std::cerr <<写'"<<输出文件<<"'失败!
";返回 1;}}//完毕返回0;}
注意:
命令行参数按顺序处理.每个转换命令从左乘到已经组合的转换矩阵,从一个单位矩阵开始.这是因为变换的串联导致矩阵的逆序乘法.(矩阵乘法是右结合的.)
例如变换的对应矩阵:
x' = 翻译(x)
x" = 旋转(x')
x"' = 比例(x")
这是
x"' = 缩放(旋转(翻译(x)))
是
Mtransform = Mscale ·M旋转 ·M翻译
和
x"' = Mscale · M旋转 ·Mtranslate · x = Mtransform · x
在
尺寸为 300 ×300.
注意:
所有嵌入的图像都从 PPM 转换为 JPEG(再次在
看起来像原来的–身份转换应该是什么.
现在,旋转 30°:
$ ./xformRGBImg cat.ppm cat.rot30.ppm 旋转 30$
要绕某个中心旋转,有一个相应的方法.需要前后翻译:
$ ./xformRGBImg cat.ppm cat.rot30c150,150.ppm 平移 -150 -150 旋转 30 平移 150 150$
输出图像可以用 w · 调整大小√2 ×·√2 以适应任何中心旋转.
因此,输出图像的大小调整为 425 ×425 其中最后一次翻译分别调整为translate 212.5 212.5
:
$ ./xformRGBImg cat.ppm cat.rot30c150,150.425x425.ppm 调整大小 425 425 平移 -150 -150 旋转 30 平移 212.5 212.5$
尚未检查缩放比例:
$ ./xformRGBImg cat.ppm cat.rot30c150,150s0.7,0.7.ppm 平移 -150 -150 旋转 30 缩放 0.7 0.7 平移 150 150$
<小时>最后,公平地说,我想提一下大哥".我的小玩具工具:ImageMagick.
Description :- I am trying to rotate an image without using OpenCV functions in C++. The rotation center need not be the center of the image. It could be a different point (offset from the image center). So far I followed a variety of sources to do image interpolation and I am aware of a source which does the job perfectly in MATLAB. I tried to mimic the same in C++ without OpenCV functions. But I am not getting the expected rotated image. Instead my output appears like a small horizontal line on the screen.
void RotateNearestNeighbor(cv::Mat src, double angle) {
int oldHeight = src.rows;
int oldWidth = src.cols;
int newHeight = std::sqrt(2) * oldHeight;
int newWidth = std::sqrt(2) * oldWidth;
cv::Mat output = cv::Mat(newHeight, newWidth, src.type());
double ctheta = cos(angle);
double stheta = sin(angle);
for (size_t i = 0; i < newHeight; i++) {
for (size_t j = 0; j < newWidth; j++) {
int oldRow = static_cast<int> ((i - newHeight / 2) * ctheta +
(j - newWidth / 2) * stheta + oldHeight / 2);
int oldCol = static_cast<int> (-(i - newHeight / 2) * stheta +
(j - newWidth / 2) * ctheta + oldWidth / 2);
if (oldRow > 0 && oldCol > 0 && oldRow <= oldHeight && oldCol <= oldWidth)
output.at<cv::Vec3b>(i, j) = src.at<cv::Vec3b>(oldRow, oldCol);
else
output.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 0, 0);
}
}
cv::imshow("Rotated cat", output);
}
The following are my input (left side) and output (right side) images
UPDATE : -
After being inspired by many answers related to this question and also the most elaborate, helpful and generous answer below, I could fix my OpenCV code to get the desired result.
Modified Code :
// Trivial constant
constexpr double Pi = 3.1415926535897932384626433832795;
/*!
* rief Function to generate transformation matrix
* param angle is the angle of rotation from user input
* param pivot is the amount of translation in x and y axes
* eturn translation matrix
*/
cv::Mat CreateTransMat(double angle, std::pair<int, int> &pivot) {
angle = Pi * angle / 180;
return (cv::Mat_<double>(3, 3) << cos(angle), -sin(angle), pivot.first,
sin(angle), cos(angle), pivot.second, 0, 0, 1);
}
/*!
* rief Function to apply coordinate transform from destination to source
* param inv_mat being the inverse transformation matrix for the transform needed
* eturn pos being the homogeneous coordinates for transformation
*/
cv::Mat CoordTransform(const cv::Mat &inv_mat, const cv::Mat &pos) {
assert(inv_mat.cols == pos.rows);
cv::Mat trans_mat = inv_mat * pos;
return (cv::Mat_<double>(1, 2) <<
trans_mat.at<double>(0, 0) / trans_mat.at<double>(0, 2),
trans_mat.at<double>(0, 1) / trans_mat.at<double>(0, 2));
}
/*!
* rief Function to transform an image based on a rotation angle and translation
matrix. When rotation and translation happen at the same time, the
two matrices can be combined
* param src being source image
* param dest being destination image
* param trans_mat being the transformation (rotation/ translation) matrix
*/
void ImageTransform(const cv::Mat &src, const cv::Mat &trans_mat, cv::Mat &dest) {
int src_rows = src.rows;
int src_cols = src.cols;
int dest_rows = dest.rows;
int dest_cols = dest.cols;
const cv::Mat inverse_mat = trans_mat.inv();
//#pragma omp parallel for simd
for (int row = 0; row < dest_rows; row++) {
//#pragma omp parallel for simd
for (int col = 0; col < dest_cols; col++) {
cv::Mat src_pos = CoordTransform(inverse_mat,
(cv::Mat_<double>(3, 1) << col, row, 1));
const int x_actual = static_cast<int>(src_pos.at<double>(0, 0) + 0.5);
const int y_actual = static_cast<int>(src_pos.at<double>(0, 1) + 0.5);
if (x_actual >= 0 && x_actual < src_cols &&
y_actual >= 0 && y_actual < src_rows)
dest.at<cv::Vec3b>(row, col) = src.at<cv::Vec3b>(y_actual, x_actual);
else
dest.at<cv::Vec3b>(row, col) = cv::Vec3b(0, 0, 0);
}
}
}
/*!
* rief User manual for command-line args input
*/
void Usage() {
std::cout << "COMMAND INPUT : -
" <<
" ./ImageTransform <image> <rotation-angle>" <<
std::endl;
}
/*!
* rief main function to read a user input location for an image and then apply the
required transformations (rotation / translation)
*/
int main(int argc, char *argv[])
{
auto start = std::chrono::steady_clock::now();
if (argc == 0 || argc < 3)
Usage();
else {
double degree = std::stod(argv[2]);
double angle = degree * CV_PI / 180.;
cv::Mat src_img = cv::imread(argv[1]);
std::pair<int, int> null_trans = std::make_pair(0, 0);
std::pair<int, int> translation_initial =
std::make_pair(src_img.cols / 2 + 1, src_img.rows / 2 + 1);
std::pair<int, int> translation_final =
std::make_pair(0, -src_img.rows / 2 - 4);
if (!src_img.data)
{
std::cout << "image null" << std::endl;
cv::waitKey(0);
}
cv::imshow("Source", src_img);
cv::Mat dest_img = cv::Mat(static_cast<int>(2 * src_img.rows),
static_cast<int>(2 * src_img.cols),
src_img.type());
cv::Mat trans_mat1 = CreateTransMat(degree, translation_initial);
ImageTransform(src_img, trans_mat1, dest_img);
cv::imshow("Interim", dest_img);
cv::Mat interim_img = dest_img;
dest_img.release();
dest_img = cv::Mat(src_img.rows, src_img.cols, src_img.type());
cv::Mat trans_mat2 = CreateTransMat(0, translation_final);
ImageTransform(interim_img, trans_mat2, dest_img);
cv::imshow("Final image", dest_img);
cv::waitKey(10);
}
auto end = std::chrono::steady_clock::now();
auto diff = end - start;
std::cout << std::chrono::duration <double, std::milli> (diff).count() <<
" ms" << std::endl;
}
Input image
Rotated image
解决方案First, I have to admit I agree with generic_opto_guy:
The approach with the loop looks good, so we would need to check the math. On thing I noticed: if (oldRow > 0 && oldCol > 0 && oldRow <= oldHeight && oldCol <= oldWidth) implies you start indexing with 1. I belife that opencv starts indexing with 0.
For all that, I couldn't resist to answer. (May be, it's just an image phase of mine.)
Instead of fiddling with sin() and cos(), I would recommend to use matrix transformation. At the first glance, this might appear over-engineered but later you will recognize that it bears much more flexibility. With a transformation matrix, you can express a lot of transformations (translation, rotation, scaling, shearing, projection) as well as combining multiple transformations into one matrix.
(A teaser for what is possible: SO: How to paint / deform a QImage in 2D?)
In an image, the pixels may be addressed by 2d coordinates. Hence a 2×2 matrix comes into mind but a 2×2 matrix cannot express translations. For this, homogeneous coordinates has been introduced – a math trick to handle positions and directions in the same space by extending the dimension by one.
To make it short, a 2d position (x, y) has the homogeneous coordinates (x, y, 1).
A position transformed with a transformation matrix:
v′ = M · v.
This may or may not change the value of third component. To convert the homogeneous coordinate to 2D position again, x and y has to be divided by 3rd component.
Vec2 transform(const Mat3x3 &mat, const Vec2 &pos)
{
const Vec3 pos_ = mat * Vec3(pos, 1.0);
return Vec2(pos_.x / pos_.z, pos_.y / pos_.z);
}
To transform a source image into a destination image, the following function can be used:
void transform(
const Image &imgSrc, const Mat3x3 &mat, Image &imgDst,
int rgbFail = 0x808080)
{
const Mat3x3 matInv = invert(mat);
for (int y = 0; y < imgDst.h(); ++y) {
for (int x = 0; x < imgDst.w(); ++x) {
const Vec2 pos = transform(matInv, Vec2(x, y));
const int xSrc = (int)(pos.x + 0.5), ySrc = (int)(pos.y + 0.5);
imgDst.setPixel(x, y,
xSrc >= 0 && xSrc < imgSrc.w() && ySrc >= 0 && ySrc < imgSrc.h()
? imgSrc.getPixel(xSrc, ySrc)
: rgbFail);
}
}
}
Note:
The transformation matrix mat
describes the transformation of source image coordinates to destination image coordinates. The nested loops iterate over destination image. Hence, the inverse matrix (representing the reverse transformation) has to be used to get the corresponding source image coordinates which map to the current destination coordinates.
… and the matrix constructor for the rotation:
enum ArgInitRot { InitRot };
template <typename VALUE>
struct Mat3x3T {
union {
VALUE comp[3 * 3];
struct {
VALUE _00, _01, _02;
VALUE _10, _11, _12;
VALUE _20, _21, _22;
};
};
// constructor to build a matrix for rotation
Mat3x3T(ArgInitRot, VALUE angle):
_00(std::cos(angle)), _01(-std::sin(angle)), _02((VALUE)0),
_10(std::sin(angle)), _11( std::cos(angle)), _12((VALUE)0),
_20( (VALUE)0), _21( (VALUE)0), _22((VALUE)1)
{ }
can be used to construct a rotation with angle
(in degree):
Mat3x3T<double> mat(InitRot, degToRad(30.0));
Note:
I would like to emphasize how the transformed coordinates are used:
const Vec2 pos = transform(matInv, Vec2(x, y));
const int xSrc = (int)(pos.x + 0.5), ySrc = (int)(pos.y + 0.5);
Rounding the results to yield one discrete pixel position is actually what is called Nearest Neighbour. Alternatively, the now discarded fractional parts could be used for a linear interpolation between neighbour pixels.
To make a small sample, I first copied image.h
, image.cc
, imagePPM.h
, and imagePPM.cc
from another answer I wrote recently. (The PPM file format has been used as it needs minimal code for file I/O.)
Next, I used linMath.h
(my minimal math collection for 3D transformations) to make a minimal math collection for 2D transformations – linMath.h
:
#ifndef LIN_MATH_H
#define LIN_MATH_H
#include <iostream>
#include <cassert>
#include <cmath>
extern const double Pi;
template <typename VALUE>
inline VALUE degToRad(VALUE angle)
{
return (VALUE)Pi * angle / (VALUE)180;
}
template <typename VALUE>
inline VALUE radToDeg(VALUE angle)
{
return (VALUE)180 * angle / (VALUE)Pi;
}
enum ArgNull { Null };
template <typename VALUE>
struct Vec2T {
typedef VALUE Value;
Value x, y;
// default constructor (leaving elements uninitialized)
Vec2T() { }
Vec2T(ArgNull): x((Value)0), y((Value)0) { }
Vec2T(Value x, Value y): x(x), y(y) { }
};
typedef Vec2T<float> Vec2f;
typedef Vec2T<double> Vec2;
template <typename VALUE>
struct Vec3T {
typedef VALUE Value;
Value x, y, z;
// default constructor (leaving elements uninitialized)
Vec3T() { }
Vec3T(ArgNull): x((Value)0), y((Value)0), z((Value)0) { }
Vec3T(Value x, Value y, Value z): x(x), y(y), z(z) { }
Vec3T(const Vec2T<Value> &xy, Value z): x(xy.x), y(xy.y), z(z) { }
explicit operator Vec2T<Value>() const { return Vec2T<Value>(x, y); }
const Vec2f xy() const { return Vec2f(x, y); }
const Vec2f xz() const { return Vec2f(x, z); }
const Vec2f yz() const { return Vec2f(y, z); }
};
typedef Vec3T<float> Vec3f;
typedef Vec3T<double> Vec3;
enum ArgInitIdent { InitIdent };
enum ArgInitTrans { InitTrans };
enum ArgInitRot { InitRot };
enum ArgInitScale { InitScale };
enum ArgInitFrame { InitFrame };
template <typename VALUE>
struct Mat3x3T {
union {
VALUE comp[3 * 3];
struct {
VALUE _00, _01, _02;
VALUE _10, _11, _12;
VALUE _20, _21, _22;
};
};
// default constructor (leaving elements uninitialized)
Mat3x3T() { }
// constructor to build a matrix by elements
Mat3x3T(
VALUE _00, VALUE _01, VALUE _02,
VALUE _10, VALUE _11, VALUE _12,
VALUE _20, VALUE _21, VALUE _22):
_00(_00), _01(_01), _02(_02),
_10(_10), _11(_11), _12(_12),
_20(_20), _21(_21), _22(_22)
{ }
// constructor to build an identity matrix
Mat3x3T(ArgInitIdent):
_00((VALUE)1), _01((VALUE)0), _02((VALUE)0),
_10((VALUE)0), _11((VALUE)1), _12((VALUE)0),
_20((VALUE)0), _21((VALUE)0), _22((VALUE)1)
{ }
// constructor to build a matrix for translation
Mat3x3T(ArgInitTrans, const Vec2T<VALUE> &t):
_00((VALUE)1), _01((VALUE)0), _02((VALUE)t.x),
_10((VALUE)0), _11((VALUE)1), _12((VALUE)t.y),
_20((VALUE)0), _21((VALUE)0), _22((VALUE)1)
{ }
// constructor to build a matrix for rotation
Mat3x3T(ArgInitRot, VALUE angle):
_00(std::cos(angle)), _01(-std::sin(angle)), _02((VALUE)0),
_10(std::sin(angle)), _11( std::cos(angle)), _12((VALUE)0),
_20( (VALUE)0), _21( (VALUE)0), _22((VALUE)1)
{ }
// constructor to build a matrix for translation/rotation
Mat3x3T(ArgInitFrame, const Vec2T<VALUE> &t, VALUE angle):
_00(std::cos(angle)), _01(-std::sin(angle)), _02((VALUE)t.x),
_10(std::sin(angle)), _11( std::cos(angle)), _12((VALUE)t.y),
_20( (VALUE)0), _21( (VALUE)0), _22((VALUE)1)
{ }
// constructor to build a matrix for scaling
Mat3x3T(ArgInitScale, VALUE sx, VALUE sy):
_00((VALUE)sx), _01( (VALUE)0), _02((VALUE)0),
_10( (VALUE)0), _11((VALUE)sy), _12((VALUE)0),
_20( (VALUE)0), _21( (VALUE)0), _22((VALUE)1)
{ }
// operator to allow access with [][]
VALUE* operator [] (int i)
{
assert(i >= 0 && i < 3);
return comp + 3 * i;
}
// operator to allow access with [][]
const VALUE* operator [] (int i) const
{
assert(i >= 0 && i < 3);
return comp + 3 * i;
}
// multiply matrix with matrix -> matrix
Mat3x3T operator * (const Mat3x3T &mat) const
{
return Mat3x3T(
_00 * mat._00 + _01 * mat._10 + _02 * mat._20,
_00 * mat._01 + _01 * mat._11 + _02 * mat._21,
_00 * mat._02 + _01 * mat._12 + _02 * mat._22,
_10 * mat._00 + _11 * mat._10 + _12 * mat._20,
_10 * mat._01 + _11 * mat._11 + _12 * mat._21,
_10 * mat._02 + _11 * mat._12 + _12 * mat._22,
_20 * mat._00 + _21 * mat._10 + _22 * mat._20,
_20 * mat._01 + _21 * mat._11 + _22 * mat._21,
_20 * mat._02 + _21 * mat._12 + _22 * mat._22);
}
// multiply matrix with vector -> vector
Vec3T<VALUE> operator * (const Vec3T<VALUE> &vec) const
{
return Vec3T<VALUE>(
_00 * vec.x + _01 * vec.y + _02 * vec.z,
_10 * vec.x + _11 * vec.y + _12 * vec.z,
_20 * vec.x + _21 * vec.y + _22 * vec.z);
}
};
typedef Mat3x3T<float> Mat3x3f;
typedef Mat3x3T<double> Mat3x3;
template <typename VALUE>
std::ostream& operator<<(std::ostream &out, const Mat3x3T<VALUE> &m)
{
return out
<< m._00 << ' ' << m._01 << ' ' << m._02 << '
'
<< m._10 << ' ' << m._11 << ' ' << m._12 << '
'
<< m._20 << ' ' << m._21 << ' ' << m._22 << '
';
}
/* computes determinant of a matrix.
*
* det = |M|
*
* mat ... the matrix
*/
template <typename VALUE>
VALUE determinant(const Mat3x3T<VALUE> &mat)
{
return mat._00 * mat._11 * mat._22
+ mat._01 * mat._12 * mat._20
+ mat._02 * mat._10 * mat._21
- mat._20 * mat._11 * mat._02
- mat._21 * mat._12 * mat._00
- mat._22 * mat._10 * mat._01;
}
/* returns the inverse of a regular matrix.
*
* mat matrix to invert
* eps epsilon for regularity of matrix
*/
template <typename VALUE>
Mat3x3T<VALUE> invert(
const Mat3x3T<VALUE> &mat, VALUE eps = (VALUE)1E-10)
{
assert(eps >= (VALUE)0);
// compute determinant and check that it its unequal to 0
// (Otherwise, matrix is singular!)
const VALUE det = determinant(mat);
if (std::abs(det) < eps) throw std::domain_error("Singular matrix!");
// reciproke of determinant
const VALUE detInvPos = (VALUE)1 / det, detInvNeg = -detInvPos;
// compute each element by determinant of sub-matrix which is build
// striking out row and column of pivot element itself
// BTW, the determinant is multiplied with -1 when sum of row and column
// index is odd (chess board rule)
// (This is usually called cofactor of related element.)
// transpose matrix and multiply with 1/determinant of original matrix
return Mat3x3T<VALUE>(
detInvPos * (mat._11 * mat._22 - mat._12 * mat._21),
detInvNeg * (mat._01 * mat._22 - mat._02 * mat._21),
detInvPos * (mat._01 * mat._12 - mat._02 * mat._11),
detInvNeg * (mat._10 * mat._22 - mat._12 * mat._20),
detInvPos * (mat._00 * mat._22 - mat._02 * mat._20),
detInvNeg * (mat._00 * mat._12 - mat._02 * mat._10),
detInvPos * (mat._10 * mat._21 - mat._11 * mat._20),
detInvNeg * (mat._00 * mat._21 - mat._01 * mat._20),
detInvPos * (mat._00 * mat._11 - mat._01 * mat._10));
}
#endif // LIN_MATH_H
and the definition of Pi
in linMath.cc
:
#include "linmath.h"
const double Pi = 3.1415926535897932384626433832795;
Having all tools available, I made the sample application xformRGBImg.cc
:
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include "linMath.h"
#include "image.h"
#include "imagePPM.h"
typedef unsigned int uint;
struct Error {
const std::string text;
Error(const char *text): text(text) { }
};
const char* readArg(int &i, int argc, char **argv)
{
++i;
if (i >= argc) throw Error("Missing argument!");
return argv[i];
}
uint readArgUInt(int &i, int argc, char **argv)
{
const char *arg = readArg(i, argc, argv); char *end;
const unsigned long value = strtoul(arg, &end, 0);
if (arg == end || *end) throw Error("Unsigned integer value expected!");
if ((uint)value != value) throw Error("Unsigned integer overflow!");
return (uint)value;
}
double readArgDouble(int &i, int argc, char **argv)
{
const char *arg = readArg(i, argc, argv); char *end;
const double value = strtod(arg, &end);
if (arg == end || *end) throw Error("Floating point value expected!");
return value;
}
std::pair<uint, uint> resize(int &i, int argc, char **argv)
{
const uint w = readArgUInt(i, argc, argv);
const uint h = readArgUInt(i, argc, argv);
return std::make_pair(w, h);
}
Mat3x3 translate(int &i, int argc, char **argv)
{
const double x = readArgDouble(i, argc, argv);
const double y = readArgDouble(i, argc, argv);
return Mat3x3(InitTrans, Vec2(x, y));
}
Mat3x3 rotate(int &i, int argc, char **argv)
{
const double angle = readArgDouble(i, argc, argv);
return Mat3x3(InitRot, degToRad(angle));
}
Mat3x3 scale(int &i, int argc, char **argv)
{
const double x = readArgDouble(i, argc, argv);
const double y = readArgDouble(i, argc, argv);
return Mat3x3(InitScale, x, y);
}
Vec2 transform(const Mat3x3 &mat, const Vec2 &pos)
{
const Vec3 pos_ = mat * Vec3(pos, 1.0);
return Vec2(pos_.x / pos_.z, pos_.y / pos_.z);
}
void transform(
const Image &imgSrc, const Mat3x3 &mat, Image &imgDst,
int rgbFail = 0x808080)
{
const Mat3x3 matInv = invert(mat);
for (int y = 0; y < imgDst.h(); ++y) {
for (int x = 0; x < imgDst.w(); ++x) {
const Vec2 pos = transform(matInv, Vec2(x, y));
const int xSrc = (int)(pos.x + 0.5), ySrc = (int)(pos.y + 0.5);
imgDst.setPixel(x, y,
xSrc >= 0 && xSrc < imgSrc.w() && ySrc >= 0 && ySrc < imgSrc.h()
? imgSrc.getPixel(xSrc, ySrc)
: rgbFail);
}
}
}
const char *const usage =
"Usage:
"
" xformRGBImg IN_FILE OUT_FILE [[CMD]...]
"
"
"
"Commands:
"
" resize W H
"
" translate X Y
"
" rotate ANGLE
"
" scale SX SY
";
int main(int argc, char **argv)
{
// read command line arguments
if (argc <= 2) {
std::cerr << "Missing arguments!
";
std::cout << usage;
return 1;
}
const std::string inFile = argv[1];
const std::string outFile = argv[2];
std::pair<uint, uint> sizeOut(0, 0);
Mat3x3 mat(InitIdent);
for (int i = 3; i < argc; ++i) try {
const std::string cmd = argv[i];
if (cmd == "resize") sizeOut = resize(i, argc, argv);
else if (cmd == "translate") mat = translate(i, argc, argv) * mat;
else if (cmd == "rotate") mat = rotate(i, argc, argv) * mat;
else if (cmd == "scale") mat = scale(i, argc, argv) * mat;
else {
std::cerr << "Wrong command!
";
std::cout << usage;
return 1;
}
} catch (const Error &error) {
std::cerr << "Wrong argument at $" << i << "
"
<< error.text << '
';
std::cout << usage;
return 1;
}
// read image
Image imgSrc;
{ std::ifstream fIn(inFile.c_str(), std::ios::binary);
if (!readPPM(fIn, imgSrc)) {
std::cerr << "Reading '" << inFile << "' failed!
";
return 1;
}
}
// set output image size
if (sizeOut.first * sizeOut.second == 0) {
sizeOut = std::make_pair(imgSrc.w(), imgSrc.h());
}
// transform image
Image imgDst;
imgDst.resize(sizeOut.first, sizeOut.second, 3 * sizeOut.second);
transform(imgSrc, mat, imgDst);
// write image
{ std::ofstream fOut(outFile.c_str(), std::ios::binary);
if (!writePPM(fOut, imgDst) || (fOut.close(), !fOut.good())) {
std::cerr << "Writing '" << outFile << "' failed!
";
return 1;
}
}
// done
return 0;
}
Note:
The command line arguments are processed in order. Each transformation command is multiplied from left to the already combined transformation matrix, starting with an identity matrix. This is because a concatenation of transformations results in the reverse ordered multiplication of matrices. (The matrix multiplication is right associative.)
E.g. the corresponding matrix for a transform:
x' = translate(x)
x" = rotate(x')
x"' = scale(x")
which is
x"' = scale(rotate(translate(x)))
is
Mtransform = Mscale · Mrotate · Mtranslate
and
x"' = Mscale · Mrotate · Mtranslate · x = Mtransform · x
Compiled and tested in cygwin:
$ g++ -std=c++11 -o xformRGBImg image.cc imagePPM.cc linMath.cc xformRGBImg.cc
$ ./xformRGBImg
Missing arguments!
Usage:
xformRGBImg IN_FILE OUT_FILE [[CMD]...]
Commands:
resize W H
translate X Y
rotate ANGLE
scale SX SY
$
Finally, a sample image cat.jpg
(converted to PPM in GIMP):
with size 300 × 300.
Note:
All embedded images are converted from PPM to JPEG (in GIMP again). (PPM is not supported in image upload, nor can I imagine that any browser can display it properly.)
To start with a minimum:
$ ./xformRGBImg cat.ppm cat.copy.ppm
$
It looks like the original – what should be expected by an identity transform.
Now, a rotation with 30°:
$ ./xformRGBImg cat.ppm cat.rot30.ppm rotate 30
$
To rotate about a certain center, there is a resp. translation before and afterwards needed:
$ ./xformRGBImg cat.ppm cat.rot30c150,150.ppm
translate -150 -150 rotate 30 translate 150 150
$
The output image can be resized with w · √2 × h · √2 to fit any center rotation in.
So, the output image is resized to 425 × 425 where the last translation is adjusted respectively to translate 212.5 212.5
:
$ ./xformRGBImg cat.ppm cat.rot30c150,150.425x425.ppm
resize 425 425 translate -150 -150 rotate 30 translate 212.5 212.5
$
The scaling has not yet been checked:
$ ./xformRGBImg cat.ppm cat.rot30c150,150s0.7,0.7.ppm
translate -150 -150 rotate 30 scale 0.7 0.7 translate 150 150
$
Finally, to be fair, I would like to mention the “big brother” of my little toy tool: ImageMagick.
相关文章