当前位置: 首页 > news >正文

小游戏和GUI编程(7) | SimpleNN 界面源码解析

小游戏和GUI编程(7) | SimpleNN 界面源码解析

0. 简介

SimpleNN 是 AdamYuan 在高中一年级时用 1 天时间写出来的简易 CNN, 使用 SFML 做 UI, 用于交互式输入手写数字,这个数字被训练好的 CNN 网络执行推理得到识别结果, 它的运行效果如下:

这一篇我们来分析 UI 界面的代码, 规划如下:

  • 完成本地构建 (预计5分钟)
  • 确定和粗读 UI 代码 (预计30分钟)
  • 拆解 UI 部件和自行重新实现 (预计2小时)

实际用时: 10:40~14:30

1. 完成本地构建: 添加 CMakeLists.txt

原版代码使用 Makefile, 其中添加了 -std=c++11, 换了 g++ 为 clang++, 我是在 macOS 下:

all: MnistTrainer MnistUI MnistTest
MnistTrainer: mnist_trainer.cpp */*.hpp */*.cppclang++ -std=c++11 mnist_trainer.cpp */*.cpp -Ofast -o MnistTrainer -lm -lpthread
MnistUI: mnist_ui.cpp NN/NN.* NN/Util.hpp MNIST/Util.hppclang++ -std=c++11 mnist_ui.cpp NN/NN.cpp -Ofast -o MnistUI -lm -lsfml-system -lsfml-window -lsfml-graphics
MnistTest: mnist_test.cpp NN/NN.* MNIST/Loader.* NN/Util.hpp MNIST/Util.hppclang++ -std=c++11 mnist_test.cpp NN/NN.cpp MNIST/Loader.cpp -Ofast -o MnistTest -lm

为什么不用 Makefile: 因为 makefile 没有内置的包管理器, pkg-config 配置多个包的话感觉很麻烦. 使用 CMake 稍微缓解一些。

找到了 3 个 main( 函数, 和 makefile 里的 3 个 target 对应:

➜  SimpleNN git:(master) ✗ ag 'main\(' --ignore-dir build
mnist_ui.cpp
113:int main(int argc, char **argv)mnist_test.cpp
6:int main(int argc, char **argv)mnist_trainer.cpp
7:int main(int argc, char **argv)

对于 UI 界面显示, 不需要 mnist_trainer.cppmnist_test.cpp, 因此写出 CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(SimpleNN)set(CMAKE_CXX_STANDARD 11)add_executable(MnistUImnist_ui.cppMNIST/Loader.cppNN/NN.cppNN/Trainer.cpp
)
find_package(SFML 2.6 COMPONENTS system window graphics REQUIRED)
target_link_libraries(MnistUI PRIVATEpthreadsfml-systemsfml-windowsfml-graphics
)

为了后续源码分析和测试方便, 再增加一个 MnistUI_my 的可执行文件目标:

add_executable(MnistUI_mymnist_ui_my.cppMNIST/Loader.cppNN/NN.cppNN/Trainer.cpp
)
target_link_libraries(MnistUI_my PRIVATEpthreadsfml-systemsfml-windowsfml-graphics
)

2. 确定和粗读 UI 代码

拆解为: 确定 UI 相关的代码文件; 粗略分析 UI 代码组成部分.

涉及的文件:

  • mnist_ui.cpp : UI 代码, 170 行
  • ui/VCR_OSD_MONO_1.001.ttf : 字体文件

下面是 mnist_ui.cpp 的简单解读:

2.1 通过命令行参数传入网络文件

使用了全局变量 snn, 从传入的参数表示的文件来加载 cnn 网络相关的内容:

SimpleNN snn;int main(int argc, char **argv)
{if(argc != 2){printf("Usage: ./MnistUI [snn filename]\n");return EXIT_FAILURE;}snn.Load(argv[1]);...
}

2.2 UI 整体代码逻辑

	InitWindow(); // 窗口部件的创建、 布局的设定Clear(); // 设定鼠标绘制区域的颜色while(window.isOpen()){while(window.pollEvent(event)){// 事件处理}// 如果鼠标左键按下了, 那么渲染鼠标的轨迹if(mouse_down)Paint();window.draw(paint_sprite);// 渲染输入纹理window.draw(input_sprite);// 渲染输出纹理window.draw(output_sprite);// 渲染输出数字纹理window.draw(output_digits_sprite);// 渲染鼠标为圆形Cursor();window.display(); // 绘制}

3. 详细解读

这一节是通过拆解 UI 代码的部件, 对每个部件进行代码粗略分析, 并摘录出用到的代码到单独的文件 Mnist_UI_my.cpp 中验证效果.

3.1 窗口部件、布局

整体布局

在这里插入图片描述

这一小节,需要看的是 InitWindow() 函数, 以及 main() 函数里 window.draw() 相关的几句调用。

InitWindow() 里, 设置了各个部件的大小:

  • paint_tex: 560x560的方格, main()中创建了它的匿名 Sprite 并且没设置位置, 因此位置是默认的 (0,0), 也就是整个窗口左边一半
window.draw(sf::Sprite(paint_tex.getTexture()));
  • input_tex: 和 paint_tex 大小一致,结合 main() 里的代码, 是位于窗口右侧
sf::Sprite input_sprite{input_tex.getTexture()};
input_sprite.setPosition(kSize, 0);
window.draw(input_sprite);
  • output_tex: 56x560的竖条, 结合 main() 里的代码, 是位于整个窗口最右侧
sf::Sprite output_sprite{output_tex.getTexture()};
output_sprite.setPosition(kSize*2, 0);
window.draw(output_sprite);

InitWindow() 详细注释

void InitWindow()
{window.create(sf::VideoMode(kSize*2 + kOutSize, kSize), "Mnist Demo", sf::Style::Titlebar | sf::Style::Close);paint_tex.create(kSize, kSize); // kSize=20*28, 这是560x560方形纹理input_tex.create(kSize, kSize);output_tex.create(kOutSize, kSize); // kOutSize=kSize/10=2*28=56, 56x560的大小output_digits_tex.create(kOutSize, kSize); // 56x560的大小, 是一个竖条形状sf::Font font; font.loadFromFile("./ui/VCR_OSD_MONO_1.001.ttf");sf::Text text; text.setFont(font); text.setCharacterSize(kOutSize);text.setFillColor(sf::Color(0, 0, 0, 255));// 竖条分成 10 部分, 每个部分是 56x56 的方格, 每个方格绘制一个数字for(unsigned i = 0; i < 10; ++i){text.setPosition(0, i * kOutSize);text.setString(std::to_string(i));output_digits_tex.draw(text);}output_digits_tex.display();// sf::CircleShape brush_circle, cursor_circle; 这里猜测是鼠标绘制时, 鼠标自身 以及 刷子 的形状brush_circle.setFillColor(sf::Color(0, 0, 0));cursor_circle.setFillColor(sf::Color(0, 0, 0, 100));brush_circle.setRadius(radius);cursor_circle.setRadius(radius);// sf::RectangleShape input_rect, output_rect;  这里暂时没看出来用途。input_rect.setSize(sf::Vector2f(kGridSize, kGridSize)); //20x20output_rect.setSize(sf::Vector2f(kOutSize, kOutSize)); //56x56
}

Clear()函数

void Clear()
{paint_tex.clear(sf::Color(255, 255, 255));
}

Clear() 把屏幕左侧的 paint_tex 区域背景颜色设定为白色.

完整代码

这里说的完整代码, 是把刚刚分析的代码摘录出来, 放到 Mnist_UI_my.cpp 里, 并编译运行

#include <SFML/Graphics.hpp>sf::RenderWindow window;
sf::Event event;constexpr int kGridSize = 20, kSize = 28*kGridSize, kOutSize = kSize / 10;
constexpr float kMinRadius = 8.0, kMaxRadius = 30.0, kRadiusStep = 1.0;sf::RenderTexture paint_tex, input_tex, output_tex, output_digits_tex;
float radius{(kMinRadius + kMaxRadius) * 0.5f};
sf::CircleShape brush_circle, cursor_circle;
sf::RectangleShape input_rect, output_rect;void InitWindow()
{window.create(sf::VideoMode(kSize*2 + kOutSize, kSize), "Mnist Demo", sf::Style::Titlebar | sf::Style::Close);paint_tex.create(kSize, kSize);input_tex.create(kSize, kSize);output_tex.create(kOutSize, kSize);output_digits_tex.create(kOutSize, kSize);const std::string asset_dir = "../";sf::Font font; font.loadFromFile(asset_dir+"/ui/VCR_OSD_MONO_1.001.ttf");sf::Text text; text.setFont(font); text.setCharacterSize(kOutSize);text.setFillColor(sf::Color(0, 0, 0, 255));for(unsigned i = 0; i < 10; ++i){text.setPosition(0, i * kOutSize);text.setString(std::to_string(i));output_digits_tex.draw(text);}output_digits_tex.display();brush_circle.setFillColor(sf::Color(0, 0, 0));cursor_circle.setFillColor(sf::Color(0, 0, 0, 100));brush_circle.setRadius(radius);cursor_circle.setRadius(radius);input_rect.setSize(sf::Vector2f(kGridSize, kGridSize));output_rect.setSize(sf::Vector2f(kOutSize, kOutSize));
}void Clear()
{paint_tex.clear(sf::Color(255, 255, 255));
}int main()
{InitWindow();Clear();while(window.isOpen()){while(window.pollEvent(event)){if(event.type == sf::Event::EventType::Closed){window.close();}}sf::Sprite paint_sprite{paint_tex.getTexture()};auto paint_sprite_position = paint_sprite.getPosition();printf("paint_sprite_position: %f, %f\n", paint_sprite_position.x, paint_sprite_position.y);window.draw(sf::Sprite(paint_tex.getTexture()));sf::Sprite input_sprite{input_tex.getTexture()};input_sprite.setPosition(kSize, 0);window.draw(input_sprite);sf::Sprite output_sprite{output_tex.getTexture()};output_sprite.setPosition(kSize*2, 0);window.draw(output_sprite);sf::Sprite output_digits_sprite{output_digits_tex.getTexture()};output_digits_sprite.setPosition(kSize*2, 0);window.draw(output_digits_sprite);window.display();}return 0;
}

由于省略了 event 的处理, 鼠标事件自然是没有响应的, 界面非常枯燥, 看起来只有左右的白色、黑色两个部分:

在这里插入图片描述

3.2 paint 区域的显示和清理

需要先开启鼠标和键盘事件的处理, 然后再启用 paint_tex 的绘制。

处理鼠标事件

main() 函数里处理鼠标事件:

while(window.pollEvent(event))
{...if(event.type == sf::Event::EventType::MouseButtonPressed)mouse_down = true;if(event.type == sf::Event::EventType::MouseButtonReleased)mouse_down = false;
}
if(mouse_down)Paint();

处理键盘事件

main() 函数中处理键盘事件: 如果用户按下了空格键, 那么调用 Clear() 函数来把左侧输入区域显示的内容清空:

while(window.pollEvent(event))
{...if(event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Space){// window.setTitle("Recognize: " + std::to_string(Recognize())); 目前不需要调用 Recognize函数,先注释掉Clear();}
}

由于 Clear() 本身是一个不复杂的函数调用, 仅仅是把 input_tex 这个纹理的颜色设定为白色。 如果是稍微耗时一些的任务,通常是在事件处理函数的地方做判断, 在外部处理。

void Clear()
{paint_tex.clear(sf::Color(255, 255, 255));
}

绘制 paint 区域

调用的 Paint() 函数是本小节的关键

void Paint()
{// 获取鼠标在窗口 window 内的位置sf::Vector2i xy = sf::Mouse::getPosition(window);// 如果鼠标坐标在窗口内部if(xy.x >= 0 && xy.x < kSize && xy.y >= 0 && xy.y < kSize){// 如果鼠标不在左侧的 input_tex 范围, 那么就做 clipint x = std::max(0, std::min(xy.x, kSize)) - radius;// 在纵向方向上, 也做了 clip, 因此如果打算在界面布局上再增加底栏,也是能处理鼠标在 input_tex 的显示的int y = std::max(0, std::min(xy.y, kSize)) - radius;// 设置笔刷的坐标brush_circle.setPosition(x, y);// 在 paint_tex 上绘制笔刷paint_tex.draw(brush_circle);}paint_tex.display();
}

其中存在 sf::CirleShape -> sf::Texture 的对象“存放”关系: 把一个 shape 存放到一个 texture 中。
而在 main() 中则进一步做了 sf::Texture -> sf::Sprite 的处理:

window.draw(sf::Sprite(paint_tex.getTexture()));

在官方教程 https://www.sfml-dev.org/tutorials/2.6/graphics-sprite.php 里给出了解释:

Most (if not all) of you are already familiar with these two very common objects, so let’s define them very briefly.

A texture is an image. But we call it “texture” because it has a very specific role: being mapped to a 2D entity.

A sprite is nothing more than a textured rectangle.

纹理(texture)是一幅图像(image)。但我们称它为 texture,因为它有一个非常具体的作用:被映射到一个2D实体上。

精灵(sprite)只不过是一个带有纹理的矩形.

为什么使用 texture + sprite, 而不是 RectangleShape?

从 SFML 的代码层更容易理解: window.draw() 我们目前写过的代码, 主要是绘制形状, 也绘制过顶点 sf::Vertex. 对于绘制形状:

class Window
{
public:...void draw(const Drawable& drawable, const RenderStates& states = RenderStates::Default);
};

因此, 如果要绘制 texture, 就需要让 texture 继承自 sf::Drawable. 但是 sf::Texturesf::RenderTexture 都没有继承自 sf::Drawable:

class SFML_GRAPHICS_API Texture : GlResource
{...
};
class SFML_GRAPHICS_API RenderTexture : public RenderTarget
{...
};

sf::Sprite 则是继承了 sf::Drawable, 并且能从 sf::Texture 创建对象:

class SFML_GRAPHICS_API Sprite : public Drawable, public Transformable
{
public:explicit Sprite(const Texture& texture); // 从整个 texture 创建 spriteSprite(const Texture& texture, const IntRect& rectangle); // 从 ROI 创建 sprite...
};

因此, 目前遇到的三种绘制方式:

  • sf::CircleShape -> window.draw(circle)
  • sf::Vertex -> window.draw(vertex, 2, sf::Lines)
  • sf::CirleShape -> sf::Texture -> sf::Sprite -> window.draw(sprite)

第三种方式中的 Sprite 是为了承载 Texture, 那么 Texture 是为了什么呢? 准确的说, 是 sf::RenderTexture 对象的 .getTexture() 方法返回的 sf::Texture 对象:

sf::RenderTexture paint_tex, input_tex, output_tex, output_digits_tex;...sf::Sprite input_sprite{input_tex.getTexture()};
input_sprite.setPosition(kSize, 0);
window.draw(input_sprite);

sf::RenderTexturesf::Texture 没有直接的继承关系:

class SFML_GRAPHICS_API RenderTexture : public RenderTarget
{...
};

对于 input_tex 这个 sf::RenderTexture 来说, 它仅仅是被创建 (.create()), 然后就没有主动调用什么方法了; input_sprite 则是对它设定了位置:

input_tex.create(kSize, kSize);sf::Sprite input_sprite{input_tex.getTexture()};
input_sprite.setPosition(kSize, 0);
window.draw(input_sprite);

为什么能设定位置? 因为 sf::Sprite 继承了 Transformable 类:

class SFML_GRAPHICS_API Sprite : public Drawable, public Transformable

看起来好像用 sf::RectangleShape 也能完成同样功能, GPT4 给的解释是:

  1. 复杂度增加:与直接使用sf::RectangleShape相比,从 texture 到 sprite 的方法在实现上更加复杂。你需要处理纹理的加载和管理,以及精灵的创建和属性设置。
  2. 资源管理:使用 texture 和 sprite 可能需要更多的注意力来管理资源,比如确保纹理在使用前已经正确加载,以及在不再需要时释放资源。

sf::Texture 这个纹理数据是被上传到 GPU 显存中, GPU 处理的速度快; 如果有多个 sf::Sprite 实例共享使用同一个 texture, 那么不需要重新上传, 只需要上传一次, 减少了显存使用和数据传输的开销。

完整的代码

把用到的代码抽取出来, 放到 Mnist_UI_my.cpp 中, 本节的代码能够在左侧区域中,使用鼠标绘制, 使用空格键清理:

在这里插入图片描述

#include <SFML/Graphics.hpp>sf::RenderWindow window;
sf::Event event;constexpr int kGridSize = 20, kSize = 28*kGridSize, kOutSize = kSize / 10;
constexpr float kMinRadius = 8.0, kMaxRadius = 30.0, kRadiusStep = 1.0;sf::RenderTexture paint_tex, input_tex, output_tex, output_digits_tex;
float radius{(kMinRadius + kMaxRadius) * 0.5f};
sf::CircleShape brush_circle, cursor_circle;
sf::RectangleShape input_rect, output_rect;void InitWindow()
{window.create(sf::VideoMode(kSize*2 + kOutSize, kSize), "Mnist Demo", sf::Style::Titlebar | sf::Style::Close);paint_tex.create(kSize, kSize);input_tex.create(kSize, kSize);output_tex.create(kOutSize, kSize);output_digits_tex.create(kOutSize, kSize);const std::string asset_dir = "../";sf::Font font; font.loadFromFile(asset_dir+"/ui/VCR_OSD_MONO_1.001.ttf");sf::Text text; text.setFont(font); text.setCharacterSize(kOutSize);text.setFillColor(sf::Color(0, 0, 0, 255));for(unsigned i = 0; i < 10; ++i){text.setPosition(0, i * kOutSize);text.setString(std::to_string(i));output_digits_tex.draw(text);}output_digits_tex.display();brush_circle.setFillColor(sf::Color(0, 0, 0));cursor_circle.setFillColor(sf::Color(0, 0, 0, 100));brush_circle.setRadius(radius);cursor_circle.setRadius(radius);input_rect.setSize(sf::Vector2f(kGridSize, kGridSize));output_rect.setSize(sf::Vector2f(kOutSize, kOutSize));
}void Clear()
{paint_tex.clear(sf::Color(255, 255, 255));
}void Paint()
{sf::Vector2i xy = sf::Mouse::getPosition(window);if(xy.x >= 0 && xy.x < kSize && xy.y >= 0 && xy.y < kSize){int x = std::max(0, std::min(xy.x, kSize)) - radius, y = std::max(0, std::min(xy.y, kSize)) - radius;brush_circle.setPosition(x, y);paint_tex.draw(brush_circle);}paint_tex.display();
}int main()
{InitWindow();Clear();bool mouse_down = false;while(window.isOpen()){while(window.pollEvent(event)){if(event.type == sf::Event::EventType::Closed){window.close();}if(event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Space){//window.setTitle("Recognize: " + std::to_string(Recognize()));Clear();}if(event.type == sf::Event::EventType::MouseButtonPressed)mouse_down = true;if(event.type == sf::Event::EventType::MouseButtonReleased)mouse_down = false;}if(mouse_down)Paint();sf::Sprite paint_sprite{paint_tex.getTexture()};auto paint_sprite_position = paint_sprite.getPosition();printf("paint_sprite_position: %f, %f\n", paint_sprite_position.x, paint_sprite_position.y);window.draw(sf::Sprite(paint_tex.getTexture()));sf::Sprite input_sprite{input_tex.getTexture()};input_sprite.setPosition(kSize, 0);window.draw(input_sprite);sf::Sprite output_sprite{output_tex.getTexture()};output_sprite.setPosition(kSize*2, 0);window.draw(output_sprite);sf::Sprite output_digits_sprite{output_digits_tex.getTexture()};output_digits_sprite.setPosition(kSize*2, 0);window.draw(output_digits_sprite);window.display();}return 0;
}

3.3 显示 input 纹理

所谓 input 纹理, 说的是把窗口左侧的 paint 区域得到的内容, 做处理后, 能够作为 cnn 网络输入的时候(或者之前一点点), 这个处理过的输入是什么样子。 换言之, 是 CNN 网络看到的图像对应的纹理, 我们对它做一个可视化。 可视化的时候, 为了看的清楚, 肯定不是 28x28 那么小的输入,但是 cnn 网络的输入大概是 28x28 的大小。

本小节我们只关注 input 区域的显示, 不关注 cnn 网络的推理, 因此需要展开 Recognize() 函数的大部分, 但也略去其中 snn 对象的 evaluate() 等方法的调用, 也就省略了最终预测结果中的数字的显示。

从键盘事件到Recognize

回顾 main() 中的键盘处理:

			if(event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Space){window.setTitle("Recognize: " + std::to_string(Recognize()));Clear();}

按下空格键后会执行 Recognize()

Recognize()浅析

Recognize() 函数, 将 paint_tex 区域手绘的内容, 拷贝一份独立的图像, 并将每个 20x20 大小的网格“捏成一个像素”, 捏的手法类似于 area resize / average pooling, 但是原始像素被 0/1 二值化处理了, 因此相当于先做阈值为 1 的二值化, 然后做 area resize, 得到了 28x28=764 大小的一维数组 nn_input, 每个元素是 [0, 1] 范围的浮点数。

对于 nn_input 每个元素, 为了在 input_tex 显示, 让每个像素映射到 [0, 255] 范围整数, 并且 ”填充“ 到 20x20 的区域, 这和原本的 "捏” 动作相反, 但是由于“捏”的过程中已经做了二值化处理, 因此现在 “填充” 回去的时候, 效果是 “像素化” 的。

width_normalize() 函数意义不明, 先注释掉。

至于 snn 网络的推理, 现在先把代码注释掉。

unsigned Recognize()
{// 根据 paint 区域绘制的纹理, 创建独立的图像拷贝sf::Image img{paint_tex.getTexture().copyToImage()};// 获取图像像素的 raw bufferconst sf::Uint8 *ptr = img.getPixelsPtr();// 网络输入是 28x28=784 大小,float 类型std::vector<float> nn_input(784);// 将每个 grid 区域(kGridSize x kGridSize, 20x20) 捏成一个像素for(unsigned i = 0; i < 784; ++i){float v = 0.0;unsigned gx = i % 28;unsigned gy = i / 28;unsigned px = gx * (kGridSize << 2);unsigned py = gy * kGridSize;// 对于每个 20x20 大小的方格, 如果不是 0,那么计数器加 1, 如果是 0 则计数器不变for(unsigned y = py; y < py + kGridSize; ++y){for(unsigned x = px; x < px + (kGridSize << 2); x += 4){v += float(ptr[y * (kSize << 2) + x] == 0);}}// 统计了 20x20 方格区域内非 0 元素数量 v, 数量 v 除以总数 20x20, 这个比值作为 28x28 网络输入的一个元素。nn_input[i] = v / float(kGridSize * kGridSize);}// width_normalize(&nn_input); 先不调用它,看是什么效果for(unsigned i = 0; i < 784; ++i){// 把 nn_input[i], 从 [0, 1] 范围的浮点数转到 [0, 255] 范围的整数 cunsigned c = 255 * nn_input[i];c = std::min(c, 255u);// 在 20x20 的区域内, 绘制相同的颜色 cunsigned gx = i % 28;unsigned gy = i / 28;input_rect.setPosition(gx * kGridSize, gy * kGridSize);input_rect.setFillColor(sf::Color(c, c, c, 255));input_tex.draw(input_rect); // 在一个 texture 的 ROI 区域上进行绘制//putchar(nn_input[i] >= 0.25 ? (nn_input[i] >= 0.5 ? (nn_input[i] >= 0.75 ? '@' : '?') : '.') : ' ');//if(i % 28 == 27) putchar('\n');}input_tex.display(); // 更新 target texture 内容。 如果不调用,我观察到的是上下颠倒的内容// 先不看 output 的处理
#if 0{snn.Evaluate(nn_input);unsigned res = std::max_element(snn.GetOutput(), snn.GetOutput() + 10) - snn.GetOutput();for(unsigned i = 0; i < 10; ++i){unsigned c = 255 * snn.GetOutput()[i];c = std::min(c, 255u);output_rect.setPosition(0, i * kOutSize);output_rect.setFillColor(sf::Color(c, c, c, 255));output_tex.draw(output_rect);}output_tex.display();}
#endifreturn 0;
}

补充说明 input_tex.display() 的调用: 它是更新纹理绘制的内容, 如果不调用, 那么内容是 “垃圾值”, 我在 M1 mac-mini 上的结果是, 不调用它会得到上下颠倒的内容。

效果和代码

在这里插入图片描述

#include <SFML/Graphics.hpp>sf::RenderWindow window;
sf::Event event;constexpr int kGridSize = 20, kSize = 28*kGridSize, kOutSize = kSize / 10;
constexpr float kMinRadius = 8.0, kMaxRadius = 30.0, kRadiusStep = 1.0;sf::RenderTexture paint_tex, input_tex, output_tex, output_digits_tex;
float radius{(kMinRadius + kMaxRadius) * 0.5f};
sf::CircleShape brush_circle, cursor_circle;
sf::RectangleShape input_rect, output_rect;void InitWindow()
{window.create(sf::VideoMode(kSize*2 + kOutSize, kSize), "Mnist Demo", sf::Style::Titlebar | sf::Style::Close);paint_tex.create(kSize, kSize);input_tex.create(kSize, kSize);output_tex.create(kOutSize, kSize);output_digits_tex.create(kOutSize, kSize);const std::string asset_dir = "../";sf::Font font; font.loadFromFile(asset_dir+"/ui/VCR_OSD_MONO_1.001.ttf");sf::Text text; text.setFont(font); text.setCharacterSize(kOutSize);text.setFillColor(sf::Color(0, 0, 0, 255));for(unsigned i = 0; i < 10; ++i){text.setPosition(0, i * kOutSize);text.setString(std::to_string(i));output_digits_tex.draw(text);}output_digits_tex.display();brush_circle.setFillColor(sf::Color(0, 0, 0));cursor_circle.setFillColor(sf::Color(0, 0, 0, 100));brush_circle.setRadius(radius);cursor_circle.setRadius(radius);input_rect.setSize(sf::Vector2f(kGridSize, kGridSize));output_rect.setSize(sf::Vector2f(kOutSize, kOutSize));
}void Clear()
{paint_tex.clear(sf::Color(255, 255, 255));
}void Paint()
{sf::Vector2i xy = sf::Mouse::getPosition(window);if(xy.x >= 0 && xy.x < kSize && xy.y >= 0 && xy.y < kSize){int x = std::max(0, std::min(xy.x, kSize)) - radius, y = std::max(0, std::min(xy.y, kSize)) - radius;brush_circle.setPosition(x, y);paint_tex.draw(brush_circle);}paint_tex.display();
}unsigned Recognize()
{sf::Image img{paint_tex.getTexture().copyToImage()};const sf::Uint8 *ptr = img.getPixelsPtr();std::vector<float> nn_input(784);for(unsigned i = 0; i < 784; ++i){float v = 0.0;unsigned gx = i % 28, gy = i / 28;unsigned px = gx * (kGridSize << 2), py = gy * kGridSize;for(unsigned y = py; y < py + kGridSize; ++y)for(unsigned x = px; x < px + (kGridSize << 2); x += 4)v += float(ptr[y * (kSize << 2) + x] == 0);nn_input[i] = v / float(kGridSize * kGridSize);}// width_normalize(&nn_input);for(unsigned i = 0; i < 784; ++i){unsigned gx = i % 28, gy = i / 28, c = 255 * nn_input[i];c = std::min(c, 255u);input_rect.setPosition(gx * kGridSize, gy * kGridSize);input_rect.setFillColor(sf::Color(c, c, c, 255));input_tex.draw(input_rect);//putchar(nn_input[i] >= 0.25 ? (nn_input[i] >= 0.5 ? (nn_input[i] >= 0.75 ? '@' : '?') : '.') : ' ');//if(i % 28 == 27) putchar('\n');}input_tex.display(); // 更新 target texture 内容。 如果不调用,我观察到的是上下颠倒的内容#if 0{snn.Evaluate(nn_input);unsigned res = std::max_element(snn.GetOutput(), snn.GetOutput() + 10) - snn.GetOutput();for(unsigned i = 0; i < 10; ++i){unsigned c = 255 * snn.GetOutput()[i];c = std::min(c, 255u);output_rect.setPosition(0, i * kOutSize);output_rect.setFillColor(sf::Color(c, c, c, 255));output_tex.draw(output_rect);}output_tex.display();}
#endifreturn 0;
}int main()
{InitWindow();Clear();bool mouse_down = false;while(window.isOpen()){while(window.pollEvent(event)){if(event.type == sf::Event::EventType::Closed){window.close();}if(event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Space){window.setTitle("Recognize: " + std::to_string(Recognize()));Clear();}if(event.type == sf::Event::EventType::MouseButtonPressed)mouse_down = true;if(event.type == sf::Event::EventType::MouseButtonReleased)mouse_down = false;}if(mouse_down)Paint();sf::Sprite paint_sprite{paint_tex.getTexture()};auto paint_sprite_position = paint_sprite.getPosition();printf("paint_sprite_position: %f, %f\n", paint_sprite_position.x, paint_sprite_position.y);window.draw(sf::Sprite(paint_tex.getTexture()));sf::Sprite input_sprite{input_tex.getTexture()};input_sprite.setPosition(kSize, 0);window.draw(input_sprite);sf::Sprite output_sprite{output_tex.getTexture()};output_sprite.setPosition(kSize*2, 0);window.draw(output_sprite);sf::Sprite output_digits_sprite{output_digits_tex.getTexture()};output_digits_sprite.setPosition(kSize*2, 0);window.draw(output_digits_sprite);window.display();}return 0;
}

3.4 执行网络推理

加载网络文件

int main(int argc, char **argv)
{if(argc != 2){printf("Usage: ./MnistUI [snn filename]\n");return EXIT_FAILURE;}snn.Load(argv[1]);...
}

width_normalize(): 裁剪掉无效图像区域

没调用 width_normalize() 时, input_tex 里存在大量空白区域(黑色), 数字大小和绘制大小一样的;

在这里插入图片描述

调用 width_normalize() 后, 相当于获取了 bounding box, 并将 bounding box 外部的区域建材掉, 将剩余的有效区域像素放大到了 28x28 大小; 识别准确率也上来了:

在这里插入图片描述

关于 width_normalize() 的源码, 本篇不做分析, 下一篇剖析 SimpleNN 实现的代码时再分析。

3.5 美化: 绘制 cursor

鼠标滚轮控制 cursor 大小

	while(window.isOpen()){while(window.pollEvent(event)){...if(event.type == sf::Event::EventType::MouseWheelScrolled){radius += kRadiusStep * (event.mouseWheel.x > 0 ? -1 : 1);radius = std::min(std::max(kMinRadius, radius), kMaxRadius);brush_circle.setRadius(radius);cursor_circle.setRadius(radius);}}}

把鼠标形状改为圆球: Cursor()

实际上是鼠标周围一圈有一个圆形, 就像是拖着一个墨球:

int main()
{while() {while() {...sf::Sprite output_digits_sprite{output_digits_tex.getTexture()};output_digits_sprite.setPosition(kSize*2, 0);window.draw(output_digits_sprite);Cursor(); /// 此处修改鼠标形状window.display();}
}

在这里插入图片描述

void Cursor()
{sf::Vector2i xy = sf::Mouse::getPosition(window);if(xy.x >= 0 && xy.x < kSize && xy.y >= 0 && xy.y < kSize){int x = std::max(0, std::min(xy.x, kSize)) - radius, y = std::max(0, std::min(xy.y, kSize)) - radius;cursor_circle.setPosition(x, y);window.draw(cursor_circle);}
}

4. 最终代码

这里贴出我做测试、添加了一些注释的 Mnist_UI_my.cpp 代码, 大部分是本篇解读过的, SimpleNN snn 对应的 NN.hpp, 以及 width_normalize() 对应的 MNIST/Util.hpp 则不在这个文件里, 使用原版的。

#include <SFML/Graphics.hpp>
#include "NN/NN.hpp"
#include "MNIST/Util.hpp"sf::RenderWindow window;
sf::Event event;constexpr int kGridSize = 20, kSize = 28*kGridSize, kOutSize = kSize / 10;
constexpr float kMinRadius = 8.0, kMaxRadius = 30.0, kRadiusStep = 1.0;sf::RenderTexture paint_tex, input_tex, output_tex, output_digits_tex;
float radius{(kMinRadius + kMaxRadius) * 0.5f};
sf::CircleShape brush_circle, cursor_circle;
sf::RectangleShape input_rect, output_rect;SimpleNN snn;void InitWindow()
{window.create(sf::VideoMode(kSize*2 + kOutSize, kSize), "Mnist Demo", sf::Style::Titlebar | sf::Style::Close);paint_tex.create(kSize, kSize);input_tex.create(kSize, kSize);output_tex.create(kOutSize, kSize);output_digits_tex.create(kOutSize, kSize);const std::string asset_dir = "../";sf::Font font; font.loadFromFile(asset_dir+"/ui/VCR_OSD_MONO_1.001.ttf");sf::Text text; text.setFont(font); text.setCharacterSize(kOutSize);text.setFillColor(sf::Color(0, 0, 0, 255));for(unsigned i = 0; i < 10; ++i){text.setPosition(0, i * kOutSize);text.setString(std::to_string(i));output_digits_tex.draw(text);}output_digits_tex.display();brush_circle.setFillColor(sf::Color(0, 0, 0));cursor_circle.setFillColor(sf::Color(0, 0, 0, 100));brush_circle.setRadius(radius);cursor_circle.setRadius(radius);input_rect.setSize(sf::Vector2f(kGridSize, kGridSize));output_rect.setSize(sf::Vector2f(kOutSize, kOutSize));
}void Clear()
{paint_tex.clear(sf::Color(255, 255, 255));
}void Cursor()
{sf::Vector2i xy = sf::Mouse::getPosition(window);if(xy.x >= 0 && xy.x < kSize && xy.y >= 0 && xy.y < kSize){int x = std::max(0, std::min(xy.x, kSize)) - radius, y = std::max(0, std::min(xy.y, kSize)) - radius;cursor_circle.setPosition(x, y);window.draw(cursor_circle);}
}void Paint()
{sf::Vector2i xy = sf::Mouse::getPosition(window);if(xy.x >= 0 && xy.x < kSize && xy.y >= 0 && xy.y < kSize){int x = std::max(0, std::min(xy.x, kSize)) - radius, y = std::max(0, std::min(xy.y, kSize)) - radius;brush_circle.setPosition(x, y);paint_tex.draw(brush_circle);}paint_tex.display();
}unsigned Recognize()
{// 根据 paint 区域绘制的纹理, 创建独立的图像拷贝sf::Image img{paint_tex.getTexture().copyToImage()};// 获取图像像素的 raw bufferconst sf::Uint8 *ptr = img.getPixelsPtr();// 网络输入是 28x28=784 大小,float 类型std::vector<float> nn_input(784);// 将每个 grid 区域(kGridSize x kGridSize, 20x20) 捏成一个像素for(unsigned i = 0; i < 784; ++i){float v = 0.0;unsigned gx = i % 28;unsigned gy = i / 28;unsigned px = gx * (kGridSize << 2);unsigned py = gy * kGridSize;// 对于每个 20x20 大小的方格, 如果不是 0,那么计数器加 1, 如果是 0 则计数器不变for(unsigned y = py; y < py + kGridSize; ++y){for(unsigned x = px; x < px + (kGridSize << 2); x += 4){v += float(ptr[y * (kSize << 2) + x] == 0);}}// 统计了 20x20 方格区域内非 0 元素数量 v, 数量 v 除以总数 20x20, 这个比值作为 28x28 网络输入的一个元素。nn_input[i] = v / float(kGridSize * kGridSize);}width_normalize(&nn_input); // 负责砍掉图像周围的空白区域for(unsigned i = 0; i < 784; ++i){// 把 nn_input[i], 从 [0, 1] 范围的浮点数转到 [0, 255] 范围的整数 cunsigned c = 255 * nn_input[i];c = std::min(c, 255u);// 在 20x20 的区域内, 绘制相同的颜色 cunsigned gx = i % 28;unsigned gy = i / 28;input_rect.setPosition(gx * kGridSize, gy * kGridSize);input_rect.setFillColor(sf::Color(c, c, c, 255));input_tex.draw(input_rect); // 在一个 texture 的 ROI 区域上进行绘制//putchar(nn_input[i] >= 0.25 ? (nn_input[i] >= 0.5 ? (nn_input[i] >= 0.75 ? '@' : '?') : '.') : ' ');//if(i % 28 == 27) putchar('\n');}input_tex.display(); // 更新 target texture 内容。 如果不调用,我观察到的是上下颠倒的内容#if 1{snn.Evaluate(nn_input);unsigned res = std::max_element(snn.GetOutput(), snn.GetOutput() + 10) - snn.GetOutput();for(unsigned i = 0; i < 10; ++i){unsigned c = 255 * snn.GetOutput()[i];c = std::min(c, 255u);output_rect.setPosition(0, i * kOutSize);output_rect.setFillColor(sf::Color(c, c, c, 255));output_tex.draw(output_rect);}output_tex.display();return res;}
#endifreturn 0;
}int main(int argc, char **argv)
{if(argc != 2){printf("Usage: ./MnistUI [snn filename]\n");return EXIT_FAILURE;}snn.Load(argv[1]);InitWindow();Clear();bool mouse_down = false;while(window.isOpen()){while(window.pollEvent(event)){if(event.type == sf::Event::EventType::Closed){window.close();}if(event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Space){window.setTitle("Recognize: " + std::to_string(Recognize()));Clear();}if(event.type == sf::Event::EventType::MouseButtonPressed)mouse_down = true;if(event.type == sf::Event::EventType::MouseButtonReleased)mouse_down = false;}if(mouse_down)Paint();sf::Sprite paint_sprite{paint_tex.getTexture()};auto paint_sprite_position = paint_sprite.getPosition();printf("paint_sprite_position: %f, %f\n", paint_sprite_position.x, paint_sprite_position.y);window.draw(sf::Sprite(paint_tex.getTexture()));sf::Sprite input_sprite{input_tex.getTexture()};input_sprite.setPosition(kSize, 0);window.draw(input_sprite);sf::Sprite output_sprite{output_tex.getTexture()};output_sprite.setPosition(kSize*2, 0);window.draw(output_sprite);sf::Sprite output_digits_sprite{output_digits_tex.getTexture()};output_digits_sprite.setPosition(kSize*2, 0);window.draw(output_digits_sprite);Cursor();window.display();}return 0;
}

总结

本篇分析了 SimpleNN 的 UI 部分, 它是基于 SFML 实现的交互式手写数字识别程序, 提供了鼠标绘制手写数字, 空格键触发输入的处理和显示、 网络的推理和结果显示, 即使不会写代码也能使用它执行手写数字识别。

具体的代码分析中, 先从界面布局的划分(paint region, input region, output region) 入手, 然后对 paint, input 两个区域的内容的绘制、 鼠标键盘的交互, 做了比较详细的分析。 而输入数据的归一化, 网络的推理, 分析的稍微粗糙一些, 主要是因为相关图像处理内容比较熟悉, 不是 UI 界面的重点。

对于 Texture 的使用, 增加了一些经验, 目前遇到过的处理方式有这几种:

  • sf::CircleShape -> window.draw(circle)
  • sf::Vertex -> window.draw(vertex, 2, sf::Lines)
  • sf::CirleShape -> sf::Texture -> sf::Sprite -> window.draw(sprite)
    其中最后一种方式,能够方便的对一个 ROI 区域进行绘制, 比手动重新绘制独立的 RectangleShape 要更方便,运行效率也更高:
sf::RectangleShape input_rect;
input_rect.setSize(sf::Vector2f(kGridSize, kGridSize));
input_rect.setPosition(gx * kGridSize, gy * kGridSize);sf::RenderTexture input_tex;
input_tex.create(kSize, kSize);
input_tex.draw(input_rect); // 区域渲染input_tex.display(); // update content

这也让我想到前一篇基于 SFML 实现的 tic-tac-toe 井字棋游戏,渲染的代码写的不太好,是对 3x3 每个区域分别绘制纹理,其实可以制作一个整个的纹理, 然后更新每个 grid 区域。

因此后续的方向有这几个:

  • 基于 texture 更新 ROI 区域的思路, 重构 tic-tac-toe 的实现
  • 进一步分析 SimpleNN 的源码, 包括推理 和 训练两个部分

References

  • https://github.com/AdamYuan/SimpleNN
  • SFML Tutorial - Sprites and textures

相关文章:

小游戏和GUI编程(7) | SimpleNN 界面源码解析

小游戏和GUI编程(7) | SimpleNN 界面源码解析 0. 简介 SimpleNN 是 AdamYuan 在高中一年级时用 1 天时间写出来的简易 CNN, 使用 SFML 做 UI, 用于交互式输入手写数字&#xff0c;这个数字被训练好的 CNN 网络执行推理得到识别结果, 它的运行效果如下&#xff1a; 这一篇我们…...

c++设计模式之代理模式

作用 代理模式主要用于&#xff0c;通过代理类&#xff0c;来控制实际对象的访问权限 案例 class VideoSite { public:virtual void freeVideo()0;virtual void vipVideo()0;virtual void trickVideo()0; };class FixBugVideoSite:public VideoSite { public:void freeVideo()…...

第5个-模糊加载

Day 5 - Blurry Loading 1. 项目展示 2. 分析思路 变化过程 数字从 0 不断增长到 100&#xff1b;中间的百分比数字逐渐消失&#xff0c;即透明度 opacity 从 1 到 0&#xff1b;背景图片从模糊变为清晰&#xff0c;滤镜 filter.blur()的参数设置为从 30px 到 0px。 小 tips…...

rtt设备io框架面向对象学习-adc设备

目录 1.adc设备基类2.adc设备基类的子类3.初始化/构造流程3.1设备驱动层3.2 设备驱动框架层3.3 设备io管理层 4.总结5.使用 1.adc设备基类 此层处于设备驱动框架层。也是抽象类。 在/ components / drivers / include / drivers 下的adc.h定义了如下adc设备基类 struct rt_ad…...

面试官:介绍一下Exception和Error之间的区别

前言 大家好&#xff0c;我是chowley&#xff0c;在我之前的面试中&#xff0c;遇到过这样一个问题&#xff1a;Exception和Error之间有什么区别&#xff1f;今天我就来好好地总结一下&#xff01; 主体 在Java编程中&#xff0c;Exception和Error都是Java中的可抛出对象&am…...

【RabbitMQ(一)】:基本介绍 | 配置安装与快速入门

应该是新年前最后一篇博客了&#xff0c;明天浅浅休息一下&#xff0c;提前祝大家新年快乐捏&#xff01;&#x1f60a;&#x1f60a;&#x1f60a; 01. 基础理解 1.1 同步调用和异步调用 &#x1f449; 同步调用 的时候调用者会 阻塞 等待被调用函数或方法执行完成&#xff…...

ElasticSearch之search API

写在前面 本文看下查询相关内容&#xff0c;这也是我们在实际工作中接触的最多的&#xff0c;所以有必要好好学习下&#xff01; 1&#xff1a;查询的分类 主要分为如下2类&#xff1a; 1:基于get查询参数的URI search 2&#xff1a;基于post body的request body search&am…...

07-Java桥接模式 ( Bridge Pattern )

Java桥接模式 摘要实现范例 桥接模式&#xff08;Bridge Pattern&#xff09;是用于把抽象化与实现化解耦&#xff0c;使得二者可以独立变化 桥接模式涉及到一个作为桥接的接口&#xff0c;使得实体类的功能独立于接口实现类&#xff0c;这两种类型的类可被结构化改变而互不影…...

golang集成sentry: go-redis

网上没有找到go-redis集成sentry的库&#xff0c; 所以我简单实现了一个 代码&#xff1a; https://github.com/Shujie-Tan/go-redis-sentry 使用方法&#xff1a; import (redis_sentry "github.com/Shujie-Tan/go-redis-sentry" ) rdb : redis.NewClient(&re…...

用EXCEL从地址(上海)中提取各区(浦东新区等区)信息

背景&#xff1a; 朋友工作需要经常用EXCEL把各上海用户收货地址中的区提取出来&#xff0c;之前一直手动处理&#xff0c;希望我帮忙用EXCEL公式直接提取处理。 数据样式&#xff1a; 中国上海市浦东新区A小区 上海徐汇区B小区 中国&#xff0c;上海&#xff0c;浦东新区&a…...

关于在分布式环境中RVN和使用场景的介绍3

简介 在《关于在分布式环境中RVN和使用场景的介绍2》和《关于在分布式环境中RVN和使用场景的介绍1》中我们介绍了RVN的概念和在一些具体用例中的使用。在本文中我们讨论一下在分布式环境中使用RVN需要注意的问题。 问题 我们在收到一条待处理的事件时&#xff0c;需要检查该…...

计算最小公倍数math.lcm()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 计算最小公倍数 math.lcm() 请问以下代码输出的结果是&#xff1f; import math print("【执行】math.lcm(2, 4)") print(math.lcm(2, 4)) print("【执行】math.lcm(1, 2, 3…...

VUE SEO 几种方案经典面试题

1、SSR服务器渲染 Vue.js 是构建客户端应用程序的框架。默认情况下&#xff0c;可以再浏览器中输出Vue组件&#xff0c;进行生成DOM和操作DOM。然而&#xff0c;也可以将同一个组件渲染未服务器端的HTML字符串&#xff0c;将它们直接发送到浏览器&#xff0c;最后将这些静态标…...

Python和VBA批量提取Word中的表格

表格在word文档中常见的文档元素之一。操作word文件时有时需要提取文件中多个表格的内容到一个新的文件&#xff0c;甚至有时还会要提取题注信息。 今天&#xff0c;给大家分享两种批量提取文档中表格的两种方法&#xff0c;分别是VBA法和Python法两种。 一、VBA法提取word中…...

Swift Combine 有序的异步操作 从入门到精通十二

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…...

国产航顺HK32F030M: 超声波测距模块串口通信数据接收与处理

参考代码 /************************************************************************************************** * file usart_async_tx_no_int_rx_rxneint.c * brief 异步串口通信例程, 通过查询TXE标志发送数据,通过RXNE中断接收数据,当中断接收到数据后会将 * …...

idea:如何连接数据库

1、在idea中打开database: 2、点击 ‘’ ---> Data Source ---> MySQL 3、输入自己的账号和密码其他空白处可以不填&#xff0c;用户和密码可以在自己的mysql数据库中查看 4、最后选择自己需要用的数据库&#xff0c;点击运用ok&#xff0c;等待刷新即可 最后&#xff1a…...

JS中ES5和ES6的区别

前言 ES5是JavaScript的第五个修订版本&#xff0c;于2009年发布。而ES6是JavaScript的第六个修订版本&#xff0c;也称为ES2015&#xff0c;于2015年发布。以下是它们两个版本之前的一些区别&#xff1a; 变量声明方式 在ES5中&#xff0c;使用var关键字进行变量声明&#…...

软考24-上午题-图1

一、数据结构的回忆 线性结构&#xff1a;&#xff08;一对一&#xff09; 除首结点没有前驱、末尾结点没有后继外&#xff0c;一个结点只有唯一的一个直接前驱和唯一的一个直接后继。 树结构&#xff1a;&#xff08;一对多&#xff09; 除根节点没有前驱节点外&#xff0c;…...

书生·浦语大模型第四课作业

基础作业&#xff1a; 构建数据集&#xff0c;使用 XTuner 微调 InternLM-Chat-7B 模型, 让模型学习到它是你的智能小助手&#xff0c;效果如下图所示&#xff0c;本作业训练出来的模型的输出需要将不要葱姜蒜大佬替换成自己名字或昵称&#xff01; 1.安装 # 如果你是在 Int…...

勒索攻击风起云涌,Sodinokibi深度分析

前言 Sodinokibi勒索病毒&#xff0c;又称为REvil勒索病毒&#xff0c;这款勒索病毒最早在国内被发现是2019年4月份&#xff0c;笔者在早期分析这款勒索病毒的时候就发现它与其他勒索病毒不同&#xff0c;于是被笔者称为GandCrab勒索病毒的“接班人”&#xff0c;为什么它是Ga…...

1124. 骑马修栅栏(欧拉路径,模板)

农民John每年有很多栅栏要修理。 他总是骑着马穿过每一个栅栏并修复它破损的地方。 John是一个与其他农民一样懒的人。 他讨厌骑马&#xff0c;因此从来不两次经过一个栅栏。 你必须编一个程序&#xff0c;读入栅栏网络的描述&#xff0c;并计算出一条修栅栏的路径&#xf…...

C# CAD2016获取数据操作BlockTableRecord、Polyline、DBObject

一、数据操作说明 //DBObject 基础类 DBObject dbObj (DBObject)tr.GetObject(outerId, OpenMode.ForRead); //Polyline 线段类 Polyline outerPolyline (Polyline)tr.GetObject(outerId, OpenMode.ForRead); //BlockTableRecord 块表类 BlockTableRecord modelSpace (Bloc…...

java SSM新闻管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM新闻管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S…...

Linux_线程

线程与进程 多级页表 线程控制 线程互斥 线程同步 生产者消费者模型 常见概念 下面选取32位系统举例。 一.线程与进程 上图是曾经我们认为进程所占用的资源的集合。 1.1 线程概念 线程是一个执行分支&#xff0c;执行粒度比进程细&#xff0c;调度成本比进程低线程是cpu…...

【selenium】

selenium是一个Web的自动化测试工具&#xff0c;最初是为网站自动化测试而开发的。Selenium可以直接调用浏览器&#xff0c;它支持所有主流的浏览器。其本质是通过驱动浏览器&#xff0c;完成模拟浏览器操作&#xff0c;比如挑战&#xff0c;输入&#xff0c;点击等。 下载与打…...

HX711压力传感器学习一(STM32)

目录 原理图&#xff1a;​ 引脚介绍&#xff1a; HX711介绍工作原理: 程序讲解&#xff1a; 整套工程&#xff1a; 发送的代码工程&#xff0c;与博客的不一致&#xff0c;如果编译有报错请按照报错和博客进行修改 原理图&#xff1a; 引脚介绍&#xff1a; VCC和GND引…...

作业2.13

1、选择题 1.1、若有定义语句&#xff1a;int a[3][6]; &#xff0c;按在内存中的存放顺序&#xff0c;a 数组的第10个元素是 D A&#xff09;a[0][4] B) a[1][3] C)a[0][3] D)a[1][4] 1.2、有数组 int a[5] {10&#xff0c;20&#xff0c;30&#xff0c;40&#xff0c;50},…...

ArcGIS学习(七)图片数据矢量化

ArcGIS学习(七)图片数据矢量化 通过上面几个任务的学习,大家应该已经掌握了ArcGIS的基础操作,并且学习了坐标系和地理数据库这两个非常重要且稍微难一些的专题。从这一任务开始,让我们进入到实战案例板块。 首先进入第一个案例一一图片数据矢量化。 我们在平时的工作学…...

G口大流量服务器选择的关键点有哪些?

G口服务器指的是接入互联网的带宽达到1Gbps以上的服务器&#xff0c;那么选择使用G口大流量服务器的用户需要注意哪些选择 关键点呢?小编为您整理关于G口大流量服务器的关键点。 G口服务器通常被用于需要大带宽支持的业务场景&#xff0c;比如视频流媒体、金融交易平台、电子商…...

MongoDB聚合:$unset

使用$unset阶段可移除文档中的某些字段。从版本4.2开始支持。 语法 移除单个字段&#xff0c;可以直接指定要移除的字段名&#xff1a; { $unset: "<field>" }移除多个字段&#xff0c;可以指定一个要移除字段名的数组&#xff1a; { $unset: [ "<…...

DS Wannabe之5-AM Project: DS 30day int prep day14

Q1. What is Alexnet? Q2. What is VGGNet? Q3. What is VGG16? Q4. What is ResNet? At the ILSVRC 2015, so-called Residual Neural Network (ResNet) by the Kaiming He et al introduced the anovel architecture with “skip connections” and features heavy b…...

【程序设计竞赛】C++与Java的细节优化

必须强调下&#xff0c;以下的任意一种优化&#xff0c;都应该是在本身采用的算法没有任何问题情况下的“锦上添花”&#xff0c;而不是“雪中送炭”。 如果下面的说法存在误导&#xff0c;请专业大佬评论指正 读写优化 C读写优化——解除流绑定 在ACM里&#xff0c;经常出现…...

Java缓冲流——效率提升深度解析

前言 大家好&#xff0c;我是chowley&#xff0c;在我之前的项目中&#xff0c;用到了缓冲流来提高字符流之间的比较速度&#xff0c;缓冲流的主要作用类似于数据库缓存&#xff0c;提高IO操作效率。 缓冲流 在Java的输入输出操作中&#xff0c;缓冲流是提高性能的重要工具之…...

16 亚稳态原理和解决方案

1. 亚稳态原理 亚稳态是指触发器无法在某个规定的时间段内到达一个可以确认的状态。在同步系统中&#xff0c;输入总是与时钟同步&#xff0c;因此寄存器的setup time和hold time是满足的&#xff0c;一般情况下是不会发生亚稳态情况的。在异步信号采集中&#xff0c;由于异步…...

C# OCR识别图片中的文字

1、从NuGet里面安装Spire.OCR 2、安装之后&#xff0c;找到安装路径下&#xff0c;默认生成的packages文件夹&#xff0c;复制该文件夹路径下的 6 个dll文件到程序的根目录 3、调用读取方法 OcrScanner scanner new OcrScanner(); string path "C:\1.png"; scann…...

使用python-numpy实现一个简单神经网络

目录 前言 导入numpy并初始化数据和激活函数 初始化学习率和模型参数 迭代更新模型参数&#xff08;权重&#xff09; 小彩蛋 前言 这篇文章&#xff0c;小编带大家使用python-numpy实现一个简单的三层神经网络&#xff0c;不使用pytorch等深度学习框架&#xff0c;来理解…...

CSS定位装饰

网页常见布局方式 标准流 块级元素独占一行---垂直布局 行内元素/行内块元素一行显示多个----水平布局 浮动 可以让原本垂直布局的块级元素变成水平布局 定位 可以让元素自由的摆放在网页的任意位置 一般用于盒子之间的层叠情况 使用定位步骤 设置定位方式 属性名&am…...

java之jvm详解

JVM内存结构 程序计数器 Program Counter Register程序计数器(寄存器) 程序计数器在物理层上是通过寄存器实现的 作用&#xff1a;记住下一条jvm指令的执行地址特点 是线程私有的(每个线程都有属于自己的程序计数器)不会存在内存溢出 虚拟机栈(默认大小为1024kb) 每个线…...

vue3学习——集成sass

安装 pnpm i sass sass-loader -D在vite.config.ts文件配置: export default defineConfig({css: {preprocessorOptions: {scss: {javascriptEnabled: true,additionalData: import "./src/styles/variable.scss";,},},},} }创建三个文件 src/styles/index.scss //…...

开关电源学习之Boost电路

如果我们需要给一个输入电压为5V的芯片供电&#xff0c;而我们只有一个3.3V的电源&#xff0c;那怎么办&#xff1f; 我们能不能把3.3V的电压升到5V&#xff1f; 一、电感的简介 而在升压的电路设计方案中&#xff0c;使用到一个重要的元器件&#xff1a;电感。 电感的特性…...

QRegExp的学习

【QT学习】QRegExp类正则表达式&#xff08;一文读懂&#xff09;-CSDN博客 [ ]:匹配括号内输入的任意字符 例&#xff1a;[123]:可以是1或2或3 {m&#xff0c;n}表达式至少重复m次&#xff0c;至多重复n次。 例&#xff1a;"ba{1,3}"可以匹配 "ba"或&…...

28.Stream流

Stream流 1. 概述2. 方法2.1 开始生成方法2.1.1 概述2.1.2 方法2.1.3 代码示例 2.2 中间操作方法2.2.1 概述2.2.2 方法2.2.3 代码示例 2.3 终结操作方法2.3.1 概述2.3.2 方法2.3.3 代码示例 2.4 收集操作方法2.4.1 概述2.4.2 方法2.4.3 代码示例 3. 代码示例14. 代码示例25. 代…...

大数据应用对企业的价值

目录 一、大数据应用价值 1.1 大数据技术分析 1.2 原有技术场景的优化 1.2.1 数据分析优化 1.2.2 高并发数据处理 1.3 通过大数据构建新需求 1.3.1 智能推荐 1.3.2 广告系统 1.3.3 产品/流程优化 1.3.4 异常检测 1.3.5 智能管理 1.3.6 人工智能和机器学习 二、大数…...

【51单片机】LED点阵屏(江科大)

9.1LED点阵屏 1.LED点阵屏介绍 LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字、图片、视频等。 2.LED点阵屏工作原理 LED点阵屏的结构类似于数码管,只不过是数码管把每一列的像素以“8”字型排列而已。原理图如下 每一行的阳极连在一起,每一列…...

Microsoft OneNote 图片文字提取

Microsoft OneNote 图片文字提取 1. 文件 -> 新建 -> 我的电脑 -> 名称 -> 位置 -> 创建笔记本2. 插入图片​​​3. 复制图片中的文本References 1. 文件 -> 新建 -> 我的电脑 -> 名称 -> 位置 -> 创建笔记本 ​ 2. 插入图片 ​​​3. 复制图片…...

Linux系统安全——iptables相关总结

在使用iptables时注意要先关闭firewalld&#xff08;systemctl stop firewalld.service&#xff09; 1.查看iptables规则 iptables -vnL 选项含义-v查看时显示更多详细信息-n所有字段以数字形式显示-L查看规则列表 例&#xff0c;拒绝来自192.168.241.22的源地址 直接丢弃 …...

深度学习(14)--x.view()详解

在torch中&#xff0c;常用view()函数来改变tensor的形状 查询官方文档&#xff1a; torch.Tensor.view — PyTorch 2.2 documentationhttps://pytorch.org/docs/stable/generated/torch.Tensor.view.html#torch.Tensor.view示例 1.创建一个4x4的二维数组进行测试 x torch.…...

最新wordpress外贸主题

日用百货wordpress外贸主题 蓝色大气的wordpress外贸主题&#xff0c;适合做日用百货的外贸公司搭建跨境电商网站使用。 https://www.jianzhanpress.com/?p5248 添加剂wordpress外贸建站主题 橙色wordpress外贸建站主题&#xff0c;适合做食品添加剂或化工添加剂的外贸公司…...

Spring Cloud Gateway:使用RestController动态更新路由

相关类介绍 动态路由&#xff08;自己控制&#xff0c;非注册中心控制&#xff09;涉及两个很重要的Bean&#xff1a; RouteDefinitionWriter&#xff1a;用于添加、修改、删除路由规则。RouteDefinitionLocator&#xff1a;用于查询路由规则。 以及一个相关事件&#xff1a…...