Imgui(3) | 基于 imgui-SFML 的 mnist 数据集查看器
文章目录
0. 介绍
把mnist数据集保存为多张.png图像、 train-label.txt 后, 编写一个 GUI 程序,查看图像和对应的标签。
这是一个简陋的demo,可以扩展它来支持其他数据集的显示。
规划:
- 处理 mnist 数据集
- 显示单张图像和label
- 图像文件名列表的显示
- 点选列表item后切换显示的图像和label
1. 处理 mnist 数据集
from mnist import MNIST
import matplotlib.pyplot as plt
import numpy as np # 导入numpy
import os
def save_images_and_labels(images, labels, directory, label_filename):
"""
将图像和标签保存到指定目录。
"""
if not os.path.exists(directory):
os.makedirs(directory)
label_file_path = os.path.join(directory, label_filename)
with open(label_file_path, 'w') as label_file:
for i, (image, label) in enumerate(zip(images, labels), start=1):
image_filename = f"{i}.png"
image_path = os.path.join(directory, image_filename)
# 将图像数据转换为NumPy数组并重塑
image_array = np.array(image, dtype=np.uint8).reshape(28, 28)
# 保存图像
plt.imsave(image_path, image_array, cmap='gray')
# 写入标签
label_file.write(f"{image_filename} {label}\n")
def main():
# 加载MNIST数据集
mndata = MNIST('mnist_data')
train_images, train_labels = mndata.load_training()
test_images, test_labels = mndata.load_testing()
# 保存训练数据集
save_images_and_labels(train_images, train_labels, 'train', 'train-label.txt')
# 保存测试数据集
save_images_and_labels(test_images, test_labels, 'test', 'test-label.txt')
if __name__ == "__main__":
main()
目录结构:
train
|-- 1.png
|-- 2.png
...
|-- train-label.txt
2. 显示单张图像和label
2.1 显示单张图像
简单起见, 直接使用 imgui-SFML 进行窗口创建, 方便后续直接添加 imgui 的 GUI widget. CMakeLists.txt 的写法,查看之前的博客内容。
加载了 train/1.png 图像文件到 sf::Texture
对象,通过 sf::Sprite
装载。 sprite 的好处是可以设定位置,还可以缩放。 我放大了20倍。关键代码:
sf::Texture texture;
if (!texture.loadFromFile("../train/1.png"))
{
std::cerr << "Error loading image" << std::endl;
return -1;
}
sf::Sprite sprite;
sprite.setTexture(texture);
sprite.setScale(20, 20);
sprite.setPosition(20, 20);
完整代码:
#include "imgui.h"
#include "imgui-SFML.h"
#include <SFML/Graphics.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Event.hpp>
#include <iostream>
int main()
{
constexpr int win_width = 960;
constexpr int win_height = 640;
sf::RenderWindow window(sf::VideoMode(win_width, win_height), "mnist viewer");
window.setFramerateLimit(60);
bool ret = ImGui::SFML::Init(window); // [imgui-SFML]
if (!ret)
return -1;
sf::CircleShape shape(100.f);
shape.setFillColor(sf::Color::Green);
sf::Texture texture;
if (!texture.loadFromFile("../train/1.png"))
{
std::cerr << "Error loading image" << std::endl;
return -1;
}
sf::Sprite sprite;
sprite.setTexture(texture);
sprite.setScale(10, 10); // 水平和垂直方向都放大20倍
sprite.setPosition(20, 20);
sf::Clock deltaClock;
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
ImGui::SFML::ProcessEvent(window, event); // [imgui-SFML]
if (event.type == sf::Event::Closed)
{
window.close();
}
}
ImGui::SFML::Update(window, deltaClock.restart()); // [imgui-SFML]
window.clear();
window.draw(sprite);
ImGui::SFML::Render(window); // [imgui-SFML]
window.display();
}
ImGui::SFML::Shutdown(); // [imgui-SFML]
return 0;
}
2.2 点选列表后更新显示的图像
sprite 对象就是一个容器, 或者说带有位置的放大镜, 你随时可以换一个新的 texture 丢给它。 现在我们通过 imgui 显示一个 list, 任意个一个 list item 被选中的时候,就加载对应的图像文件,获取 texture 后装到 sprite 里头,界面上就看到更新的图像了:
关键代码:
// 使用ImGui创建一个简单的列表
ImGui::Begin("Image List");
// 假设文件名为1.png到10.png
for (int i = 1; i <= 10; ++i) {
std::string fileName = std::to_string(i) + ".png";
// 如果列表项被点击
if (ImGui::Selectable(fileName.c_str(), currentFile == fileName)) {
currentFile = fileName; // 更新当前选中的文件名
// 加载对应的纹理
if (!texture.loadFromFile(currentFile)) {
std::cerr << "Failed to load " << currentFile << std::endl;
} else {
sprite.setTexture(texture);
}
}
}
ImGui::End();
2.3 显示 label
train-label.txt 每一行是 <image_name> <image_label>
的形式, 例如:
1.png 0
使用哈希表存储这一映射关系, 然后每当通过GUI交互选择了新的文件名字时, 从字典里查询出对应的 label。
在 imgui 里显示当前图像的 label 很简单, 只需要添加 ImGui::Text, 并用 ImGui::Begin() 和 End() 包裹即可:
int currentLabel = label_map[currentFile];
ImGui::Begin("Label Info");
ImGui::Text("Current Image: %s", currentFile.c_str());
ImGui::Text("Label: %d", currentLabel);
ImGui::End();
增大显示的字体
label 是我们关注的重要信息, 要用大字体显示 label, 需要先加载字体,并执行build和刷新, 否则程序运行阶段会遇到 imgui 的检查失败。
关键代码:
// 加载字体
const std::string asset_dir = "../../games/Resources";
const std::string font_path = asset_dir + "/Arial.ttf";
// 在这里添加字体
ImFont* bigFont = ImGui::GetIO().Fonts->AddFontFromFileTTF(font_path.c_str(), 24.0f);
if (!bigFont) {
std::cerr << "Failed to load font." << std::endl;
}
// 构建字体图集
ImGui::GetIO().Fonts->Build();
// 更新SFML中的字体纹理
ret = ImGui::SFML::UpdateFontTexture();
if (!ret)
return -2;
while()
{
while()
{
// 显示当前图像的标签
if (!currentFile.empty()) {
int currentLabel = label_map[currentFile];
ImGui::Begin("Label Info", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::PushFont(bigFont); // 使用更大的字体
ImGui::Text("Label: %d", currentLabel);
ImGui::PopFont(); // 恢复默认字体
ImGui::End();
}
}
}
如果不这样设定, 会遇到这些报错:
Assertion failed: (g.IO.Fonts->IsBuilt() && "Font Atlas not built! Make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()"), function ErrorCheckNewFrameSanityChecks, file imgui.cpp, line 9579.
Assertion failed: (io.Fonts->TexID != (ImTextureID) nullptr), function RenderDrawLists, file imgui-SFML.cpp, line 876.
效果:
2.4 使用完整的列表
读取文件名字的时候,原来是硬编码10张图,现在改为从 unordered_map 里读取。 注意, 由于这个数据量不大, 因此没有另外开启线程。
for (int i = 1; i <= 10; ++i) {
std::string fileName = std::to_string(i) + ".png";
...
}
for (const auto& kv : label_map)
{
const std::string fileName = kv.first;
...
}
最终效果:
总结
通过使用 SFML, 加载并显示了了图像(texture->sprite->window)。 通过使用 imgui, 显示了图像文件列表、 label, 并且列表被选中元素和图像、 label 是联动的。
在设置 label 字体的时候, 需要额外调用 imgui 的构建字体和 imgui-SFML 的刷新, 来避免运行时候的检查报错。
是一个很简陋的图像数据集查看工具,可以进一步完善。
源码放在: https://github.com/zchrissirhcz/imgui-sfml-examples/tree/main/mnist-viewer