随着近几年AI的迅速发展,GPU变得越来越抢手。然而,GPU的全称为Graphics Processing Unit, 从名字中就可以看出,GPU是为了处理图形而诞生的,后来才被应用到科学计算等领域中。

1. 我们眼中的GPU

GPU的架构大致如下:

其中,标有SP(Streaming Processor)的方格是一个小处理器,它比CPU的功能要弱很多,但它们在GPU中的数量很多,每一个SP可以处理一部分数据,很多个SP就能处理大量的数据啦。

不必在意GPU具体是怎么设计的,我们关心的问题只有两个:

1. GPU如何保存我们的数据?

2. 如何让GPU处理数据?

对于第一个问题,答案很简单:GPU有自己的内存,我们需要把数据传到GPU上。值得注意的是,Vulkan中抽象出了两种不同的内存:一种是VkBuffer, 这种内存基本上就是我们平时所说的主机内存,是一个连续的二进制块;另一种是VkImage, 仔细观察上面这张图,其中有一个名为Texture Unit的块(texture的意思是纹理,日常使用的jpg/png图片就可以作为纹理)。把纹理与普通的内存区分开来出于实际需要,目前只要知道二者有区别即可。

对于第二个问题,GPU通过队列(Queue)管理命令,用户通过调用形如vkCmdXXX的函数将命令传到队列,之后GPU会依次执行这些命令。队列类似于日常生活中的排队,先到先得,先传送到GPU的命令会先执行。队列有很多种,不同的队列有不同的功能,例如计算队列、图形队列等等。GPU中至少有一个队列,多个功能相同的队列构成了一个队列族(Queue Family).

上面这些,正如“地图”的第二层所画的那样:

2. 查找可用的物理设备

以后,我们会输出各种信息,为了少写点代码,定义宏Log用于输出信息:

#define Log(message) std::cout << "[INFO] " << message << std::endl

例如,Log("Here is a bug"); 会被替换为 std::cout << "[INFO] " << "Here is a bug" << std::endl;.

另外还定义了一个功能相似的宏Error, 在程序出错时输出错误信息并终止程序:

#define Error(message) std::cerr << "[ERROR] " << message << std::endl; exit(-1)

之前我们我们实现了几个名为createXXX的函数,用于创建某个特定的对象,但物理设备显然不需要新建,只需要选择合适的物理设备即可,因此接下来我们要实现的函数为selectPhysicalDevice, 用于选择满足特定要求的设备。

除了GPU外,Vulkan还支持其它类型的物理设备,我们默认“物理设备”一词指的是GPU.

首先在VulkanApp类中添加以下成员表示物理设备:

  1. VkPhysicalDevice mPhysicalDevice; // 物理设备

使用vkEnumeratePhysicalDevices查找所有可选的物理设备:

  1. VkResult vkEnumeratePhysicalDevices(
  2. VkInstance instance,
  3. uint32_t& pPhysicalDeviceCount,
  4. VkPhysicalDevice& pPhysicalDevices);

这个函数会将一个VkPhysicalDevice数组返回给用户,其中:

instance: 上一篇文章中创建的实例对象mInstance;

pPhysicalDeviceCount: 数组的大小;

pPhysicalDevices: 数组的首地址。当此参数为空指针nullptr时,此函数会向pPhysicalDeviceCount指向的位置写入设备的数量。

从而,查找物理设备的代码如下:

  1. /* 查找所有可选的物理设备 */
  2. uint32_t physicalDeviceCount = 0;
  3. vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, nullptr);
  4. vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
  5. vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, physicalDevices.data());

此函数的第一个参数是上一篇博客中创建的实例对象mInstance, 后面两个参数分别是两个指针,分别指向物理设备的数量和一个

首先,将函数vkEnumeratePhysicalDevices的最后一个参数设置为nullptr, 从而physicalDeviceCount会获取到可用的设备数量;随后,使用vector分配相应大小的内存;最后,将内存的首地址传入函数,此函数会向内存写入数据。

以上代码展示了从Vulkan函数中获取一个数组的套路:

  1. 获取大小
  2. 分配相应大小的内存
  3. 将分配好内存的首地址传入函数,由函数向相应位置写入数据。

从Vulkan的函数中获取数组基本上都类似于上面这种写法,以后再遇到时不再另外说明。

3. 物理设备的属性与特性

上一节中我们获得了可用物理设备的数组, 可以通过VkPhysicalDevice查找物理设备的属性(Property)与特性(Feature)。

属性与特性的区别

属性是物理设备的一些性质,例如显卡的名称、版本号等;而特性是物理设备支持的功能。如果用人作类比,身高、体重是人的属性,会写代码、会弹钢琴是人的特性。

定义结构体VkPhysicalDeviceProperties,并使用特定的函数获取物理设备的属性与特性信息:

  1. for (VkPhysicalDevice physicalDevice : physicalDevices) {
  2. /* 获取显卡的属性 */
  3. VkPhysicalDeviceProperties physicalDeviceProperties;
  4. vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);
  5. /* 获取显卡的特性 */
  6. VkPhysicalDeviceFeatures physicalDeviceFeatures;
  7. vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);
  8. /* 输出所有可选的设备名称 */
  9. Log("device name: " << physicalDeviceProperties.deviceName);
  10. }

代码的最后加了一条输出信息,现在运行代码,应该能看到所有可选设备的名称。

一个悲伤的故事

博主最初的游戏本下岗后,为了戒掉游戏瘾,于是买了台只带有集显的笔记本电脑。这样的电脑是玩不动3A大作的,但现在博主有点后悔了。因此,博主的电脑中只有一块支持Vulkan的集显,如果这块集显不支持Vulkan, 那博主也没办法继续学下去了。

这也是标题为“零号显卡”的原因——电脑中只有一块可选的集显。

4. 物理设备的内存和队列

类似地,我们也可以查找物理设备支持的内存和队列。

以下代码可以获取物理设备的内存信息:

  1. /* 获取显卡的内存信息 */
  2. VkPhysicalDeviceMemoryProperties physicalDeviceMemoryProperties;
  3. vkGetPhysicalDeviceMemoryProperties(physicalDevice, &physicalDeviceMemoryProperties);
  4. Log("device supports " << physicalDeviceMemoryProperties.memoryTypeCount << " types of memory:");
  5. for (int i = 0; i < physicalDeviceMemoryProperties.memoryTypeCount; i++) {
  6. auto flags = physicalDeviceMemoryProperties.memoryTypes[i].propertyFlags;
  7. if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) {
  8. Log(" device local memory");
  9. }
  10. }

上述代码只是个演示,由于目前了解物理设备的内存信息并没有什么用处,所以在此不过多介绍,感兴趣的朋友可自行查找文档。

获取队列就比较重要了。之前提到过,相同功能的队列集合构成一个队列族(Queue Family), 需要查找物理设备支持哪些功能的队列族(例如计算功能、图形功能等等)。以下是获取队列族信息的代码:

  1. /* 获取队列族的信息 */
  2. uint32_t queueFamilyCount = 0;
  3. vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
  4. vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
  5. vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilies.data());

有了之前获取物理设备的经验,相信以上代码对你来说一定是小菜一碟。其中,结构体VkQueueFamilyProperties描述了一个队列族的信息,其定义如下:

  1. typedef struct VkQueueFamilyProperties {
  2. VkQueueFlags queueFlags; // 队列族的功能
  3. uint32_t queueCount; // 队列族的队列个数
  4. uint32_t timestampValidBits;
  5. VkExtent3D minImageTransferGranularity;
  6. } VkQueueFamilyProperties;

其中:

queueFlags:描述了队列族的功能,如果支持某项功能,相应的位置就会被设置为1。例如,如果队列族支持图形功能,那么queueFlags & VK_QUEUE_GRAPHICS_BIT的结果就不为零;

queueCount: 此队列族中队列的个数;

另外两个成员不用管它们。

5. 选择合适的物理设备

物理设备需要支持下列功能:

  1. 支持交换链(Swapchain)扩展(至于什么是交换链,之后的文章会加以说明);
  2. 支持几何着色器(Geometry Shader) (同上,之后的文章会加以说明);
  3. 支持图形队列功能;
  4. 支持显示功能。

5.1. 检查交换链扩展

首先在VulkanApp类中添加成员mRequiredExtensions, 表示需要的扩展:

  1. const vector<const char*> mRequiredExtensions = {
  2. VK_KHR_SWAPCHAIN_EXTENSION_NAME, // 等价于字符串"VK_KHR_swapchain"
  3. };

之后,获取设备支持的扩展信息:

  1. /* 获取物理设备支持的扩展信息 */
  2. uint32_t extensionCount = 0;
  3. vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
  4. vector<VkExtensionProperties> availableExtensions(extensionCount);
  5. vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, availableExtensions.data());

接下来,检查mRequiredExtensions中的字符串是否都在availableExtensions中(结构体VkExtensionProperties有一个成员extensionName,是一个指向扩展名称字符串的指针)。下面使用了<cstring>中的strcmp函数比较字符串是否相等,代码如下:

  1. for (const char* requiredExtensionName : mRequiredExtensions) {
  2. bool isSupported = false;
  3. for (const auto& availableExtension : availableExtensions) {
  4. if (strcmp(requiredExtensionName, availableExtension.extensionName) == 0) {
  5. isSupported = true;
  6. Log("extension " << requiredExtensionName << " is supported");
  7. break;
  8. }
  9. }
  10. if (isSupported == false) {
  11. Log("extension " << requiredExtensionName << " is not supported");
  12. }
  13. }

以上代码没什么技术含量,或许你可以实现个更好的。

5.2. 检查是否支持几何着色器

这一信息包含在物理设备的特性内,之前介绍过如何获取物理设备的特性信息,只要检查结构体VkPhysicalDeviceFeatures中的成员geometryShader(类型为VkBool32)是否为True即可:

  1. /* 2. 检查设备是否支持几何着色器 */
  2. VkPhysicalDeviceFeatures physicalDeviceFeatures;
  3. vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);
  4. if (physicalDeviceFeatures.geometryShader) {
  5. Log("geometry shader is supported");
  6. }
  7. else {
  8. Log("geometry shader is not supported");
  9. }

5.3. 检查是否支持图形功能

我们需要找到一个支持图形功能的队列族,并记录下此队列族的索引(之后会用到)。为此,首先在VulkanApp类中定义一个成员变量,用于记录图形队列族的索引:

  1. int mGraphicsQueueFamilyIndex; // 支持图形功能的队列族索引

上一节介绍过如何获取队列族的信息。接下来,只要依次检查每一个队列族,即检查结构体VkQueueFamilyProperties中的queueFlags位,直到找到合适的队列族:

  1. for (int i = 0; i < queueFamilyCount; i++) {
  2. /*  5.3. 检查是否支持图形功能 */
  3. if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
  4. Log("graphics is supported");
  5. mGraphicsQueueFamilyIndex = i; // 保留队列族的索引
  6. }
  7. else {
  8. Log("graphics is not supported");
  9. }
  10. }

5.4. 检查是否支持显示功能

类似于图形功能,我们需要找到一个支持显示功能(与VkSurfaceKHR相关)的队列族,并记录下此队列族的索引。首先在VulkanApp中定义一个成员变量记录索引:

  1. int mPresentQueueFamilyIndex; // 支持显示功能的队列族索引

使用函数vkGetPhysicalDeviceSurfaceSupportKHR检查:

  1. for (int i = 0; i < queueFamilyCount; i++) {
  2. /*  5.3. 检查是否支持图形功能 */
  3. ......
  4. /* 5.4. 检查是否支持显示功能 */
  5. VkBool32 isPresentSupport = false;
  6. vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, mSurface, &isPresentSupport);
  7. if (isPresentSupport) {
  8. mPresentQueueFamilyIndex = i;
  9. Log("find present queue family index " << i);
  10. }
  11. else {
  12. Log("present is not supported");
  13. }
  14. }

vkGetPhysicalDeviceSurfaceSupportKHR需要传入的参数有:物理设备(VkPhysicalDevice), 队列族的索引和上一节创建的表面对象(mSurface), 最后一个参数是指向VkBool32的指针,函数向其中写入检查的结果。

如果待选的物理设备通过了以上四重考验,那么我们就可以选择此设备:

  1. mPhysicalDevice = physicalDevice;

6. 到目前为止的完整代码

现在运行代码,应该能看到以下输出:

值得注意的是,在这里,图形与显示选择的是同一个队列族,都是0号队列族。当然,它们也可以分属不同的队列族,这会影响到后面的一部分代码,到时候再说。

下一节,我们将研究逻辑设备(VkDevice),或者简称为设备,它是对物理设备的抽象。之后可以看到,大多数API都需要一个设备作为参数。

写到这里时,博主碰到了MSVC的一个小bug:某行类似于/* XXX */的中文注释,使得编译器在预处理阶段删去了此注释后面的部分代码。

错误原因与文件的编码格式相关,有一种解决方案是在注释的前后空2个空格,例如/*  XX  */.

总之,博主是长见识了。

到目前为止的完整代码
  1.  #define GLFW_INCLUDE_VULKAN
  2. #include <GLFW/glfw3.h>
  3. #include <iostream>
  4. #include <vector>
  5. using std::vector;
  6. #include <cstring>
  7. #define Log(message) std::cout << "[INFO] " << message << std::endl
  8. #define Error(message) std::cerr << "[ERROR] " << message << std::endl; exit(-1)
  9. static void if_fail(VkResult result, const char* message);
  10. class VulkanApp {
  11. public:
  12. VulkanApp() {
  13. glfwInit(); // 初始化glfw库
  14. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); // 禁用OpenGL相关的API
  15. glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); // 禁止调整窗口大小
  16. createInstance();
  17. createSurface();
  18. selectPhysicalDevice();
  19. }
  20. ~VulkanApp() {
  21. vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
  22. vkDestroyInstance(mInstance, nullptr);
  23. glfwDestroyWindow(mWindow);
  24. glfwTerminate();
  25. }
  26. void Run() {
  27. while (!glfwWindowShouldClose(mWindow)) {
  28. glfwPollEvents();
  29. }
  30. }
  31. private:
  32. const vector<const char*> mRequiredLayers = {
  33. "VK_LAYER_KHRONOS_validation"
  34. };
  35. const vector<const char*> mRequiredExtensions = {
  36. VK_KHR_SWAPCHAIN_EXTENSION_NAME, // 等价于字符串"VK_KHR_swapchain"
  37. };
  38. VkInstance mInstance; // 实例
  39. VkPhysicalDevice mPhysicalDevice; // 物理设备
  40. int mGraphicsQueueFamilyIndex = -1; // 支持图形功能的队列族索引
  41. int mPresentQueueFamilyIndex = -1; // 支持显示功能的队列族索引
  42. int mWidth = 800; // 窗口宽度
  43. int mHeight = 600; // 窗口高度
  44. GLFWwindow* mWindow = nullptr; // glfw窗口指针
  45. VkSurfaceKHR mSurface;
  46. void createInstance() {
  47. /* 填充VkApplicationInfo结构体 */
  48. VkApplicationInfo appInfo{
  49. VK_STRUCTURE_TYPE_APPLICATION_INFO, // .sType
  50. nullptr, // .pNext
  51. "I don't care", // .pApplicationName
  52. VK_MAKE_VERSION(1, 0, 0), // .applicationVersion
  53. "I don't care", // .pEngineName
  54. VK_MAKE_VERSION(1, 0, 0), // .engineVersion
  55. VK_API_VERSION_1_0, // .apiVersion
  56. };
  57. /* 获取glfw要求支持的扩展 */
  58. uint32_t glfwExtensionCount = 0;
  59. const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
  60. /* 输出glfw所需的扩展 */
  61. std::cout << "[INFO] glfw needs the following extensions:\n";
  62. for (int i = 0; i < glfwExtensionCount; i++) {
  63. std::cout << " " << glfwExtensions[i] << std::endl;
  64. }
  65. /* 填充VkInstanceCreateInfo结构体 */
  66. VkInstanceCreateInfo instanceCreateInfo{
  67. VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, // .sType
  68. nullptr, // .pNext
  69. 0, // .flags
  70. &appInfo, // .pApplicationInfo
  71. mRequiredLayers.size(), // .enabledLayerCount
  72. mRequiredLayers.data(), // .ppEnabledLayerNames
  73. glfwExtensionCount, // .enabledExtensioncount
  74. glfwExtensions, // .ppEnabledExtensionNames
  75. };
  76. /* 如果创建实例失败,终止程序 */
  77. if_fail(
  78. vkCreateInstance(&instanceCreateInfo, nullptr, &mInstance),
  79. "failed to create instance"
  80. );
  81. }
  82. void createSurface() {
  83. mWindow = glfwCreateWindow(mWidth, mHeight, "Vulkan App", nullptr, nullptr); // 创建glfw窗口
  84. if (mWindow == nullptr) {
  85. std::cerr << "failed to create window\n";
  86. exit(-1);
  87. }
  88. /* 创建VkSurfaceKHR对象 */
  89. if_fail(
  90. glfwCreateWindowSurface(mInstance, mWindow, nullptr, &mSurface),
  91. "failed to create surface"
  92. );
  93. }
  94. void selectPhysicalDevice() {
  95. /* 查找所有可选的物理设备 */
  96. uint32_t physicalDeviceCount = 0;
  97. vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, nullptr);
  98. vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount);
  99. vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, physicalDevices.data());
  100. mPhysicalDevice = VK_NULL_HANDLE;
  101. for (VkPhysicalDevice physicalDevice : physicalDevices) {
  102. /* 1. 检查物理设备是否支持扩展 */
  103. /* 获取物理设备支持的扩展信息 */
  104. uint32_t extensionCount = 0;
  105. vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
  106. vector<VkExtensionProperties> availableExtensions(extensionCount);
  107. vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, availableExtensions.data());
  108. bool isAllRequiredExtensionsSupported = true; // 检查此物理设备是否支持所有的扩展
  109. for (const char* requiredExtensionName : mRequiredExtensions) {
  110. bool isSupported = false;
  111. for (const auto& availableExtension : availableExtensions) {
  112. if (strcmp(requiredExtensionName, availableExtension.extensionName) == 0) {
  113. isSupported = true;
  114. break;
  115. }
  116. }
  117. if (isSupported == false) {
  118. isAllRequiredExtensionsSupported = false;
  119. break;
  120. }
  121. }
  122. if (isAllRequiredExtensionsSupported) {
  123. Log("all required extensions are supported");
  124. }
  125. else {
  126. continue;
  127. }
  128. /* 2. 检查物理设备是否支持几何着色器 */
  129. VkPhysicalDeviceFeatures physicalDeviceFeatures;
  130. vkGetPhysicalDeviceFeatures(physicalDevice, &physicalDeviceFeatures);
  131. if (physicalDeviceFeatures.geometryShader) {
  132. Log("geometry shader is supported");
  133. }
  134. else {
  135. continue;
  136. }
  137. /* 获取队列族的信息 */
  138. uint32_t queueFamilyCount = 0;
  139. vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr);
  140. vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
  141. vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilies.data());
  142. for (int i = 0; i < queueFamilyCount; i++) {
  143. /*  5.3. 检查是否支持图形功能 */
  144. if (mGraphicsQueueFamilyIndex < 0 && (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) {
  145. Log("find graphics queue family index " << i);
  146. mGraphicsQueueFamilyIndex = i; // 保留队列族的索引
  147. }
  148. /* 5.4. 检查是否支持显示功能  */
  149. if (mPresentQueueFamilyIndex < 0) {
  150. VkBool32 isPresentSupport = false;
  151. vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, mSurface, &isPresentSupport);
  152. if (isPresentSupport) {
  153. mPresentQueueFamilyIndex = i;
  154. Log("find present queue family index " << i);
  155. }
  156. else {
  157. Log("present is not supported");
  158. }
  159. }
  160. }
  161. if (mGraphicsQueueFamilyIndex >= 0 && mPresentQueueFamilyIndex >= 0) {
  162. mPhysicalDevice = physicalDevice;
  163. /* 获取物理设备的属性 */
  164. VkPhysicalDeviceProperties physicalDeviceProperties;
  165. vkGetPhysicalDeviceProperties(mPhysicalDevice, &physicalDeviceProperties);
  166. Log("select physical device: " << physicalDeviceProperties.deviceName);
  167. }
  168. }
  169. /* 如果没找到合适的物理设备 */
  170. if (mPhysicalDevice == VK_NULL_HANDLE) {
  171. Error("can't find suitable physical device");
  172. }
  173. }
  174. };
  175. int main() {
  176. VulkanApp app;
  177. app.Run();
  178. }
  179. static void if_fail(VkResult result, const char* message) {
  180. if (result != VK_SUCCESS) {
  181. std::cerr << "[error] " << message << std::endl;
  182. exit(-1);
  183. }
  184. }

Vulkan学习苦旅03:零号显卡,启动!(选择物理设备VkPhysicalDevcie)的更多相关文章

  1. Linux学习心得之 双显卡、中文输入法及svn初步使用

    作者:枫雪庭 出处:http://www.cnblogs.com/FengXueTing-px/ 欢迎转载 Linux学习心得之 双显卡.中文输入法及svn初步使用 1.前言 2.Linux双显卡解决 ...

  2. C++ GUI Qt4学习笔记03

    C++ GUI Qt4学习笔记03   qtc++spreadsheet文档工具resources 本章介绍创建Spreadsheet应用程序的主窗口 1.子类化QMainWindow 通过子类化QM ...

  3. Redis:学习笔记-03

    Redis:学习笔记-03 该部分内容,参考了 bilibili 上讲解 Redis 中,观看数最多的课程 Redis最新超详细版教程通俗易懂,来自 UP主 遇见狂神说 7. Redis配置文件 启动 ...

  4. Windows phone 8 学习笔记(4) 应用的启动

    原文:Windows phone 8 学习笔记(4) 应用的启动 Windows phone 8 的应用除了可以直接从开始菜单以及应用列表中打开外,还可以通过其他的方式打开.照片中心.音乐+视频中心提 ...

  5. (转)redis 学习笔记(1)-编译、启动、停止

    redis 学习笔记(1)-编译.启动.停止   一.下载.编译 redis是以源码方式发行的,先下载源码,然后在linux下编译 1.1 http://www.redis.io/download 先 ...

  6. springboot 学习之路 9 (项目启动后就执行特定方法)

    目录:[持续更新.....] spring 部分常用注解 spring boot 学习之路1(简单入门) spring boot 学习之路2(注解介绍) spring boot 学习之路3( 集成my ...

  7. 机器学习实战(Machine Learning in Action)学习笔记————03.决策树原理、源码解析及测试

    机器学习实战(Machine Learning in Action)学习笔记————03.决策树原理.源码解析及测试 关键字:决策树.python.源码解析.测试作者:米仓山下时间:2018-10-2 ...

  8. CAN总线学习系列之三——CAN控制器的选择

    CAN总线学习系列之三——CAN控制器的选择 在进行CAN总线开发前,首先要选择好CAN总线控制器.下面就比较一些控制器的特点. 一些主要的CAN总线器件产品 制造商 产品型号 器件功能及特点 Int ...

  9. OpenCV 学习笔记03 边界框、最小矩形区域和最小闭圆的轮廓

    本节代码使用的opencv-python 4.0.1,numpy 1.15.4 + mkl 使用图片为 Mjolnir_Round_Car_Magnet_300x300.jpg 代码如下: impor ...

  10. OpenCV 学习笔记03 findContours函数

    opencv-python   4.0.1 1 函数释义 词义:发现轮廓! 从二进制图像中查找轮廓(Finds contours in a binary image):轮廓是形状分析和物体检测和识别的 ...

随机推荐

  1. 悟空活动中台 - 基于 WebP 的图片高性能加载方案

    本文首发于 vivo互联网技术 微信公众号 链接: https://mp.weixin.qq.com/s/rSpWorfNTajtqq_pd7H-nw作者:悟空中台研发团队 一.背景 移动端网页的加载 ...

  2. warning: LF will be replaced by CRLF in public/tinymce/langs/zh_CN.js

    windows使用git时出现:warning:LF will be replaced by CRLF windows中的换行符为 CRLF, 而在linux下的换行符为LF,所以在执行add . 时 ...

  3. Makeflie脚本使用

    1.目标 2.Makefile的作用 自动化编译仿真 文件有引用层级关系,Tb会引用RTL顶层,RTL顶层也会引用一些其他的小的模块,编译的时候被引用的文件需要先进行编译. 脚本有两种模式,debug ...

  4. [转帖]字符集 AL32UTF8 和 UTF8

    https://blog.51cto.com/comtv/383254# 文章标签职场休闲字符集 AL32UTF8 和 UTF8文章分类数据库阅读数1992 The difference betwee ...

  5. [转帖]shell编程:变量的数值计算实践(五)

    https://www.cnblogs.com/luoahong/articles/9224495.html 算术运算符 变量的数值(整数)计算   1)(())用法:(此方法很常用)** 范例:sh ...

  6. [转帖]Debian9换源(阿里源)(Linux子系统)

    http://www.taodudu.cc/news/show-5410026.html?action=onClick 默认你已经装好Linux子系统. Step 0: 换源核心就是把/etc/apt ...

  7. [转帖]使用Linux命令快速查看某一行

      原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 当年,我还是Linux菜鸟的时候,就在简历上写着精通Linux命令了,而当面试官问我"如何快速查看 ...

  8. 高性能Redis服务器注意事项

    摘要 昨天简单理了理安装与配置相关的 但是很多比较重要的核心性能参数并没有进行学习与探讨 就基于昨天理解不深入的地方进行进一步的学习与了解 希望能够提高Redis-Server的性能. 第一部分: 规 ...

  9. Oracle12c(未更新任何补丁) 使用compression=all 参数导出之后导入失败

    1. 最近使用Oracle12c 进行相关的测试工作, 平台linux 和 windows 都有一个问题 备份恢复使用的 compression=all 时导入数据库不管是oracle12c还是 or ...

  10. Python 潮流周刊#12:Python 中如何调试死锁问题?

    查看全文: https://pythoncat.top/posts/2023-07-22-weekly 文章&教程 1.使用 PyStack 调试 Python 中的崩溃和死锁 (英) 2.介 ...