C++跨平台“点绘机” 代码解读

news2024/7/6 19:17:23

前言

球球大作战可以自定义皮肤,用画刷绘制。
想着用软件来绘制。

初次尝试,没有达成最终目的,不过也有很大收获。

仓库链接:https://github.com/sixsixQAQ/dolphin

问题

这个半成品,已经有了基本结构了,而且做了跨平台处理。就当作代码案例来讲了,记录自己的思考。 太完美的东西反而无迹可寻。

后面准备第二代“行绘机”,不再更新它,因为它的出发点就是错的:

设计时是基于“点绘”实现的,忽略了实际的画笔粗细,当绘图板提供的画笔较粗时,根本无法完成精细绘制。(实际上,应该采用“行绘”来实现。)

但是如果画笔可以很细,比如windows自带绘图板mspaint,仍然可以使用。

效果

在这里插入图片描述
请添加图片描述

功能实现 && 代码解读

C++代码600 ~ 700行。

主要用Qt框架、平台API、pthread线程来实现。
由于对Qt不是不是很熟悉,很多地方可能设计/实现不是最佳。

实现划分

$ ls include
Image.h  MainWindow.h  Mouse.h  api.h

头文件反映了实现上的划分。

1. 伪API设计

首先,因为要跨平台,所以封装了特定平台的API,声明在api.h中。

api.h:

#ifndef API_H
#define API_H

void init_API();

void API_mouse_down();

void API_mouse_up();

void API_get_mouse_pos(int *x, int*y);

void API_set_mouse (int x, int y);

void destroy_API();

#endif

(Linux的API并未完全封装,因为主要在Windows上测试,频繁切换系统很费时间,也不打算再封装了。)

我们且称这些封装过后的API为伪API,目的就是提供统一的接口。

一看名字你就知道伪API怎么用了。
可是你理解为什么要有某个伪API,参数为什么要那样设置、功能为什么要那样划分吗?

(1) void API_mouse_down()和void API_mouse_up()

一个模拟鼠标点击的伪API。

 void API_mouse_click()

为什么要有这个伪API?
因为Qt、标准库都没有为我们提供模拟鼠标点击的操作。

Qt确实有模拟鼠标单击的,但是只能作用于Qt的组件,而我们需要点击非Qt组件。


如何设计这个接口?

我们很容易这样想:void API_mouse_click()

还有人可能:void API_mouse_click(int delay),给点击之间加上间隔。
要不要这样这样?我觉得不要,因为delay完全可以由库使用者在外部分设置,他可以在调用之间写上sleep()usleep(),多一个参数就是多余,对大家都是负担。C语言的重要特点就是精简,只提供一个达到目的的方式,重复并不好。

然而,click这个语义本身就不好,它不够细化。
假设库使用者要实现按下鼠标后持续一会儿呢?比如实现拖拽什么的。

哦,你或许会想,
设计成这样:void API_mouse_click(int up_down_delay),然后让使用者传一个参数。
首先,多了一个参数,加重了实现和调用的负担;
最致命的,调用者可能无法提前知道这个参数的值——比如要将左上角的图标拖到右下角,我怎么知道要拖多久,从而给出一个参数?


所以如果你不想重复实现某些东西,而且让接口更精简,就要将click分成两段,最终声明入下:

void API_mouse_down();

void API_mouse_up();

你可能要问我如何区分左键、右键、中间键?
然而我自始至终都只需要左键点击,让不需要的功能见鬼去吧,你不会需要它的,它只会白白让你的代码变复杂。

即便以后真的需要,再加上也不困难。

(2)void void API_set_mouse (int x, int y)

为什么要有这个伪API?

确实,Qt的QCursoe::setPos()足以设置鼠标到桌面任何位置。
它在Linux上工作很好。
然而在Windows下,当它和鼠标点击API(windows API)一起工作时,总是看起来丢掉了一些调用,绘制的图形会少了些许点,也就是进行了缩放。
然而像素数量并没有变多。后面会提及缩放的问题。

在这里插入图片描述
理想的绘制如下:
在这里插入图片描述

(3) void API_get_mouse (int *x, int *y);

为什么要有这个伪API?

确实,Qt的QCursoe::pos()足以获取桌面上的鼠标坐标。
它在Linux上1920 *1080的分辨率下工作很好;

然而在Windows下,最大可达位置只有1536 *864,少掉的数字去哪里了?

我发现是QCursoe::setPos()会受到桌面缩放的的影响。
在这里插入图片描述
当你将缩放设置100%时,它将看起来和预期一般。

这意味着什么?
这意味着QCursor::pos()的坐标是相对于虚拟的缩放后的桌面,而不是相对于真实的分辨率。

即便是受缩放影响也无妨,可是由于前面的API_mouse_pos()使用了平台的API,而Windows平台的API将不受缩放影响,

这会导致虽然Qcursoe::pos()告诉我们鼠标在(1534, 864)——在虚拟桌面上代表着最右下角的坐标,但是用Windows API去将鼠标设置到(1534, 854)却无法设置到桌面右下角,因为它不考虑缩放,会将(1534, 854)视作真实的分辨率。

这无疑是一种欺骗,而且用户也不会喜欢你告诉他一个虚拟的坐标,而是真实的坐标。

另外,void API_get_mouse (int *x, int *y),通过指针回传坐标,而非返回值,因为返回值只能有一个。
我知道你要说结构体了,这样引入了新的结构,对大家都是一种负担。传两个坐标毕竟不是那么复杂。
没有使用int&来引用,这样C、C++都使用。

(4)void init_API()和void destroy_API()

为什么要有这两个伪API?

起因是Linux下的桌面API调用都需要这样:

 Display * display = XOpenDisplay(NULL);
	/*
	 *	利用display完成一些API调用
	 *	...
	*/
XCloseDisplay (display)

这看起来不会是一笔小的开销,特别是对于鼠标移动、点击、位置获取这样的超频繁调用。

我们只需要打开次、关闭一次即可。
gcc的__attribute__((constructor))__attribute__((destructor))很好,可是它们依赖编译器。

C语言的实现风格,就是设置初始化函数,别无他法。

另外,即使是Windows也能从中获益,下面是windows下的实现:

static INPUT G_mouse_down;
static INPUT G_mouse_up;

void init_API()
{
    G_mouse_down.type = INPUT_MOUSE;
    G_mouse_down.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;

    G_mouse_up.type = INPUT_MOUSE;
    G_mouse_up.mi.dwFlags = MOUSEEVENTF_LEFTUP;
}

void API_mouse_down()
{
    SendInput(1, &G_mouse_down, sizeof(INPUT));
}
void API_mouse_up()
{
    SendInput(1, &G_mouse_up, sizeof(INPUT));
}

有了init就给了我们一个便利:重复的东西只用初始化一次。

虽然这里的INPUT类型直接静态初始化也可完成,但是如果以后有需要动态初始化的类型,也能很方便地实现。

2. Mouse.h

这是Mouse类的实现。

class Mouse final {
public:
	static Mouse &get_instance();
	//获取鼠标位置
	QPoint get_pos() const;
	//移动鼠标
	void move_to (const QPoint &pos);
	//单击鼠标左键
	void click (int down_up_delay=0);
private:
	Mouse();
	~Mouse();
	Mouse (const Mouse &) = delete;
	Mouse &operator= (const Mouse &) = delete;
};

前面已经封装好API接口了。
为什么要有这个类?
两点原因:

  1. 屏蔽实现,可能并不需要调用API,比如可以调用Qt库的函数。
  2. 直接调用API不好用,我们更希望以面向对象的方式。

如何设计这个类?
很显然鼠标是唯一资源,典型的单例模式适用者。

有关C++的单例模式,请参考另一篇博客:4. 单例模式(Singleton)

既然是单例模式,就无需过多考虑继承、虚函数的问题了,因为这个模式的一个特点就是不适合继承。

只需简简单单地再封装一层接口,调用底层伪API / Qt函数即可。

3. Image.h

class RGB {
public:
	RGB (uint8_t r, uint8_t g, uint8_t b);
	uint8_t get_red() const;
	uint8_t get_green() const;
	uint8_t get_blue() const;
private:
	uint8_t m_red, m_green, m_blue;
};

class fileOpenError : runtime_error {
public:
	fileOpenError (const string &msg);
};

class Image {
public:
	Image (const string &file);
	RGB pixel_at (int x, int y);
	int get_width() const;
	int get_height() const;
private:
	QImage m_image;
};

很明显RGB类是服务于Image类的,不必说它。

为什么要有Image类?
老实说,起初我是用OpenCV来实现这个类,OpenCV中获取像素需要手动的方式。而且OpenCV的效率理应比QImage更高,

封装这个类用来屏蔽掉底层操作。

后来发现,始终无法在Windows上完美地用MinGW编译OpenCV,总是缺东西、缺特性,心里不安,索性不用它了。

底层便重新用QImage来实现了,使用者无需改变,只需要将Image实现改写即可。

很明显这个类降低了接口使用和接口实现的耦合度,体现了面向对象的优点。

4. MainWindow.h

下来是重活了,

#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H

#include <QWidget>
#include <memory>
#include <pthread.h>

class QLabel;
class QPushButton;
class QLineEdit;
class QSpinBox;
class QVBoxLayout;
class QHBoxLayout;

namespace YQ {

using std::unique_ptr;
using std::shared_ptr;
using std::make_unique;
using std::make_shared;

class Image;

class MainWindow : public QWidget {
	Q_OBJECT
public:
	MainWindow();
	~MainWindow();
private:
	shared_ptr<Image> m_image;

    bool m_Signal_terminate_draw;
    void keyPressEvent(QKeyEvent *e)override;

    pthread_mutex_t m_Mutex_update_pos;
	bool m_Signal_terminate_update_pos;
    bool m_Signal_terminate_check_hotkey;
	pthread_t m_Thread_update_pos;
    pthread_t m_Thread_check_hotkey;

	static void *update_pos (void *instance);
    static void *check_hotkey(void *instance);
private:
	QLabel *m_Label_image_wrapper;
	QPushButton *m_Button_start;
	QPushButton *m_Button_choose_image;
	
	QLineEdit *m_LineEdit_img_size;
	QLineEdit *m_LineEdit_current_x;
	QLineEdit *m_LineEdit_current_y;
	QLineEdit *m_LineEdit_draw_center_x;
	QLineEdit *m_LineEdit_draw_center_y;
	QLineEdit *m_LineEdit_draw_radius;
	QLineEdit *m_LineEdit_rgb_begin;
	QLineEdit *m_LineEdit_rgb_end;
	QLineEdit *m_LineEdit_click_delay;//毫秒数
    QLineEdit *m_LineEdit_down_up_delay;
    QLineEdit *m_LineEdit_row_dilute_ratio;//无损绘制的稀散程度,拉伸率
    QLineEdit *m_LineEdit_column_dilute_ratio;
    QSpinBox *m_SpinBox_row_pixel_step;//有损绘制的步长,像素数为单位
    QSpinBox *m_SpinBox_column_pixel_step;
    bool all_args_filled();
private slots:
	void connect_all_slots();
	void on_Button_start_clicked();
	void on_Button_choose_image_clicked();
	
	void on_MainWindow_request_to_update_cursor_pos (int x, int y);//更新控件
signals:
	void request_to_update_cursor_pos (int x, int y);//子线程请求主线程更新控件的信号
};

}

#endif // MAIN_WINDOW_H

首先,下面的这些前置声明:

class QLabel;
class QPushButton;
class QLineEdit;
class QSpinBox;
class QVBoxLayout;
class QHBoxLayout;

这是《Effiective C++》中推荐的做法,降低文件之间的相互依赖。
同时,成员得用指针 / 引用 / shared_ptr<>

但是不能是unique_ptr<>,这好像是因为unique_ptr<>需要在展开的地方就要获取类型大小(用于析构)。

然后,一个Q_OBJECT,因为我们用到了Qt的信号和槽机制。

关于Q_OBJECT,见另一篇博客:Qt核心特点

下来的成员:

private:
	shared_ptr<Image> m_image;

    bool m_Signal_terminate_draw;
    void keyPressEvent(QKeyEvent *e)override;

    pthread_mutex_t m_Mutex_update_pos;
	bool m_Signal_terminate_update_pos;
    bool m_Signal_terminate_check_hotkey;
	pthread_t m_Thread_update_pos;
    pthread_t m_Thread_check_hotkey;

	static void *update_pos (void *instance);
    static void *check_hotkey(void *instance);

除了m_Image外,其他都是线程的。

先简单说下,后面实现详细说:

  • bool m_Signal_terminate_draw;
    • 如其名,Signal,用来终止绘制过程的信号,其实就是个flag,主线程在绘制之前会将它设为false,然后开始绘制循环。循环中会不断检测这个flag,如果为true(被其他人设置了),就会停止绘制。
  • void keyPressEvent(QKeyEvent *e)override;
    • 覆写父类的按键事件处理程序。当按ESC时会将m_Signal_terminate_draw设置为true,导致绘制循环的终止。
    • 仅作为对照:这个函数其实没用,因为当焦点不在Qt组件上时, Qt程序不会收到事件,就不会触发这个函数。所以后面用平台API实现了按键检测。
  • pthread_mutex_t m_Mutex_update_pos;
    • 互斥元,有个线程用于更新当前鼠标位置,更新之前它需要lock这个锁。
    • 它的作用就是当开始绘制时,集中CPU去绘制。绘制线程(主线程)会lock这个锁,阻止鼠标位置更新线程使用CPU(浪费资源)。
  • bool m_Signal_terminate_update_pos;
    • 和前面类似,用于终止鼠标位置更新线程。
  • bool m_Signal_terminate_check_hotkey;
    • 用于终止按键检测线程。
  • pthread_t m_Thread_update_pos;
    • 鼠标位置更新线程。
  • pthread_t m_Thread_check_hotkey;
    • 按键检测线程
  • static void *update_pos (void *instance);
    • 鼠标位置更新线程的入口函数,参数为MainWindow *
  • static void *check_hotkey(void *instance);
    • 按键检测线程的入口函数,参数为MainWindow *
(1)鼠标位置获取线程
/**
 * 子线程,用来更新当前鼠标位置。
 * 一定不要直接操作主线程的控件,会有bug。
 * 用Qt的信号与槽机制,发送信号,请求主线程去更新自己的控件。
 */
void *MainWindow::update_pos (void *instance_)
{
	auto instance = static_cast<MainWindow *> (instance_);
	for (; !instance->m_Signal_terminate_update_pos;) {
        pthread_mutex_lock(&instance->m_Mutex_update_pos);
		auto pos = Mouse::get_instance().get_pos();
		emit instance->request_to_update_cursor_pos (pos.x(), pos.y());
        pthread_mutex_unlock(&instance->m_Mutex_update_pos);
        usleep(10000);
	}
	return NULL;
}

这就是鼠标位置获取线程的全部了。
一个很重要的就是不要直接操作主线程的控件,不然会跑着跑着就报“段错误”,导致程序终止。

这里采用官方比较推荐的做法,利用Qt的信号与槽机制,发射信号,主线程接收到信号后调用槽函数,完成更新。

这里加上usleep(10000),不让它太快,够用就行了,在你的视觉效果看起来没什么区别,可是对CPU来说,却轻松了10000倍!!!
你可以打开任务管理器看看,如果没有这一行,CPU占用会相当高,少则20%,多则50%。而有了这一行后,只有百分之零点几,撑死2%。

提醒:这里用了usleep(),已经破坏跨平台性了,MSVC不能用,但是MinGW仍然能用,赶时间就没有进一步处理,知道就好。

(2) 按键检测线程

#ifdef WIN32
#include <windows.h>
namespace YQ{

void *MainWindow::check_hotkey(void *instance_) {
    auto instance = static_cast<MainWindow*>(instance_);
    if(!RegisterHotKey(NULL,1,0,VK_ESCAPE)) {
        QMessageBox::warning(instance, "错误", "ESC热键注册失败");
        return NULL;
    }
    MSG msg;
    while(GetMessage(&msg,NULL,0,0) &&!instance->m_Signal_terminate_check_hotkey){
        if(msg.message == WM_HOTKEY){
            if(msg.wParam == 1){
                instance->m_Signal_terminate_draw = true;
            }
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    UnregisterHotKey(NULL,1);
    return NULL;
}

}
#endif

赶进度,就没有进行API的细分,直接一整个条件编译写了进去,其实这也不太好,Linux的版本也没写。

这个线程的业务就一行:

 instance->m_Signal_terminate_draw = true;

当接收到VK_ESCAPE(ESC)键后,将这个flag位设置会true,这样,绘制循环下一波就会因为检测到这个flag为true而提前退出。

其他都是常规代码。

(3)小结

细节还有很多没说,说不过来。
比如构造函数里面这个:

m_Signal_terminate_update_pos = false;
m_Signal_terminate_check_hotkey= false;
pthread_create (&m_Thread_update_pos, NULL, MainWindow::update_pos, this);
pthread_create(&m_Thread_check_hotkey, NULL, MainWindow::check_hotkey,this);

这个顺序就不能变,flag变量得再线程创建之前就设置好,不然线程刚一创建就可能检测到flag为true从而退出了。

错误&&解决思路

这个过程中,遇到过两个大问题。

  1. 内存泄漏 / 二次释放。
  2. windows下内存占用巨高。

1. 内存泄漏 / 二次释放

这个问题,主要是因为没有注意组件的parent

ainWindow::MainWindow()
{
	auto *Label_show = new QLabel (this);
	auto *Label_current_img_size = new QLabel (this);
	auto *Label_input_center = new QLabel (this);
	auto *Label_input_radius = new QLabel (this);
	auto *Label_input_rgb_interval = new QLabel (this);
	auto *Label_input_click_delay = new QLabel (this);
    auto *Label_input_down_up_delay = new QLabel(this);
    auto *Label_input_pixel_degree = new QLabel(this);
    auto *Label_input_dilute_ratio = new QLabel(this);
	auto *VBox_top = new QVBoxLayout();
	auto *Grid_labels_and_blanks = new QGridLayout();
	auto *HBox_buttons = new QHBoxLayout();
......

看上面的例子,这些标签、布局组件,我并没有把他们放到成员变量里,是因为不能放吗?
当然可以作为成员,只是没必要。

一个布局组件、一个指示性标签,有必要作为成员吗?我们又不需要在运行时读取/设置它们。

成员越多,我们要管理的就越多,完全可以让它们放野。

可是这些类谁来管?

答案是让parent接管。

继承自QObjectQWidget的类,都会接管它们的孩子的内存管理任务。只需要在孩子中调用child->setParent()来设置parent即可。

设置过parent的组件,会由parent来释放内存,此时如果再释放就二次释放了。

QGridLayoutQHBoxLayout等布局组件,不会负责子组件的内存释放。但是它会负责子布局的内存释放。——这就是说,addLayout()方法添加的布局,由父布局管理,addWidget()方法添加的组件,不由该布局管理。

如果你对组件之间的关系感到疑惑,可以调用QObject::dumpObjectTree()来看看关系树。

2. Windows内存占用巨高

这个程序刚在Linux下测试好时,在linux下占用也就十几二十MB的样子,我还拿valgrind进行了完整的检查,没有确定的内存泄漏。

拿到Windows下时,直接炸了——内存占用直飙8G、9G,CPU也占了50%,甚至屏闪、宕机。

人都傻了,我还以为是极其隐蔽的内存泄漏。
拿工具测了又测,都说没有。

最终慢慢测试了出来,问题有两点,

  1. 鼠标位置更新线程中,位置获取API调用太频繁了,这导致CPU占用很高。。
  2. Windows鼠标点击API的封装不太好。

起初我是这么封装的:

void API_mouse_click()
{
	INPUT input[2];
    input[0].type = INPUT_MOUSE;
    input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;

    input[1].type = INPUT_MOUSE;
    input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
    SendInput(2, &input, sizeof(INPUT));
}

当鼠标快速点击时,内存直接爆满。

虽然我初步推断是:每次调用都会创建栈变量INPUT,有不必要的开销。

但也不尽合理,因为栈变量,栈帧剥离也就随之消亡了,难道函数调用会堆积到如此地步?

未解之谜,所幸问题都平息,程序内存占用只有十几二十MB,CPU也最多2%。

总之,“点绘”不好,有空第二代“行绘机”了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/503360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高德地图api 地理编码(地址-->坐标)geocoder.getLocation在官方可以测试出结果,下载代码到本地却用不了 问题解决

问题 高德地图api 地理编码&#xff08;地址-->坐标&#xff09;功能&#xff0c;通过输入 地址信息 得到 经纬度信息。geocoder.getLocation在官方可以测试出结果&#xff0c;下载代码到本地却用不了。 官方示例测试&#xff0c;可以从地址得到坐标 下载官方代码本地运行却…

常见注意力机制解析

1.Squeeze-and-Excitation&#xff08;SE&#xff09; SE的主要思想是通过对输入特征进行压缩和激励&#xff0c;来提高模型的表现能力。具体来说&#xff0c;SE注意力机制包括两个步骤&#xff1a;Squeeze和Excitation。在Squeeze步骤中&#xff0c;通过全局平均池化操作将输…

【2023年Mathorcup杯数学建模竞赛C题】电商物流网络包裹应急调运与结构优化--完整作品分享

1.问题背景 2.论文摘要 为了应对电商物流网络中物流场地和线路电商物流网络中物流场地和线路上货量波动的情况&#xff0c; 设计合理的物流网络调整方案以保障物流网络的正常运行。本文运用 0-1 整数规划模型&#xff0c;多目标动 态规划模型&#xff0c;给出了问题的结果。 针…

深入讲解eMMC简介

1 eMMC是什么 eMMC是embedded MultiMediaCard的简称&#xff0c;即嵌入式多媒体卡,是一种闪存卡的标准&#xff0c;它定义了基于嵌入式多媒体卡的存储系统的物理架构和访问接口及协议&#xff0c;具体由电子设备工程联合委员会JEDEC订立和发布。它是对MMC的一个拓展&#xff0…

redi缓存使用

1、缓存的特征 第一个特征&#xff1a;在一个层次化的系统中&#xff0c;缓存一定是一个快速子系统&#xff0c;数据存在缓存中时&#xff0c;能避免每次从慢速子系统中存取数据。 第二个特征&#xff1a;缓存系统的容量大小总是小于后端慢速系统的&#xff0c;不可能把所有数…

GAMES101 计算机图形学 | 学习笔记 (上)

目录 环境安装什么是计算机图形学物体上点的坐标变换顺序齐次坐标光栅化如何判定一个点在三角形内光栅化填充三角形示例代码光栅化产生的问题 采样不足&#xff08;欠采样&#xff09;导致锯齿抗锯齿滤波算法 环境安装 1. C中安装opencv库 2. C中安装eigen库 3. C中安装open…

ChatGPT调教指北,技巧就是效率!

技巧就是效率 很多人都知道ChatGPT很火很强&#xff0c;几乎无所不能&#xff0c;但跨越了重重门槛之才有机会使用的时候却有些迷茫&#xff0c;一时间不知道如何使用它。如果你就是把他当作一个普通的智能助手来看待&#xff0c;那与小爱同学有什么区别&#xff1f;甚至还差劲…

热乎的面经——踏石留印

⭐️前言⭐️ 本篇文章记录博主面试北京某公司所记录的面经&#xff0c;希望能给各位带来帮助。 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主将持续更新学习记录收获&#xff0c;友友们有任何问题可以在评论…

Origin如何绘制三维图形?

文章目录 0.引言1.使用矩阵簿窗口2.三维数据转换3.三维绘图4.三维曲面图5.三维XYY图6.三维符号、条状、矢量图7.等高线图 0.引言 因科研等多场景需要&#xff0c;绘制专业的图表&#xff0c;笔者对Origin进行了学习&#xff0c;本文通过《Origin 2022科学绘图与数据》及其配套素…

63.空白和视觉层级的实战应用

例如看我们之前的小网页&#xff1b; 这些标题的上下距离一样&#xff0c;这样让我们很容易对这些标题进行混淆&#xff0c;我们可以适当的添加一点空白 header, section {margin-bottom: 96px; }这样看上去似乎就好很多&#xff01; 除此之外&#xff0c;如我们之间学的空…

【line features】线特征

使用BinaryDescriptor接口提取线条并将其存储在KeyLine对象中&#xff0c;使用相同的接口计算每个提取线条的描述符&#xff0c;使用BinaryDescriptorMatcher确定从不同图像获得的描述符之间的匹配。 opencv提供接口实现 线提取和描述符计算 下面的代码片段展示了如何从图像中…

K8S相关核心概念

个人笔记&#xff1a; 要弄明白k8s的细节&#xff0c;需要知道k8s是个什么东西。它的主要功能&#xff0c;就是容器的调度--也就是把部署实例&#xff0c;根据整体资源的使用状况&#xff0c;部署到任何地方 注意任何这两个字&#xff0c;预示着你并不能够通过常规的IP、端口…

如何全面学习Object-C语言的语法知识 (Xmind Copilot生成)

网址&#xff1a;https://xmind.ai/login/ 登录后直接输入&#xff1a;如何全面学习Object-C语言的语法知识&#xff0c;就可以生成大纲 点击右上角的 按钮&#xff0c;可以显示md格式的问题&#xff0c;再点击生成全文&#xff0c;就可以生成所有内容了&#xff0c; 还有这个…

CentOS7/8 安装 5+ 以上的Linux kernel

CentOS以稳定著称&#xff0c;稳定在另外一方面就是保守。所以CentOS7还在用3.10&#xff0c;CentOS8也才是4.18。而当前最新的Linux Kernel都更新到6.0 rc3了。其他较新的发行版都用上了5.10的版本。本文简单介绍如何在CentOS7、8上直接安装5.1以上版本的第三方内核。 使用ted…

5.8晚间黄金行情走势分析及短线交易策略

近期有哪些消息面影响黄金走势&#xff1f;本周黄金多空该如何研判&#xff1f; ​黄金消息面解析&#xff1a;周一亚洲时段&#xff0c;现货黄金小幅反弹&#xff0c;目前交投于2024.3美元/盎司附近&#xff0c;一方面是金价上周五守住了 2000 整数关口&#xff0c;逢低买盘涌…

java环境Springboot框架中配置使用GDAL,并演示使用GDAL读取shapefile文件

GDAL是应用广泛的空间数据处理库&#xff0c;可以处理几何、栅格数据&#xff0c;Springboot是常用的JAVA后端开发框架。本文讲解如何在Springboot中配置使用GDAL。本文示例中使用的GDAL版本为3.4.1&#xff08;64位&#xff09; 图1 GDAL读取shp效果 一、部署GDAL类库 将GDA…

什么是点对点传输?什么是点对多传输

点对点技术&#xff08;peer-to-peer&#xff0c; 简称P2P&#xff09;又称对等互联网络技术&#xff0c;是一种网络新技术&#xff0c;依赖网络中参与者的计算能力和带宽&#xff0c;而不是把依赖都聚集在较少的几台服务器上。P2P网络通常用于通过Ad Hoc连接来连接节点。这类网…

WiFi(Wireless Fidelity)基础(四)

目录 一、基本介绍&#xff08;Introduction&#xff09; 二、进化发展&#xff08;Evolution&#xff09; 三、PHY帧&#xff08;&#xff08;PHY Frame &#xff09; 四、MAC帧&#xff08;MAC Frame &#xff09; 五、协议&#xff08;Protocol&#xff09; 六、安全&#x…

功能测试常用的测试用例大全

登录、添加、删除、查询模块是我们经常遇到的&#xff0c;这些模块的测试点该如何考虑 1)登录 ① 用户名和密码都符合要求(格式上的要求) ② 用户名和密码都不符合要求(格式上的要求) ③ 用户名符合要求&#xff0c;密码不符合要求(格式上的要求) ④ 密码符合要求&#xff0c;…

1_1torch学习

一、torch基础知识 1、torch安装 pytorch cuda版本下载地址&#xff1a;https://download.pytorch.org/whl/torch_stable.html 其中先看官网安装torch需要的cuda版本&#xff0c;之后安装cuda版本&#xff0c;之后采用pip 下载对应的torch的gpu版本whl来进行安装。使用pip安装…