[英]Rotate an image in C++ without using OpenCV functions
说明:-我试图在不使用C ++中使用OpenCV函数的情况下旋转图像。 旋转中心不必是图像的中心。 这可能是一个不同的点(与图像中心的偏移)。 到目前为止,我跟着各种各样的来源做图像插值和我知道的一个来源 ,其完美地完成这项工作在MATLAB。 我试图在没有OpenCV函数的C ++中模仿相同的东西。 但是我没有得到预期的旋转图像。 相反,我的输出在屏幕上看起来像一条小水平线。
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);
}
在受到与此问题相关的许多答案以及下面最详尽,最有用和最慷慨的答案的启发之后,我可以修复我的OpenCV代码以获得所需的结果。
修改后的代码:
// Trivial constant
constexpr double Pi = 3.1415926535897932384626433832795;
/*!
* \brief 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
* \return 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);
}
/*!
* \brief Function to apply coordinate transform from destination to source
* \param inv_mat being the inverse transformation matrix for the transform needed
* \return 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));
}
/*!
* \brief 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);
}
}
}
/*!
* \brief User manual for command-line args input
*/
void Usage() {
std::cout << "COMMAND INPUT : - \n\n" <<
" ./ImageTransform <image> <rotation-angle>" <<
std::endl;
}
/*!
* \brief 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;
}
首先,我必须承认我同意generic_opto_guy :
使用循环的方法看起来不错,因此我们需要检查数学。 在我注意到的事情上:如果(oldRow> 0 && oldCol> 0 && oldRow <= oldHeight && oldCol <= oldWidth)表示您从1开始索引。我相信opencv从0开始索引。
对于所有这些,我都忍不住回答。 (可能是,这只是我的图像阶段。)
我建议不要使用sin()和cos(),而是建议使用矩阵变换。 乍一看,这可能看起来是过度设计的,但是稍后您将意识到它具有更大的灵活性。 使用转换矩阵,您可以表达很多转换(平移,旋转,缩放,剪切,投影),以及将多个转换组合到一个矩阵中。
(一个可能的预告片: SO:如何在2D中绘制/变形QImage? )
在图像中,像素可以由2d坐标寻址。 因此,想到了2×2矩阵,但是2×2矩阵无法表达翻译。 为此,引入了齐次坐标 -通过将尺寸扩展一倍来处理同一空间中的位置和方向的数学技巧。
简而言之,二维位置(x,y)具有齐次坐标(x,y,1)。
用转换矩阵转换的位置:
v´ = M · v 。
这可能会或可能不会更改第三部分的值。 为了再次将齐次坐标转换为2D位置,x和y必须除以3rd分量。
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);
}
}
}
注意:
变换矩阵mat
描述了源图像坐标到目标图像坐标的变换。 嵌套循环遍历目标图像。 因此,必须使用逆矩阵(代表逆变换)来获取映射到当前目标坐标的相应源图像坐标。
…以及旋转的矩阵构造函数:
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)
{ }
可用于构造一个angle
旋转(以度为单位):
Mat3x3T<double> mat(InitRot, degToRad(30.0));
注意:
我想强调一下如何使用转换后的坐标:
const Vec2 pos = transform(matInv, Vec2(x, y));
const int xSrc = (int)(pos.x + 0.5), ySrc = (int)(pos.y + 0.5);
将结果取整以产生一个离散像素位置实际上就是所谓的“最近邻居”。 可替代地,现在丢弃的小数部分可以用于相邻像素之间的线性插值。
为了制作一个小样本,我首先从我最近写的另一个答案中复制了image.h
, image.cc
, imagePPM.h
和imagePPM.cc
。 (已使用PPM文件格式,因为它只需要用于文件I / O的最少代码。)
接下来,我使用linMath.h
(我的3D转换最小数学集合)为2D转换创建最小数学集合– 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 << '\t' << m._01 << '\t' << m._02 << '\n'
<< m._10 << '\t' << m._11 << '\t' << m._12 << '\n'
<< m._20 << '\t' << m._21 << '\t' << m._22 << '\n';
}
/* 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
以及linMath.cc
中Pi
的定义:
#include "linmath.h"
const double Pi = 3.1415926535897932384626433832795;
在拥有所有可用工具的情况下,我制作了示例应用程序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:\n"
" xformRGBImg IN_FILE OUT_FILE [[CMD]...]\n"
"\n"
"Commands:\n"
" resize W H\n"
" translate X Y\n"
" rotate ANGLE\n"
" scale SX SY\n";
int main(int argc, char **argv)
{
// read command line arguments
if (argc <= 2) {
std::cerr << "Missing arguments!\n";
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!\n";
std::cout << usage;
return 1;
}
} catch (const Error &error) {
std::cerr << "Wrong argument at $" << i << "\n"
<< error.text << '\n';
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!\n";
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!\n";
return 1;
}
}
// done
return 0;
}
注意:
命令行参数按顺序处理。 从单位矩阵开始,将每个转换命令从左向已组合的转换矩阵相乘。 这是因为变换的串联导致矩阵的逆序乘法。 (矩阵乘法是右关联的。)
例如,用于转换的相应矩阵:
x' = 翻译 ( x )
x“ = 旋转 ( x' )
x“' = 比例尺 ( x” )
这是
x“' = 比例尺 ( 旋转 ( 平移 ( x )))
是
M 变换 = M 比例 · M 旋转 · M 平移
和
x“' = M 比例 · M 旋转 · M 平移 · x = M 变换 · x
在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
$
最后,生成一个样本图像cat.jpg
(在GIMP中转换为PPM ):
尺寸为300×300。
注意:
所有嵌入的图像都从PPM转换为JPEG(再次在GIMP中 )。 (图片上传不支持PPM,我也无法想象任何浏览器都能正确显示它。)
首先开始:
$ ./xformRGBImg cat.ppm cat.copy.ppm
$
看起来很原始-身份转换应该期望什么。
现在,旋转30°:
$ ./xformRGBImg cat.ppm cat.rot30.ppm rotate 30
$
要绕某个中心旋转,有一个响应。 之前和之后需要的翻译:
$ ./xformRGBImg cat.ppm cat.rot30c150,150.ppm \
translate -150 -150 rotate 30 translate 150 150
$
可以使用w·√2×h·√2调整输出图像的大小以适合任何中心旋转。
因此,将输出图像的大小调整为425×425,在此位置上一次转换分别被调整为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
$
缩放比例尚未检查:
$ ./xformRGBImg cat.ppm cat.rot30c150,150s0.7,0.7.ppm \
translate -150 -150 rotate 30 scale 0.7 0.7 translate 150 150
$
最后,为了公平起见,我想提到我的小玩具工具的“老大哥”: ImageMagick 。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.