AuTop代码整理
Published in:2024-04-28 | category: 智能车
Words: 3.9k | Reading time: 18min | reading:

圆环circle

circle.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include "circle.h"
#include "motor.h"
#include "main.h"

enum circle_type_e circle_type = CIRCLE_NONE;

//方便串口收发
const char *circle_type_name[CIRCLE_NUM] = {
"CIRCLE_NONE",
"CIRCLE_LEFT_BEGIN", "CIRCLE_RIGHT_BEGIN",
"CIRCLE_LEFT_RUNNING", "CIRCLE_RIGHT_RUNNING",
"CIRCLE_LEFT_IN", "CIRCLE_RIGHT_IN",
"CIRCLE_LEFT_OUT", "CIRCLE_RIGHT_OUT",
"CIRCLE_LEFT_END", "CIRCLE_RIGHT_END",
};

// 编码器,用于防止一些重复触发等。
int64_t circle_encoder;

int none_left_line = 0, none_right_line = 0;
int have_left_line = 0, have_right_line = 0;

void check_circle() {
// 非圆环模式下,单边L角点, 单边长直道
if (circle_type == CIRCLE_NONE && Lpt0_found && !Lpt1_found && is_straight1) {
circle_type = CIRCLE_LEFT_BEGIN;
}
if (circle_type == CIRCLE_NONE && !Lpt0_found && Lpt1_found && is_straight0) {
circle_type = CIRCLE_RIGHT_BEGIN;
}
}

void run_circle() {
int64_t current_encoder = get_total_encoder();

// 左环开始,寻外直道右线
if (circle_type == CIRCLE_LEFT_BEGIN) {
track_type = TRACK_RIGHT;

//先丢左线后有线
if (rpts0s_num < 0.2 / sample_dist) { none_left_line++; }
if (rpts0s_num > 1.0 / sample_dist && none_left_line > 2) {
have_left_line++;
if (have_left_line > 1) {
circle_type = CIRCLE_LEFT_IN;
none_left_line = 0;
have_left_line = 0;
circle_encoder = current_encoder;
}
}
}
//入环,寻内圆左线
else if (circle_type == CIRCLE_LEFT_IN) {
track_type = TRACK_LEFT;

//编码器打表过1/4圆 应修正为右线为转弯无拐点
if (rpts0s_num < 0.1 / sample_dist ||
current_encoder - circle_encoder >= ENCODER_PER_METER * (3.14 * 1 / 2)) { circle_type = CIRCLE_LEFT_RUNNING; }
}
//正常巡线,寻外圆右线
else if (circle_type == CIRCLE_LEFT_RUNNING) {
track_type = TRACK_RIGHT;

if (Lpt1_found) rpts1s_num = rptsc1_num = Lpt1_rpts1s_id;
//外环拐点(右L点)
if (Lpt1_found && Lpt1_rpts1s_id < 0.4 / sample_dist) {
circle_type = CIRCLE_LEFT_OUT;
}
}
//出环,寻内圆
else if (circle_type == CIRCLE_LEFT_OUT) {
track_type = TRACK_LEFT;

//右线为长直道
if (is_straight1) {
circle_type = CIRCLE_LEFT_END;
}
}
//走过圆环,寻右线
else if (circle_type == CIRCLE_LEFT_END) {
track_type = TRACK_RIGHT;

//左线先丢后有
if (rpts0s_num < 0.2 / sample_dist) { none_left_line++; }
if (rpts0s_num > 1.0 / sample_dist && none_left_line > 3) {
circle_type = CIRCLE_NONE;
none_left_line = 0;
}
}
//右环控制,前期寻左直道
else if (circle_type == CIRCLE_RIGHT_BEGIN) {
track_type = TRACK_LEFT;

//先丢右线后有线
if (rpts1s_num < 0.2 / sample_dist) { none_right_line++; }
if (rpts1s_num > 1.0 / sample_dist && none_right_line > 2) {
have_right_line++;
if (have_right_line > 1) {
circle_type = CIRCLE_RIGHT_IN;
none_right_line = 0;
have_right_line = 0;
circle_encoder = current_encoder;
}
}
}
//入右环,寻右内圆环
else if (circle_type == CIRCLE_RIGHT_IN) {
track_type = TRACK_RIGHT;

//编码器打表过1/4圆 应修正为左线为转弯无拐点
if (rpts1s_num < 0.1 / sample_dist ||
current_encoder - circle_encoder >= ENCODER_PER_METER * (3.14 * 1 / 2)) { circle_type = CIRCLE_RIGHT_RUNNING; }

}
//正常巡线,寻外圆左线
else if (circle_type == CIRCLE_RIGHT_RUNNING) {
track_type = TRACK_LEFT;

//外环存在拐点,可再加拐点距离判据(左L点)
if (Lpt0_found) rpts0s_num = rptsc0_num = Lpt0_rpts0s_id;
if (Lpt0_found && Lpt0_rpts0s_id < 0.4 / sample_dist) {
circle_type = CIRCLE_RIGHT_OUT;
}
}
//出环,寻内圆
else if (circle_type == CIRCLE_RIGHT_OUT) {
track_type = TRACK_RIGHT;

//左长度加倾斜角度 应修正左右线找到且为直线
//if((rpts1s_num >100 && !Lpt1_found)) {have_right_line++;}
if (is_straight0) {
circle_type = CIRCLE_RIGHT_END;
}
}
//走过圆环,寻左线
else if (circle_type == CIRCLE_RIGHT_END) {
track_type = TRACK_LEFT;

//左线先丢后有
if (rpts1s_num < 0.2 / sample_dist) { none_right_line++; }
if (rpts1s_num > 1.0 / sample_dist && none_right_line > 2) {
circle_type = CIRCLE_NONE;
none_right_line = 0;
}
}
}

// 绘制圆环模式下的调试图像
void draw_circle() {

}

circle.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef CIRCLE_H
#define CIRCLE_H

enum circle_type_e {
CIRCLE_NONE = 0, // 非圆环模式
CIRCLE_LEFT_BEGIN, CIRCLE_RIGHT_BEGIN, // 圆环开始,识别到单侧L角点另一侧长直道。
CIRCLE_LEFT_IN, CIRCLE_RIGHT_IN, // 圆环进入,即走到一侧直道,一侧圆环的位置。
CIRCLE_LEFT_RUNNING, CIRCLE_RIGHT_RUNNING, // 圆环内部。
CIRCLE_LEFT_OUT, CIRCLE_RIGHT_OUT, // 准备出圆环,即识别到出环处的L角点。
CIRCLE_LEFT_END, CIRCLE_RIGHT_END, // 圆环结束,即再次走到单侧直道的位置。
CIRCLE_NUM, //
};

extern const char *circle_type_name[CIRCLE_NUM];

extern enum circle_type_e circle_type;

void check_circle();

void run_circle();

void draw_circle();

#endif // CIRCLE_H

非圆环模式下判断圆环

单边L角点, 单边长直道

1
2
3
4
5
6
7
8
9
void check_circle() {
// 非圆环模式下,单边L角点, 单边长直道
if (circle_type == CIRCLE_NONE && Lpt0_found && !Lpt1_found && is_straight1) {
circle_type = CIRCLE_LEFT_BEGIN;
}
if (circle_type == CIRCLE_NONE && !Lpt0_found && Lpt1_found && is_straight0) {
circle_type = CIRCLE_RIGHT_BEGIN;
}
}

圆环执行

run_circle()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
void run_circle() {
int64_t current_encoder = get_total_encoder();

// 左环开始,寻外直道右线
if (circle_type == CIRCLE_LEFT_BEGIN) {
track_type = TRACK_RIGHT;

//先丢左线后有线
if (rpts0s_num < 0.2 / sample_dist) { none_left_line++; }
if (rpts0s_num > 1.0 / sample_dist && none_left_line > 2) {
have_left_line++;
if (have_left_line > 1) {
circle_type = CIRCLE_LEFT_IN;
none_left_line = 0;
have_left_line = 0;
circle_encoder = current_encoder;
}
}
}
//入环,寻内圆左线
else if (circle_type == CIRCLE_LEFT_IN) {
track_type = TRACK_LEFT;

//编码器打表过1/4圆 应修正为右线为转弯无拐点
if (rpts0s_num < 0.1 / sample_dist ||
current_encoder - circle_encoder >= ENCODER_PER_METER * (3.14 * 1 / 2)) { circle_type = CIRCLE_LEFT_RUNNING; }
}
//正常巡线,寻外圆右线
else if (circle_type == CIRCLE_LEFT_RUNNING) {
track_type = TRACK_RIGHT;

if (Lpt1_found) rpts1s_num = rptsc1_num = Lpt1_rpts1s_id;
//外环拐点(右L点)
if (Lpt1_found && Lpt1_rpts1s_id < 0.4 / sample_dist) {
circle_type = CIRCLE_LEFT_OUT;
}
}
//出环,寻内圆
else if (circle_type == CIRCLE_LEFT_OUT) {
track_type = TRACK_LEFT;

//右线为长直道
if (is_straight1) {
circle_type = CIRCLE_LEFT_END;
}
}
//走过圆环,寻右线
else if (circle_type == CIRCLE_LEFT_END) {
track_type = TRACK_RIGHT;

//左线先丢后有
if (rpts0s_num < 0.2 / sample_dist) { none_left_line++; }
if (rpts0s_num > 1.0 / sample_dist && none_left_line > 3) {
circle_type = CIRCLE_NONE;
none_left_line = 0;
}
}
//右环控制,前期寻左直道
else if (circle_type == CIRCLE_RIGHT_BEGIN) {
track_type = TRACK_LEFT;

//先丢右线后有线
if (rpts1s_num < 0.2 / sample_dist) { none_right_line++; }
if (rpts1s_num > 1.0 / sample_dist && none_right_line > 2) {
have_right_line++;
if (have_right_line > 1) {
circle_type = CIRCLE_RIGHT_IN;
none_right_line = 0;
have_right_line = 0;
circle_encoder = current_encoder;
}
}
}
//入右环,寻右内圆环
else if (circle_type == CIRCLE_RIGHT_IN) {
track_type = TRACK_RIGHT;

//编码器打表过1/4圆 应修正为左线为转弯无拐点
if (rpts1s_num < 0.1 / sample_dist ||
current_encoder - circle_encoder >= ENCODER_PER_METER * (3.14 * 1 / 2)) { circle_type = CIRCLE_RIGHT_RUNNING; }

}
//正常巡线,寻外圆左线
else if (circle_type == CIRCLE_RIGHT_RUNNING) {
track_type = TRACK_LEFT;

//外环存在拐点,可再加拐点距离判据(左L点)
if (Lpt0_found) rpts0s_num = rptsc0_num = Lpt0_rpts0s_id;
if (Lpt0_found && Lpt0_rpts0s_id < 0.4 / sample_dist) {
circle_type = CIRCLE_RIGHT_OUT;
}
}
//出环,寻内圆
else if (circle_type == CIRCLE_RIGHT_OUT) {
track_type = TRACK_RIGHT;

//左长度加倾斜角度 应修正左右线找到且为直线
//if((rpts1s_num >100 && !Lpt1_found)) {have_right_line++;}
if (is_straight0) {
circle_type = CIRCLE_RIGHT_END;
}
}
//走过圆环,寻左线
else if (circle_type == CIRCLE_RIGHT_END) {
track_type = TRACK_LEFT;

//左线先丢后有
if (rpts1s_num < 0.2 / sample_dist) { none_right_line++; }
if (rpts1s_num > 1.0 / sample_dist && none_right_line > 2) {
circle_type = CIRCLE_NONE;
none_right_line = 0;
}
}
}
详细解读
get_total_encoder()
1
2
3
int64_t get_total_encoder() {
return (int64_t) ((motor_l.total_encoder + motor_r.total_encoder) / 2);
}

这段代码定义了一个函数 get_total_encoder(),用于计算左右两个电机的编码器总和并取平均值。具体步骤如下:

  1. 获取左电机和右电机的编码器值 motor_l.total_encodermotor_r.total_encoder
  2. 将左右两个电机的编码器值相加得到总和。
  3. 将总和除以2取平均值,然后以 int64_t 类型返回结果。

track_type:单边得中线的边

1
2
3
4
//编码器打表过1/4圆   应修正为右线为转弯无拐点

if (rpts0s_num < 0.1 / sample_dist ||
current_encoder - circle_encoder >= ENCODER_PER_METER * (3.14 * 1 / 2)) { circle_type = CIRCLE_LEFT_RUNNING; }

这段代码的逻辑似乎与之前的代码相关,用于检测编码器在打表过1/4圆时的状态,并进行相应的修正。让我来解释一下这段代码的作用:

  1. 首先判断条件 rpts0s_num < 0.1 / sample_dist,如果条件成立,说明小车还未行驶到1/4圆的位置,或者右线未被检测到。
  2. 或者判断条件 current_encoder - circle_encoder >= ENCODER_PER_METER * (3.14 * 1 / 2),这个条件看起来是在判断当前编码器值与开始打表时的编码器值之差是否超过了1/4圆的周长对应的编码器值。
  3. 如果以上两个条件有一个成立,就会将 circle_type 设置为 CIRCLE_LEFT_RUNNING,表示小车正在左环行驶的阶段。

在检测到小车行驶过1/4圆或者右线为转弯时将 circle_type 修改为 CIRCLE_LEFT_RUNNING,可能是为了校准小车在左环赛道上行驶时的特定状态。这段代码与之前的代码一起,似乎在实现一个复杂的状态机来控制小车在左环赛道上的运动。如果您有更多的问题或需要进一步解释,请随时告诉我。


什么是编码器打表:
编码器打表(Encoder Look-Up Table)通常指的是在编码器模块中使用查找表(Look-Up Table)来进行编码的过程。在信号处理领域或者通信系统中,编码器的作用是将输入信号转换为特定的编码形式,以便传输、存储或处理。而编码器打表的概念是通过预先构建一个表格,将输入信号映射到特定的编码值,从而实现编码的过程。

在这种情况下,编码器打表的优势在于可以提高编码的速度和效率。通过查找表,编码器可以以常量时间复杂度快速查找到对应的编码值,而不必每次都进行复杂的计算或逻辑判断。这样可以加快编码过程,特别是在需要频繁编码大量数据时,可以节省时间和计算资源。

总的来说,编码器打表是一种提高编码效率的方法,通过预先建立编码映射表,可以在编码过程中快速准确地找到对应的编码值,提高系统性能和速度。


迷宫巡线部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 前进方向定义:
* 0
* 3 1
* 2
*/
AT_DTCM_SECTION_ALIGN_INIT(const int dir_front[4][2], 8) = {{0, -1},
{1, 0},
{0, 1},
{-1, 0}};
AT_DTCM_SECTION_ALIGN_INIT(const int dir_frontleft[4][2], 8) = {{-1, -1},
{1, -1},
{1, 1},
{-1, 1}};
AT_DTCM_SECTION_ALIGN_INIT(const int dir_frontright[4][2], 8) = {{1, -1},
{1, 1},
{-1, 1},
{-1, -1}};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 左手迷宫巡线
AT_ITCM_SECTION_INIT(void findline_lefthand_adaptive(image_t *img, int block_size, int clip_value, int x, int y, int pts[][2], int *num)) {
assert(img && img->data);
assert(num && *num >= 0);
assert(block_size > 1 && block_size % 2 == 1);
int half = block_size / 2;
int step = 0, dir = 0, turn = 0;
while (step < *num && half < x && x < img->width - half - 1 && half < y && y < img->height - half - 1 && turn < 4) {
int local_thres = 0;
for (int dy = -half; dy <= half; dy++) {
for (int dx = -half; dx <= half; dx++) {
local_thres += AT(img, x + dx, y + dy);
}
}
local_thres /= block_size * block_size;
local_thres -= clip_value;

int current_value = AT(img, x, y);
int front_value = AT(img, x + dir_front[dir][0], y + dir_front[dir][1]);
int frontleft_value = AT(img, x + dir_frontleft[dir][0], y + dir_frontleft[dir][1]);
if (front_value < local_thres) {
dir = (dir + 1) % 4;
turn++;
} else if (frontleft_value < local_thres) {
x += dir_front[dir][0];
y += dir_front[dir][1];
pts[step][0] = x;
pts[step][1] = y;
step++;
turn = 0;
} else {
x += dir_frontleft[dir][0];
y += dir_frontleft[dir][1];
dir = (dir + 3) % 4;
pts[step][0] = x;
pts[step][1] = y;
step++;
turn = 0;
}
}
*num = step;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

// 右手迷宫巡线
AT_ITCM_SECTION_INIT(void findline_righthand_adaptive(image_t *img, int block_size, int clip_value, int x, int y, int pts[][2], int *num)) {
assert(img && img->data);
assert(num && *num >= 0);
assert(block_size > 1 && block_size % 2 == 1);
int half = block_size / 2;
int step = 0, dir = 0, turn = 0;
while (step < *num && 0 < x && x < img->width - 1 && 0 < y && y < img->height - 1 && turn < 4) {
int local_thres = 0;
for (int dy = -half; dy <= half; dy++) {
for (int dx = -half; dx <= half; dx++) {
local_thres += AT(img, x + dx, y + dy);
}
}
local_thres /= block_size * block_size;
local_thres -= clip_value;

int current_value = AT(img, x, y);
int front_value = AT(img, x + dir_front[dir][0], y + dir_front[dir][1]);
int frontright_value = AT(img, x + dir_frontright[dir][0], y + dir_frontright[dir][1]);
if (front_value < local_thres) {
dir = (dir + 3) % 4;
turn++;
} else if (frontright_value < local_thres) {
x += dir_front[dir][0];
y += dir_front[dir][1];
pts[step][0] = x;
pts[step][1] = y;
step++;
turn = 0;
} else {
x += dir_frontright[dir][0];
y += dir_frontright[dir][1];
dir = (dir + 1) % 4;
pts[step][0] = x;
pts[step][1] = y;
step++;
turn = 0;
}
}
*num = step;
}

逆透视

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize,
int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT,
const Scalar &borderValue = Scalar())
{
CV_INSTRUMENT_REGION()

CV_Assert(!src.empty());
CV_Assert(src.depth() == CV_8U || src.depth() == CV_32F);
CV_Assert(M.rows() == 3 && M.cols() == 3);

Mat mapx(dsize, CV_32F);
Mat mapy(dsize, CV_32F);

Mat transform(2, 3, CV_64F);
M.getMat().colRange(0, 2).convertTo(transform, transform.type());

buildWarpPerspectiveMaps(transform, flags, dsize, mapx, mapy);

remap(src, dst, mapx, mapy, flags, borderMode, borderValue);
}

这段代码是OpenCV中的warpPerspective函数的实现,用于对输入图像进行透视变换。下面是对代码中的函数及参数的详细解释:

warpPerspective函数参数解释:

  • src:输入图像,类型为InputArray。
  • dst:输出图像,类型为OutputArray。
  • M:3x3的变换矩阵,描述了透视变换,类型为InputArray。
  • dsize:输出图像的尺寸,类型为Size。
  • flags:插值方法,用于指定在进行像素值插值时要使用的方法,默认为INTER_LINEAR。
  • borderMode:边界像素插值模式,用于指定当目标图像像素位于源图像之外时的处理方式,默认为BORDER_CONSTANT。
  • borderValue:边界填充的像素值,类型为Scalar。

warpPerspective函数主要步骤解释:

  1. CV_INSTRUMENT_REGION:用于性能测量和优化的宏定义。

  2. CV_Assert:断言,用于判断条件是否成立,如果条件不成立则会抛出一个异常。

  3. Mat mapxMat mapy:分别用于存储X和Y方向的映射结果。

  4. Mat transform:是一个2x3的矩阵,用于保存M矩阵的前两列,并将其转换为CV_64F类型。

  5. buildWarpPerspectiveMaps:根据透视变换矩阵和指定的插值方法,构建出目标图像在源图像上的映射关系。

  6. remap:根据生成的映射关系对输入图像进行透视变换,并存储结果到输出图像dst中。
    M.getMat().colRange(0, 2).convertTo(transform, transform.type());详细解释
    这行代码是针对OpenCV中的矩阵操作。让我逐步解释一下:

  7. M.getMat(): 这部分获取一个OpenCV的矩阵M

  8. .colRange(0, 2): 这个部分表示对该矩阵M的列进行切片操作,只选择从第0列到第2列(不包括第2列)的部分。

  9. .convertTo(transform, transform.type()): 这一部分是将切片后的列数据进行类型转换并赋值给另外一个矩阵transform。这里的transform是一个目标矩阵,它将包含转换后的数据。transform.type()返回transform矩阵的类型。

因此,整体来看,这行代码的作用是从矩阵M中选择第0列到第1列的数据,并将其转换为指定类型,然后存储到矩阵transform中。

这是 buildWarpPerspectiveMaps 函数的伪代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void buildWarpPerspectiveMaps(Size src_size, Matx33f M, bool inverse_map, Mat& xmap, Mat& ymap) {
int width = src_size.width;
int height = src_size.height;

xmap.create(height, width, CV_32F);
ymap.create(height, width, CV_32F);

for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Vec3f src_pt = Vec3f(x, y, 1); // Source point
Vec3f dst_pt = M * src_pt; // Apply transformation matrix

float map_x, map_y;
if (!inverse_map) {
map_x = dst_pt[0] / dst_pt[2];
map_y = dst_pt[1] / dst_pt[2];
} else {
map_x = src_pt[0] - dst_pt[0];
map_y = src_pt[1] - dst_pt[1];
}

xmap.at<float>(y, x) = map_x;
ymap.at<float>(y, x) = map_y;
}
}
}

这个伪代码实现描述了 buildWarpPerspectiveMaps 函数的大致操作步骤。实际的 buildWarpPerspectiveMaps 函数是 OpenCV 中用于构建透视变换映射的函数,具体实现会更加复杂并且包含了更多细节。如果你需要实际的代码实现,建议查阅 OpenCV 的源代码或者官方文档中提供的具体函数实现。希望这个简要描述能够帮助到你,如果有其他问题或者需要更多帮助,请随时告诉我。

注意事项:

  • 输入图像src不能为空。
  • 输入图像src的深度必须为CV_8U或CV_32F。
  • 变换矩阵M必须为一个3x3矩阵。
  • warpPerspective函数实现了透视变换的核心逻辑,通过构建变换矩阵并利用remap函数实现对图像的透视变换操作。

如果你有任何关于这段代码的疑问或需要进一步解释,请随时告诉我。

@brief Applies a perspective transformation to an image.

The function warpPerspective transforms the source image using the specified matrix:

[$\texttt{dst} (x,y) = \texttt{src} \left ( \frac{M_{11} x + M_{12} y + M_{13}}{M_{31} x + M_{32} y + M_{33}} ,
\frac{M_{21} x + M_{22} y + M_{23}}{M_{31} x + M_{32} y + M_{33}} \right )$]

when the flag #WARP_INVERSE_MAP is set. Otherwise, the transformation is first inverted with invert
and then put in the formula above instead of M. The function cannot operate in-place.

@param src input image.
@param dst output image that has the size dsize and the same type as src .
@param M $ 3\times 3 $ transformation matrix.
@param dsize size of the output image.
@param flags combination of interpolation methods (#INTER_LINEAR or #INTER_NEAREST) and the
optional flag #WARP_INVERSE_MAP, that sets M as the inverse transformation (
$\texttt{dst}\rightarrow\texttt{src} $ ).
@param borderMode pixel extrapolation method (#BORDER_CONSTANT or #BORDER_REPLICATE).
@param borderValue value used in case of a constant border; by default, it equals 0.

@sa warpAffine, resize, remap, getRectSubPix, perspectiveTransform

Prev:
一些图像处理的函数
Next:
逆透视