一、背景
在OpenCV中,可以画圆、线、矩形、椭圆和多边形,但并不能画出虚线,现希望通过OpenCV已有的函数画出由点或线组成的虚线。
cv::circle() // 画一个简单圆
cv::clipLine() // 判断一条直线是否在给定的矩形内
cv::ellipse() // 画一个椭圆,可以倾斜,或者只有部分圆弧
cv::ellipse2Poly() // 计算一个近似椭圆的多边形
cv::fillConvexPoly() // 画一个填充的简单多边形
cv::fillPoly() // 画一个填充的任意多边形
cv::line() // 画一条简单直线
cv::rectangle() // 画一个简单矩形
cv::polyLines() // 画多重折线
二、实现
注意事项:
1、OpenCV虽然没有画点的函数,但可通过cv::circle()实现,只要半径够短,线够厚,一个实心圆可看成一个点。
2、创建空白图片时,其图片类型应选择CV_32FC3
或CV_64FC3
。存储像素的位数越多,画出的斜线越直。
3、两个点画出一条线,故使用g_first
和g_last
两个Point
来存储所画的线。画线主要分为水平线、垂直线和倾斜线三种情况。倾斜线表示方法如下,设置每个点或线的间隔,已知 x x x即可求出 y y y,由此可确定每个点或每条线的坐标。
y − y 1 y 2 − y 1 = x − x 1 x 2 − x 1 ⇒ y = ( y 2 − y 1 ) ( x 2 − x 1 ) ∗ ( x − x 1 ) + y 1 \frac{y-y_1}{y_2-y_1} = \frac{x-x_1}{x_2-x_1} \Rightarrow y = \frac{(y2-y1)}{(x2-x1)}*(x-x1)+y1 y2−y1y−y1=x2−x1x−x1⇒y=(x2−x1)(y2−y1)∗(x−x1)+y1
操作方法:
1、运行程序,在空白图片上按下鼠标左键并移动,鼠标左键释放时即可画出一条虚线。鼠标右键点击可擦掉已画出的虚线。
2、程序包含2种模式,模式1画出由点组成的虚线,模式2画出由线组成的虚线。鼠标默认使用模式1,可通过键盘输入1
或2
选择不同模式。键盘输入除1
或2
的键会自动退出程序。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
Point2f g_first, g_last; // 线的起点和终点
// 画由点组成的虚线
void draw_dotted_line1(Mat img, Point2f p1, Point2f p2, Scalar color, int thickness)
{
float n = 15; //虚点间隔
float w = p2.x - p1.x, h = p2.y - p1.y;
float l = sqrtf(w * w + h * h);
int m = l / n;
n = l / m; // 矫正虚点间隔,使虚点数为整数
circle(img, p1, 1, color, thickness); // 画起点
circle(img, p2, 1, color, thickness); // 画终点
// 画中间点
if (p1.y == p2.y) // 水平线:y = m
{
float x1 = min(p1.x, p2.x);
float x2 = max(p1.x, p2.x);
for (float x = x1 + n; x < x2; x = x + n)
circle(img, Point2f(x, p1.y), 1, color, thickness);
}
else if (p1.x == p2.x) // 垂直线, x = m
{
float y1 = min(p1.y, p2.y);
float y2 = max(p1.y, p2.y);
for (float y = y1 + n; y < y2; y = y + n)
circle(img, Point2f(p1.x, y), 1, color, thickness);
}
else // 倾斜线,与x轴、y轴都不垂直或平行
{
// 直线方程的两点式:(y-y1)/(y2-y1)=(x-x1)/(x2-x1) -> y = (y2-y1)*(x-x1)/(x2-x1)+y1
float m = n * abs(w) / l;
float k = h / w;
float x1 = min(p1.x, p2.x);
float x2 = max(p1.x, p2.x);
for (float x = x1 + m; x < x2; x = x + m)
circle(img, Point2f(x, k * (x - p1.x) + p1.y), 1, color, thickness);
}
}
// 画由线组成的虚线
void draw_dotted_line2(Mat img, Point2f p1, Point2f p2, Scalar color, int thickness)
{
float n = 15; //线长度
float w = p2.x - p1.x, h = p2.y - p1.y;
float l = sqrtf(w * w + h * h);
// 矫正线长度,使线个数为奇数
int m = l / n;
m = m % 2 ? m : m + 1;
n = l / m;
circle(img, p1, 1, color, thickness); // 画起点
circle(img, p2, 1, color, thickness); // 画终点
// 画中间点
if (p1.y == p2.y) //水平线:y = m
{
float x1 = min(p1.x, p2.x);
float x2 = max(p1.x, p2.x);
for (float x = x1, n1 = 2 * n; x < x2; x = x + n1)
line(img, Point2f(x, p1.y), Point2f(x + n, p1.y), color, thickness);
}
else if (p1.x == p2.x) //垂直线, x = m
{
float y1 = min(p1.y, p2.y);
float y2 = max(p1.y, p2.y);
for (float y = y1, n1 = 2 * n; y < y2; y = y + n1)
line(img, Point2f(p1.x, y), Point2f(p1.x, y + n), color, thickness);
}
else // 倾斜线,与x轴、y轴都不垂直或平行
{
// 直线方程的两点式:(y-y1)/(y2-y1)=(x-x1)/(x2-x1) -> y = (y2-y1)*(x-x1)/(x2-x1)+y1
float n1 = n * abs(w) / l;
float k = h / w;
float x1 = min(p1.x, p2.x);
float x2 = max(p1.x, p2.x);
for (float x = x1, n2 = 2 * n1; x < x2; x = x + n2)
{
Point p3 = Point2f(x, k * (x - p1.x) + p1.y);
Point p4 = Point2f(x + n1, k * (x + n1 - p1.x) + p1.y);
line(img, p3, p4, color, thickness);
}
}
}
// 矫正坐标
void correct(Point2f &p, const Point2f &p1, const Point2f &p2)
{
if (p.x < p1.x)
p.x = p1.x;
else if (p.x > p2.x)
p.x = p2.x;
if (p.y < p1.y)
p.y = p1.y;
else if (p.y > p2.y)
p.y = p2.y;
}
// 鼠标回调函数
void mouse_callback(int event, int x, int y, int flags, void *param)
{
static size_t i = 0; // 鼠标操作步骤的标识
switch (event)
{
case cv::EVENT_LBUTTONDOWN: // 鼠标左键点击
if (i == 0)
{
g_first = Point2f(x, y); // 保存线的起点
g_last = Point2f(x + 1, y + 1); // 保存线的终点
i = 1;
}
break;
case cv::EVENT_MOUSEMOVE: // 鼠标移动
if (i == 1)
g_last = Point2f(x, y); // 刷新线的终点
break;
case cv::EVENT_LBUTTONUP: // 鼠标左键释放
if (i == 1)
i = 2; // 不再画线
break;
case cv::EVENT_RBUTTONDOWN: // 鼠标右键点击,清除内容
i = 0;
g_first = Point2f(0, 0);
g_last = Point2f(0, 0);
break;
default:
break;
}
}
int main()
{
const int w = 800, h = 600;
const string window_name = "image";
Mat image_original = cv::Mat(h, w, CV_32FC3, cv::Scalar(255, 255, 255)); // 选择CV_32FC3,画出的斜线更直
cv::imshow(window_name, image_original);
cv::setMouseCallback(window_name, mouse_callback); // 鼠标函数
char flag = '1';
while (true)
{
Mat img = image_original.clone(); // 克隆图片,方便在同一张图片上多次画线
correct(g_last, Point2f(0, 0), Point2f(img.cols, img.rows));
if (flag == '1')
draw_dotted_line1(img, g_first, g_last, Scalar(0, 255, 0), 2); // 模式1画由点组成的虚线
else
draw_dotted_line2(img, g_first, g_last, Scalar(0, 255, 0), 2); // 模式2画由线组成的虚线
imshow(window_name, img);
char c = waitKey(1); // 获取键盘输入的字符
if (c == '1' || c == '2')
flag = c;
else if (c > 0) // 若不按'1'或'2',按其它字符键会自动退出。
break;
}
return 0;
}
运行结果: