基于MNN的Android端MNIST模型部署实战:从环境配置到推理优化
1. 环境准备从零搭建你的Android MNN开发环境很多朋友一听到在Android上部署AI模型第一反应就是“头大”觉得要配置一堆环境编译各种库光是想想就劝退了。其实吧真没你想的那么复杂。我刚开始接触MNN的时候也这么觉得但跟着步骤走一遍你会发现就像搭积木一样一块一块拼起来就成了。咱们今天就用最经典的MNIST手写数字识别模型来开刀目标很明确在Android手机上跑起来一个能识别数字的App。我会把每一步都掰开揉碎了讲保证你跟着做一遍就能成功。首先你得把“工地”准备好。核心就三样东西Android Studio、MNN框架库和OpenCV。Android Studio是咱们的“总指挥部”所有代码编写、编译、调试都在这里完成。我建议你直接去官网下载最新稳定版安装过程一路“Next”就行记得勾选“Android Virtual Device”以便后面用模拟器测试。安装好后第一次启动可能会下载一些SDK组件喝杯咖啡等它完成就好。接下来是重头戏MNN。这是阿里开源的一个超轻量级深度学习推理引擎专门为移动端优化过速度快、体积小特别适合我们这种在手机端跑模型的场景。你不能直接用它的源码得先把它编译成Android能用的动态库也就是.so文件。别怕编译我教你个最稳的方法。打开你的终端Linux或MacWindows可以用WSL或者PowerShell按照MNN官方GitHub仓库的README操作。最关键的一步是编译脚本的配置。你需要写一个build_android.sh脚本内容大致如下#!/bin/bash # 设置Android NDK的路径这个路径你得换成自己电脑上的 export ANDROID_NDK/path/to/your/ndk # 使用CMake进行配置这里我们编译arm64-v8a架构的因为现在大部分手机都是64位的 cmake .. \ -DCMAKE_TOOLCHAIN_FILE$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DCMAKE_BUILD_TYPERelease \ -DANDROID_ABIarm64-v8a \ -DANDROID_STLc_shared \ -DMNN_BUILD_CONVERTEROFF \ -DMNN_BUILD_DEMOOFF \ -DMNN_BUILD_TRAINOFF \ -DMNN_BUILD_TRAIN_MINIOFF \ -DMNN_BUILD_BENCHMARKOFF \ -DMNN_OPENCLOFF \ -DMNN_VULKANOFF \ -DMNN_OPENMPOFF \ -DMNN_USE_THREAD_POOLON # 开始编译-j8表示用8个线程并行编译速度更快 make -j8执行这个脚本后如果一切顺利你会在source目录下找到编译好的libMNN.so和libMNN_Express.so等库文件。把它们当成宝贝收好待会儿要放进Android工程里。最后是OpenCV它主要负责图像处理比如我们待会儿要把手机拍的照片调整成模型需要的28x28大小。同样你需要下载OpenCV的Android SDK包。解压后里面会有一个sdk/native目录这个路径我们等下在Android Studio里会用到。环境准备的最后一步是创建一个新的Android项目。打开Android Studio选择“Create New Project”然后选“Native C”模板这样它会自动帮我们配置好JNIJava Native Interface的基本环境省去了很多手动配置的麻烦。项目创建好后先别急着写代码咱们得把刚才准备好的“建材”——MNN和OpenCV的库文件搬到工程里来。2. 工程配置让CMake找到你的“左膀右臂”工程建好了就像毛坯房接下来得通水通电把MNN和OpenCV这两个核心工具引入进来。这里的关键在于编写CMakeLists.txt文件你可以把它理解为整个C部分的“建筑图纸”告诉编译器去哪里找头文件、链接哪个库。很多新手都在这里栽跟头不是库找不到就是链接失败我当初也是折腾了好几个小时。别担心我把我调试成功的配置直接给你你对照着改改路径就行。在你的Android项目里CMakeLists.txt通常放在app/src/main/cpp/目录下。我们用文本编辑器打开它把里面的内容替换成下面这样cmake_minimum_required(VERSION 3.4.1) # 1. 设置OpenCV的路径 # 这里需要替换成你电脑上OpenCV Android SDK解压后的实际路径 set(OpenCV_DIR /Users/YourName/Downloads/OpenCV-android-sdk/sdk/native/jni) find_package(OpenCV REQUIRED) # 2. 设置MNN的路径 # 这里替换成你下载的MNN源码的根目录路径 set(MNN_DIR /Users/YourName/Projects/MNN) # 3. 包含MNN的头文件 # 必须把这些目录都包含进来否则编译时会报“头文件找不到”的错误 include_directories(${MNN_DIR}/include) include_directories(${MNN_DIR}/include/MNN) include_directories(${MNN_DIR}/source) include_directories(${MNN_DIR}/source/core) include_directories(${MNN_DIR}/source/backend) # 4. 添加我们自己写的C源文件 # 假设我们写了一个叫 mnist_mnn.cpp 的文件 add_library( native-lib SHARED mnist_mnn.cpp) # 5. 导入预编译好的MNN动态库 # 首先你需要把编译好的 libMNN.so 文件复制到项目的 app/libs/arm64-v8a/ 目录下 add_library( MNN SHARED IMPORTED ) set_target_properties( MNN PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../libs/${ANDROID_ABI}/libMNN.so ) # 6. 查找Android NDK自带的日志库 find_library( log-lib log ) # 7. 链接所有库 # 这里把我们的 native-lib、MNN、OpenCV和日志库都链接在一起 target_link_libraries( native-lib MNN jnigraphics ${OpenCV_LIBS} ${log-lib})这个配置文件干了这么几件事首先它通过find_package找到了OpenCV然后通过include_directories把MNN的一大堆头文件路径告诉了编译器接着它创建了我们自己的native-lib共享库最关键的一步是add_library( MNN SHARED IMPORTED )它声明了我们要使用一个外部编译好的libMNN.so文件并且指定了它的位置最后用target_link_libraries把大家绑在一起。光有CMakeLists.txt还不够我们还得告诉Android Studio的Gradle构建系统去哪里找我们放好的.so库文件。打开app目录下的build.gradle文件在android块里找到defaultConfig加上下面这几行android { ... defaultConfig { ... externalNativeBuild { cmake { cppFlags -stdc14 # 使用C14标准 arguments -DANDROID_STLc_shared # 使用共享的C运行时库 abiFilters arm64-v8a # 只编译64位版本减小APK体积 } } // 这行非常重要它告诉Gradle我们的.so库在libs目录下 sourceSets { main { jniLibs.srcDirs [libs] } } } ... }配置到这里你的工程就应该能成功编译了。你可以先点一下Android Studio那个绿色的“运行”按钮看看项目能不能在一个空的模拟器或真机上安装并启动。如果这一步成功了恭喜你最磨人但最重要的基础搭建工作已经完成了接下来我们就可以开始写真正做事的C推理代码了。3. 模型转换把你的PyTorch/TensorFlow模型变成MNN格式模型部署的第一步往往不是写代码而是“翻译”模型。你在PC上训练好的模型无论是PyTorch的.pth还是TensorFlow的.pb手机都不认识。我们需要用MNN提供的转换工具把它们变成MNN自家认识的.mnn格式。这个过程听起来很技术但其实就像把Word文档转成PDF一样是个标准化流程。MNN的转换工具做得挺傻瓜化的我以最常用的ONNX模型为例因为PyTorch和TensorFlow都能很方便地导出ONNX带你走一遍。首先确保你已经按照第一步的方法编译了MNN在编译输出的目录里应该能找到MNNConvert这个可执行文件。假设你有一个训练好的MNIST模型并已经把它导出成了mnist.onnx文件。我们打开终端切换到转换工具所在的目录执行下面这条命令./MNNConvert -f ONNX --modelFile mnist.onnx --MNNModel mnist.mnn --bizCode MNN简单解释一下这几个参数-f ONNX指定输入模型格式是ONNX--modelFile后面跟你的输入模型路径--MNNModel指定输出的.mnn文件名字--bizCode可以随便起个名标识这个模型的业务。执行成功后当前目录下就会生成一个mnist.mnn文件这个就是我们Android端需要的模型文件了。但是这里有个大坑我踩过必须提醒你模型输入输出的名字。转换命令默认会使用模型里的节点名但有时候这些名字很古怪或者你根本不知道是啥。为了在C代码里正确获取输入输出Tensor你最好在转换前或者转换后搞清楚你的模型输入输出叫什么。一个更稳妥的办法是在转换时使用MNNConvert的--info参数先看看模型结构./MNNConvert -f ONNX --modelFile mnist.onnx --info这个命令不会真的转换而是打印出模型的详细信息包括输入输出的名字、维度等。比如你可能会看到输出类似Inputs: input.1, shape: [1, 1, 28, 28]Outputs: output, shape: [1, 10]。记下这个input.1和output它们就是你在C代码里需要用到的tensor name。把生成的mnist.mnn文件放到Android项目的app/src/main/assets/目录下。这样当App打包时这个模型文件就会被封装进APK里我们可以通过AssetManager在运行时把它读取出来拷贝到手机的存储空间再加载。这一步做完模型就准备好了它静静地躺在你的Assets文件夹里等着被C代码调用。4. C推理核心用MNN API跑通前向传播环境搭好了模型也转换好了现在终于到了最核心的环节写C代码调用MNN的接口把一张图片喂给模型并得到识别结果。这部分代码会通过JNI被上层的Java调用是整个App的“发动机”。我把它拆解成几个关键函数你一看就懂。首先我们在cpp目录下创建一个新文件比如叫mnist_mnn.cpp然后开始引入必要的头文件#include jni.h #include android/bitmap.h #include opencv2/opencv.hpp #include MNN/Interpreter.hpp #include MNN/Tensor.hpp #include MNN/ImageProcess.hpp #include string #include android/log.h #define LOG_TAG MNN_MNIST #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) using namespace MNN; using namespace cv;头文件包含了JNI、Android位图操作、OpenCV图像处理以及MNN的核心类。我们还定义了几个日志宏方便在Android的Logcat里输出调试信息这在排查问题时非常有用。接下来我们写一个核心的推理函数。这个函数接收一个OpenCV的Mat对象代表图像数据和一个模型文件的路径返回一个识别出的整数0-9。// 核心推理函数 int run_mnist(const cv::Mat input_image, const std::string model_path) { // 1. 创建解释器从文件加载模型 std::shared_ptrInterpreter interpreter(Interpreter::createFromFile(model_path.c_str())); if (interpreter nullptr) { LOGE(Failed to load model from: %s, model_path.c_str()); return -1; } // 2. 配置调度使用CPU后端单线程MNIST很简单单线程足够 ScheduleConfig config; config.type MNN_FORWARD_CPU; config.numThread 1; // 3. 创建会话Session这是模型在运行时的上下文 auto session interpreter-createSession(config); interpreter-releaseModel(); // 模型数据已加载到会话中可以释放原始模型数据以节省内存 // 4. 获取输入和输出Tensor auto input_tensor interpreter-getSessionInput(session, nullptr); // 如果模型只有一个输入可以用nullptr auto output_tensor interpreter-getSessionOutput(session, nullptr); // 同上 // 5. 图像预处理调整大小、归一化、转换颜色通道 cv::Mat processed; cv::resize(input_image, processed, cv::Size(28, 28)); // MNIST要求28x28 processed.convertTo(processed, CV_32FC3, 1.0 / 255.0); // 归一化到[0,1] // 6. 将OpenCV Mat数据拷贝到MNN Tensor // 注意MNN Tensor的默认布局是NCHW而OpenCV Mat是HWC需要转换 std::vectorint dims{1, processed.rows, processed.cols, 3}; // NHWC auto nhwc_tensor Tensor::createfloat(dims, nullptr, Tensor::TENSORFLOW); ::memcpy(nhwc_tensor-hostfloat(), processed.data, nhwc_tensor-size()); input_tensor-copyFromHostTensor(nhwc_tensor); // 这个函数会处理布局转换 // 7. 执行推理 interpreter-runSession(session); // 8. 获取输出结果 std::shared_ptrTensor output_host(new Tensor(output_tensor, output_tensor-getDimensionType())); output_tensor-copyToHostTensor(output_host.get()); auto output_data output_host-hostfloat(); // 9. 后处理找到概率最大的那个数字 int predicted_digit 0; float max_score output_data[0]; for (int i 1; i 10; i) { if (output_data[i] max_score) { max_score output_data[i]; predicted_digit i; } } LOGI(推理完成预测数字为: %d, 得分: %f, predicted_digit, max_score); return predicted_digit; }这段代码是MNN推理的标准流程几乎可以套用到任何分类模型上。我加了详细的注释你重点理解几个关键点一是ScheduleConfig它决定了模型在什么硬件上跑CPU/GPU以及用多少线程二是copyFromHostTensor这个函数它非常智能能自动帮你处理不同数据布局比如NHWC转NCHW的转换省去了我们手动转置的麻烦三是后处理对于MNIST十分类输出是一个10维的向量我们取最大值索引就是预测结果。写好这个核心函数后我们还需要一个JNI函数作为桥梁让Java层能够调用它。这个函数负责把Java传过来的Android Bitmap转换成OpenCV Mat然后调用上面的推理函数。5. JNI桥梁打通Java与C的任督二脉JNIJava Native Interface是Java和C之间的“翻译官”。在Android里我们的用户界面和逻辑是用Java/Kotlin写的但高性能的模型推理是用C写的JNI就是连接这两层的桥梁。很多开发者觉得JNI很复杂其实对于我们的需求只需要写好一个函数就够了。我们在mnist_mnn.cpp文件末尾加上这个JNI函数extern C JNIEXPORT jint JNICALL Java_com_example_mnistapp_MainActivity_predictFromBitmap( JNIEnv* env, jobject /* this */, jobject bitmap, jstring modelPathStr) { // 1. 将Java的String转换成C的std::string const char *model_path env-GetStringUTFChars(modelPathStr, nullptr); std::string model_path_cpp(model_path); env-ReleaseStringUTFChars(modelPathStr, model_path); // 2. 将Android Bitmap转换成OpenCV Mat AndroidBitmapInfo bitmap_info; void* pixels; // 锁定Bitmap像素获取信息 if (AndroidBitmap_getInfo(env, bitmap, bitmap_info) 0) { LOGE(AndroidBitmap_getInfo failed!); return -1; } if (AndroidBitmap_lockPixels(env, bitmap, pixels) 0) { LOGE(AndroidBitmap_lockPixels failed!); return -1; } cv::Mat mat; // 根据Bitmap的格式通常是RGBA_8888创建对应的Mat if (bitmap_info.format ANDROID_BITMAP_FORMAT_RGBA_8888) { mat cv::Mat(bitmap_info.height, bitmap_info.width, CV_8UC4, pixels); cv::cvtColor(mat, mat, cv::COLOR_RGBA2RGB); // MNN模型通常需要RGB输入 } else if (bitmap_info.format ANDROID_BITMAP_FORMAT_RGB_565) { mat cv::Mat(bitmap_info.height, bitmap_info.width, CV_8UC2, pixels); cv::cvtColor(mat, mat, cv::COLOR_BGR5652RGB); } else { LOGE(Unsupported bitmap format!); AndroidBitmap_unlockPixels(env, bitmap); return -1; } // 3. 调用核心推理函数 int result run_mnist(mat, model_path_cpp); // 4. 解锁Bitmap像素 AndroidBitmap_unlockPixels(env, bitmap); return result; }这个函数的名字Java_com_example_mnistapp_MainActivity_predictFromBitmap是有讲究的它遵循JNI的命名规则Java_包名_类名_方法名。你需要把com_example_mnistapp替换成你自己项目的包名。函数接收两个参数一个jobject代表Android的Bitmap对象一个jstring代表模型在手机上的绝对路径。函数内部先用AndroidBitmap_getInfo和AndroidBitmap_lockPixels这两个Android NDK提供的函数安全地获取Bitmap的像素数据并转换成OpenCV的Mat对象。这里要注意颜色空间的转换手机摄像头或图片通常是RGBA或RGB565格式而模型一般需要RGB格式所以用cv::cvtColor做一下转换。转换完成后调用我们写好的run_mnist函数得到识别结果最后返回给Java层。写完C部分记得在Java层对应的MainActivity里声明这个本地方法public class MainActivity extends AppCompatActivity { // 加载我们编译好的动态库 static { System.loadLibrary(native-lib); } // 声明本地方法和C里的函数名对应 public native int predictFromBitmap(Bitmap bitmap, String modelPath); // ... 其他代码 }这样从Java到C的通道就完全打通了。你在Java里拿到一张图片的Bitmap再准备好模型文件的路径调用predictFromBitmap这个方法就能在C层完成推理并返回结果。整个过程虽然涉及两层语言但逻辑非常清晰。6. Android界面与调用构建一个简单的演示AppC引擎准备好了现在我们需要一个简单的Android界面来驱动它。这个界面不需要多华丽能展示一张图片、点个按钮开始识别、再显示结果就行。我们沿用Native C项目模板自动生成的MainActivity和布局稍作修改。首先修改res/layout/activity_main.xml布局文件我们放一个ImageView用来显示图片一个TextView显示识别结果再加两个Button?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp TextView android:layout_widthwrap_content android:layout_heightwrap_content android:textMNIST手写数字识别演示 android:textSize20sp android:layout_gravitycenter_horizontal android:layout_marginBottom20dp/ ImageView android:idid/imageView android:layout_width300dp android:layout_height300dp android:layout_gravitycenter_horizontal android:scaleTypefitCenter android:backgroundandroid:color/darker_gray/ LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:layout_marginTop20dp Button android:idid/btn_load android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:layout_marginEnd8dp android:text加载图片 / Button android:idid/btn_predict android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:text开始识别 / /LinearLayout TextView android:idid/tv_result android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_horizontal android:layout_marginTop20dp android:text结果 android:textSize24sp android:textColor#FF5722/ /LinearLayout然后在MainActivity.java中我们要实现按钮点击逻辑核心是完成两件事一是把放在assets文件夹里的mnist.mnn模型文件拷贝到手机的内部存储这样才能被C代码读取二是把测试图片比如一张手写数字的PNG图加载成Bitmap并调用JNI函数。public class MainActivity extends AppCompatActivity { private ImageView mImageView; private TextView mResultView; private Bitmap mTestBitmap; private String mModelPath; static { System.loadLibrary(native-lib); } public native int predictFromBitmap(Bitmap bitmap, String modelPath); Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mImageView findViewById(R.id.imageView); mResultView findViewById(R.id.tv_result); findViewById(R.id.btn_load).setOnClickListener(v - loadImage()); findViewById(R.id.btn_predict).setOnClickListener(v - runPrediction()); // 初始化拷贝模型文件 copyModelToCache(); } private void copyModelToCache() { // 将assets中的模型文件拷贝到应用缓存目录 try { File cacheFile new File(getCacheDir(), mnist.mnn); if (!cacheFile.exists()) { InputStream is getAssets().open(mnist.mnn); FileOutputStream fos new FileOutputStream(cacheFile); byte[] buffer new byte[1024]; int length; while ((length is.read(buffer)) 0) { fos.write(buffer, 0, length); } fos.flush(); fos.close(); is.close(); Log.i(MainActivity, 模型文件拷贝成功: cacheFile.getAbsolutePath()); } mModelPath cacheFile.getAbsolutePath(); } catch (IOException e) { Log.e(MainActivity, 拷贝模型文件失败, e); Toast.makeText(this, 模型加载失败, Toast.LENGTH_SHORT).show(); } } private void loadImage() { // 从drawable资源加载一张测试图片 mTestBitmap BitmapFactory.decodeResource(getResources(), R.drawable.test_digit_7); mImageView.setImageBitmap(mTestBitmap); mResultView.setText(结果--); } private void runPrediction() { if (mTestBitmap null || mModelPath null) { Toast.makeText(this, 请先加载图片和模型, Toast.LENGTH_SHORT).show(); return; } // 在子线程中执行推理避免阻塞UI new Thread(() - { final int result predictFromBitmap(mTestBitmap, mModelPath); // 回到主线程更新UI runOnUiThread(() - mResultView.setText(结果 result)); }).start(); } }这段Java代码做了几件关键事情在onCreate里我们调用copyModelToCache把打包在APK里的模型文件解压到手机的缓存目录并获取其绝对路径这个路径会传给C代码。loadImage方法从应用的res/drawable目录加载一张预设的、写有数字“7”的图片你需要自己准备一张28x28左右的黑底白字图片命名为test_digit_7.png并放入drawable目录。runPrediction方法是最重要的它在一个新线程里调用我们声明的本地方法predictFromBitmap并将结果更新到UI上。记住推理操作一定要放在子线程因为模型推理是计算密集型任务如果在主线程UI线程执行会导致界面卡死无响应Android系统甚至会强制关闭你的App。7. 性能优化与调试技巧让你的App更快更稳代码跑通只是第一步要让App真正可用我们还得关注性能和稳定性。在移动端资源非常宝贵优化做得好用户体验天差地别。我这里分享几个我实战中总结出来的针对MNN在Android端部署的优化技巧。第一招选择合适的计算后端和线程数。在创建ScheduleConfig时我们用的是MNN_FORWARD_CPU。对于MNIST这种超小模型CPU单线程完全足够甚至是最快的因为启动GPUOpenCL/Vulkan会有额外的开销。但对于更大的模型你可以尝试MNN_FORWARD_OPENCL或MNN_FORWARD_VULKAN来利用手机的GPU速度可能会有数倍提升。线程数numThread也不是越多越好一般设置为手机CPU的大核数比较合适比如4或8你可以做个简单的基准测试在App启动时尝试不同配置把推理时间打印到Logcat里对比。第二招预热Warm Up和会话复用。模型第一次推理通常比较慢因为涉及到内存分配、算子初始化等操作。我们可以在App启动后、用户使用前先跑一次虚拟数据比如全零的Tensor进行“预热”。更高级的做法是复用Session。在我们的代码里每次调用predictFromBitmap都会创建新的Interpreter和Session然后销毁这很浪费。理想的做法是在一个全局管理器里初始化并持有一个Session每次推理都复用它。但要注意线程安全如果多个线程同时调用需要加锁或者创建多个Session。第三招输入Tensor的内存复用。在run_mnist函数里我们每次推理都创建了一个新的nhwc_tensor。对于连续推理的场景比如摄像头实时识别我们可以把这个Tensor创建一次然后每次只更新它的数据部分避免反复申请释放内存。第四招利用MNN的Express模块可选。如果你的模型比较简单或者你想用更简洁的API可以尝试MNN的Express模块。它提供了类似PyTorch的声明式编程接口代码写起来更直观。但Express模块会增加一些库体积对于MNIST这种小项目可能没必要但对于复杂模型预处理后处理可能会更方便。调试方面最强大的工具就是Android Studio的Logcat。我们在C代码里用__android_log_print输出的日志都会在这里显示。多打日志尤其是在关键步骤比如模型加载成功与否、预处理后的图像尺寸、推理耗时。你可以用clock()函数或者C11的chrono库来精确测量推理时间#include chrono auto start std::chrono::high_resolution_clock::now(); interpreter-runSession(session); auto end std::chrono::high_resolution_clock::now(); std::chrono::durationdouble elapsed end - start; LOGI(推理耗时: %f 秒, elapsed.count());把优化后的代码集成进去再次运行你的App。点击“加载图片”再点“开始识别”你应该能在TextView里立刻看到预测出的数字并且在Logcat里看到类似“推理耗时: 0.015秒”的日志。这意味着从你按下按钮到看到结果只用了十几毫秒完全达到了实时性的要求。至此一个完整的、优化过的Android端MNIST模型部署项目就全部完成了。从环境搭建、模型转换、C核心开发、JNI桥接到最后的界面集成和优化我们一步步走通了整个流程。虽然是以MNIST为例但整个框架和思路是通用的你可以很方便地替换成自己的人脸识别、物体检测等模型快速在移动端实现AI能力。

相关新闻

YOLO实战指南:从零开始使用LabelImg构建自定义数据集

YOLO实战指南:从零开始使用LabelImg构建自定义数据集

1. 为什么你需要自己的YOLO数据集? 如果你已经对YOLO(You Only Look Once)这个目标检测模型有所耳闻,甚至跟着教程跑通了官方的COCO或者VOC数据集,那你肯定已经体验过它的强大——识别几十上百种常见物体,又…

2026/7/5 0:25:32 阅读更多 →
无需公网IP,用Windows IIS与内网穿透打造个人WebDAV云盘

无需公网IP,用Windows IIS与内网穿透打造个人WebDAV云盘

1. 为什么你需要一个自己的WebDAV云盘? 你是不是也遇到过这样的烦恼?手机拍的照片、视频越来越多,128G、256G的存储空间动不动就告急,每次都得手动导到电脑硬盘里,时间一长,文件散落在各处,找起…

2026/7/3 6:07:58 阅读更多 →
三菱A800变频器A8NC板卡与CC-Link网络集成指南

三菱A800变频器A8NC板卡与CC-Link网络集成指南

1. 从零开始:认识你的A800变频器与A8NC板卡 如果你刚拿到一台三菱A800变频器和那块看起来有点神秘的A8NC板卡,心里可能有点打鼓:这玩意儿到底能干啥?简单来说,A800变频器是驱动电机的“大脑”,负责控制电机…

2026/7/4 10:39:27 阅读更多 →

最新新闻

海光K100_AI单卡全离线部署PPT生成系统

海光K100_AI单卡全离线部署PPT生成系统

一、引言随着人工智能技术迅猛发展,大语言模型与多模态生成技术的深度融合正在重塑各行各业的创作范式。其中,智能演示文稿(PPT)生成作为AI办公自动化的重要方向,正经历从“模板填充”到“智能体自主创作”的根本性变革…

2026/7/5 7:06:01 阅读更多 →
收放板机如何应对特殊板件——从超薄板到厚铜板的取放策略

收放板机如何应对特殊板件——从超薄板到厚铜板的取放策略

背景PCB制造中,收放板机面对的板件规格跨度极大。内层芯板薄至0.05mm,刚性极低,拿在手里都感觉会折;外层厚铜板可达8.0mm,重量大,对夹持力有较高要求。同一台设备要在不同规格之间稳定取放,靠的…

2026/7/5 7:06:01 阅读更多 →
2026年实践,合韵汤泉与海鲜自助结合后表现如何?

2026年实践,合韵汤泉与海鲜自助结合后表现如何?

2026年,合韵汤泉与海鲜自助结合后的表现非常出色。作为国内首家海洋主题微度假汤泉生活馆,北京合韵汤泉通过引入海鲜自助等高端餐饮服务,不仅提升了顾客的整体体验,还显著增加了其市场竞争力。表现亮点提升综合体验:海…

2026/7/5 7:04:00 阅读更多 →
Python社交网络分析:从脏数据清洗到图构建的七道硬核工序

Python社交网络分析:从脏数据清洗到图构建的七道硬核工序

1. 这不是“画个关系图”就完事的——为什么用Python做社交网络分析,90%的人连数据清洗这关都过不去“Social Network Analysis in Python”这个标题听起来很学术、很技术,但如果你真把它当成一门“学几个networkx函数就能发论文”的速成课,那…

2026/7/5 7:02:00 阅读更多 →
5分钟快速上手:Parsec VDD虚拟显示器完全指南

5分钟快速上手:Parsec VDD虚拟显示器完全指南

5分钟快速上手:Parsec VDD虚拟显示器完全指南 【免费下载链接】parsec-vdd ✨ Perfect virtual display for game streaming 项目地址: https://gitcode.com/gh_mirrors/pa/parsec-vdd 你是否曾经因为缺少物理显示器而无法充分利用远程服务器?或者…

2026/7/5 6:59:59 阅读更多 →
基于WebGPU与WASM的本地AI图像修复与超分工具Inpaint-Web部署与实战

基于WebGPU与WASM的本地AI图像修复与超分工具Inpaint-Web部署与实战

🚀 30款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度 在实际图像处理工作中,我们经常遇到两类棘手问题:一是从网络获取的图片分辨率过低,放大后细节模糊…

2026/7/5 6:57:59 阅读更多 →

日新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻