news 2026/9/16 10:11:56

GenieX Android SDK 四层 JNI 桥接架构解析:端侧 LLM 与 VLM 的完整调用链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GenieX Android SDK 四层 JNI 桥接架构解析:端侧 LLM 与 VLM 的完整调用链路

GenieX Android SDK 四层 JNI 桥接架构解析:端侧 LLM 与 VLM 的完整调用链路

【免费下载链接】GenieXRun frontier LLMs and VLMs locally on Qualcomm devices across NPU, GPU, and CPU with a few lines of code项目地址: https://gitcode.com/GitHub_Trending/ne/GenieX

本文以 bindings/android/README.md 为主线,结合 GenieX 仓库中 Kotlin、C++ 与核心库源码,系统拆解 Android 端 SDK 的“四层 JNI 桥接”架构:从面向协程的高层 Kotlin API,到声明external方法的 JNI 接口、实现类型转换与回调桥接的 C++ 层,再到最终执行推理的核心libgeniex.so。读者将掌握 LLM/VLM 在 Android 端的创建、流式生成、停止、聊天模板等完整调用链路,理解插件注册与原生库加载机制,并学会如何构建与发布 Android AAR。

一、整体架构:四层 JNI 桥接模式

GenieX 的 Android 绑定通过JNI(Java Native Interface)桥接将 Kotlin 上层 API 与 C 语言核心推理库连接起来,整体划分为四个职责清晰的层级:

┌─────────────────────────────────────────────────────────┐ │ Layer 4: Public API (Kotlin) │ │ ModelWrapper.kt - Coroutine-based high-level API │ │ (LlmWrapper, VlmWrapper) │ └────────────────┬────────────────────────────────────────┘ │ calls ┌────────────────▼────────────────────────────────────────┐ │ Layer 3: JNI Interface (Kotlin) │ │ Model.kt - Declares external native methods │ │ (Llm, Vlm) │ └────────────────┬────────────────────────────────────────┘ │ JNI boundary ┌────────────────▼────────────────────────────────────────┐ │ Layer 2: JNI Bridge (C++) │ │ model_bridge_jni.cpp - Implements JNI methods │ │ Java_com_geniex_sdk_jni_Model_* functions │ └────────────────┬────────────────────────────────────────┘ │ calls C API ┌────────────────▼────────────────────────────────────────┐ │ Layer 1: Core Library (C) │ │ libgeniex.so - Provides ml_* API from ml.h │ │ (ml_llm_create, ml_llm_generate, etc.) │ └─────────────────────────────────────────────────────────┘

四层的职责划分非常清晰:Kotlin 层只面向业务、C 层只面向推理,中间由 JNI 边界完成语言屏障的跨越。每一层都可以独立演进——更换底层推理引擎不影响上层 API,调整 Kotlin API 形态也无须改动 C 核心。

需要说明的是,原文档 Layer 1 中标注的ml.h/ml_*命名属于早期设计约定。从当前仓库源码看,核心 C API 的实际头文件位于 sdk/include/geniex.h,对外暴露的是geniex_LLMgeniex_llm_create()geniex_llm_generate()geniex_*前缀函数(详见下文“Layer 1”一节),架构分层思想本身保持不变。

二、Layer 4:面向协程的高层 Kotlin API

最高层是业务开发者直接接触的 Kotlin API,位于bindings/android/app/src/main/java/com/geniex/sdk/下,代表是 LlmWrapper.kt 与 VlmWrapper.kt。

2.1 构建器模式:Builder + suspend build()

两个 Wrapper 都采用Builder 模式 + 协程的设计:Builder负责收集输入参数,build()是挂起函数,在指定调度器上完成原生句柄的创建。

// LlmWrapper 构建入口 class LlmWrapper private constructor( private val dispatcher: CoroutineDispatcher ) : Closeable { private val llm = Llm() private var handle: Long = 0 companion object { @JvmStatic fun builder() = Builder() } class Builder { private var llmCreateInput: LlmCreateInput? = null private var dispatcher: CoroutineDispatcher = Dispatchers.IO fun llmCreateInput(llmCreateInput: LlmCreateInput) = apply { this.llmCreateInput = llmCreateInput } fun dispatcher(dispatcher: CoroutineDispatcher) = apply { this.dispatcher = dispatcher } suspend fun build(): Result<LlmWrapper> = withContext(dispatcher) { try { val input = llmCreateInput ?: throw IllegalArgumentException("modelPath required") val wrapper = LlmWrapper(dispatcher) wrapper.handle = wrapper.llm.create(input) Result.success(wrapper) } catch (e: Exception) { Result.failure(e) } } } }

关键设计点:

  • 默认调度器为Dispatchers.IO,保证模型创建这类耗时操作不阻塞主线程;调用方可通过dispatcher(...)覆盖。
  • 句柄(handle: Long)是原生对象在 Kotlin 侧的“令牌”,所有后续操作都以它作为参数传给 JNI。
  • build()Result包装结果,模型路径缺失等参数问题会以异常形式捕获并转为Result.failure
  • Wrapper 实现Closeableclose()内部委托destroy()释放原生资源。

VlmWrapper 的构建方式完全对称(VlmWrapper.kt):

val wrapper = VlmWrapper.builder() .vlmCreateInput(input) .dispatcher(Dispatchers.IO) .build()

2.2 流式生成:callbackFlow 包装原生回调

两个 Wrapper 都提供了generateStreamFlow(prompt, config): Flow<LlmStreamResult>,用callbackFlow把原生回调模型翻译成 Kotlin Flow,上层可以像收集普通 Flow 一样处理流式输出:

fun generateStreamFlow(prompt: String, config: GenerationConfig): Flow<LlmStreamResult> = callbackFlow { withContext(dispatcher) { val callback = object : LLMTokenCallback { override fun onToken(token: String): Boolean { trySend(LlmStreamResult.Token(token)) return true } override fun onComplete(result: LlmGenerateResult) { trySend(LlmStreamResult.Completed(result.profileData)) close() } } try { val result = llm.generate(handle, prompt, config, callback) Log.d(TAG, "llm result:$result") } catch (e: Exception) { trySend(LlmStreamResult.Error(e)) close() } } awaitClose { close() } }

流经这个 Flow 的事件分为三类:

事件含义
LlmStreamResult.Token(token)每次产出的新 token 文本
LlmStreamResult.Completed(profileData)生成结束,携带 ProfilingData 性能统计
LlmStreamResult.Error(e)生成过程发生异常

onToken返回Boolean是原生端用来控制“是否继续生成”的语义:返回false即可让 C 侧停止吐 token(详见 jni_cb.cpp 的 stop_flag 机制)。

2.3 生命周期与辅助方法

LlmWrapper 还提供以下方法(VlmWrapper 结构相同):

  • applyChatTemplate(messages, tools, enableThinking, addGenerationPrompt):把多轮ChatMessage用模型的聊天模板格式化成单条提示词;tools为可选的工具调用 JSON,enableThinking控制是否开启“思考模式”。
  • stopStream():挂起函数,请求终止正在进行的流式生成。
  • reset():重置模型状态(对应原生geniex_llm_reset)。
  • destroy():释放原生句柄并清零,幂等安全。

VlmWrapper 额外提供injectMediaPathsToConfig(messages, config),它调用原生extractMediaPaths从多模态消息里抽取图片/音频路径,回填到GenerationConfigimagePathsaudioPaths字段中,方便直接构造带媒体的生成请求。

三、Layer 3:JNI 接口声明层

第三层位于bindings/android/app/src/main/java/com/geniex/sdk/jni/,是声明external原生方法的内部类,业务代码不应直接触碰。

LLm.kt 声明的原生方法:

internal class Llm { external fun create(llmCreateInputObj: LlmCreateInput): Long external fun reset(handle: Long): Int external fun destroy(handle: Long): Int external fun stopStream(handle: Long) external fun applyChatTemplate( handle: Long, messages: Array<ChatMessage>, tools: String?, enableThinking: Boolean, addGenerationPrompt: Boolean = true ): LlmApplyChatTemplateOutput external fun generate( handle: Long, prompt: String, config: GenerationConfig, cb: LLMTokenCallback ): LlmGenerateResult }

Vlm.kt 与之对应,并多出两个多模态专属方法:

internal class Vlm { external fun create(vlmCreateInput: VlmCreateInput): Long external fun destroy(handle: Long): Int external fun reset(handle: Long): Int external fun getCapabilities(handle: Long): VlmCapabilities external fun generate(handle: Long, prompt: String, config: GenerationConfig, cb: LLMTokenCallback): LlmGenerateResult external fun applyChatTemplate(handle: Long, messages: Array<VlmChatMessage>, tools: String?, enableThinking: Boolean): LlmApplyChatTemplateOutput external fun stopStream(handle: Long) external fun extractMediaPaths(messages: Array<VlmChatMessage>): Pair<Array<String>, Array<String>> }

getCapabilities用于查询 VLM 的能力信息(VlmCapabilities.kt),extractMediaPaths则负责解析多模态消息中的媒体引用。这一层是 JNI 命名约定的枢纽:Kotlin 类com.geniex.sdk.jni.Llmcreate方法,对应 C++ 侧的Java_com_geniex_sdk_jni_Llm_create函数。

四、Layer 2:C++ JNI 桥接层

这是整个桥接最核心的一层,位于bindings/android/app/src/main/cpp/,负责将 Java/Kotlin 数据类型转换为 C 结构体,并调用核心库 API

4.1 LLM 桥接:llm_bridge_jni.cpp

JNI 函数与核心 C API 的映射关系如下:

JNI 函数调用的核心 API职责
Java_com_geniex_sdk_jni_Llm_creategeniex_llm_create()解析LlmCreateInputgeniex_LlmCreateInput,创建geniex_LLM*句柄并以jlong返回
Java_com_geniex_sdk_jni_Llm_destroygeniex_llm_destroy()释放句柄对应资源
Java_com_geniex_sdk_jni_Llm_generategeniex_llm_generate()发起生成,注册 token/完成回调
Java_com_geniex_sdk_jni_Llm_stopStream置位停止标志通过std::atomic<bool>通知生成线程停止
Java_com_geniex_sdk_jni_Llm_applyChatTemplategeniex_llm_apply_chat_template()聊天模板格式化
Java_com_geniex_sdk_jni_Llm_resetgeniex_llm_reset()重置模型状态

create为例,它展示了完整的类型转换流程:

extern "C" JNIEXPORT jlong JNICALL Java_com_geniex_sdk_jni_Llm_create( JNIEnv* env, jobject thiz, jobject llm_create_input_obj) { geniex_LlmCreateInput create_input = extract_llm_create_input(env, llm_create_input_obj); geniex_LLM* handle = nullptr; int32_t result = geniex_llm_create(&create_input, &handle); if (result != GENIEX_SUCCESS || !handle) { throw_runtime_exception(env, "Llm create failed: %s", geniex_get_error_message(static_cast<geniex_ErrorCode>(result))); return 0; } return reinterpret_cast<jlong>(handle); }

类型转换工具集中在 jniutils.cpp(extract_llm_create_inputextract_generation_configjstring2str等),回调处理集中在 jni_cb.cpp 与 jni_cb.h。

4.2 流式回调的线程模型

generate的实现揭示了流式回调的关键细节:

  • 每次生成前,在全局g_stopFlags表中为句柄登记一个std::atomic<bool>停止标志;
  • C 侧每产出一个 token,通过jni_cb_emit_token回调 Kotlin 的onToken
  • stopStream只是把对应标志置为true,下一次回调即被拦截;
  • 回调完成后清理全局引用与停止标志,保证stop_flag的生命周期安全。

jni_cb.cpp里还有一个值得注意的工程细节——token 编码转换。原生 SDK 输出标准 UTF-8(可能包含 4 字节 emoji),而NewStringUTF期望 JNI 修改版 UTF-8,会破坏增补平面字符。因此jni_cb.cpp实现了utf8_to_jstring:手动解码 UTF-8 序列为 Unicode 码点,再转为 UTF-16 代理对,最后用NewString构造jstring。这正是流式中文、emoji 输出不乱码的底层保障。

4.3 SDK 初始化桥接:geniex_sdk.cpp

JNI_OnLoad是原生库的入口,完成三件事:

  1. 重定向stdout/stderr到 logcat;
  2. 通过geniex_set_log(android_sdk_log_to_logcat)把 SDK 日志路由到GenieXSdk标签;
  3. 调用geniex_init()完成核心库初始化。

此外它还实现了插件注册与 QAIRT 运行时切换的原生方法:

  • registerPlugin(pluginLibPath)dlopen加载插件.sodlsym取出plugin_idcreate_plugin两个符号,交给geniex_register_plugin()注册;
  • setQairtRuntimePath(path):切换 QAIRT 运行时路径。文档与源码注释特别强调:必须在init()之前调用,因为 QNN 库每个进程只会加载一次、从不卸载;路径不可用时错误会在创建模型时才暴露。

五、Layer 1:核心 C 库 libgeniex.so

最底层是核心推理库,对应仓库 sdk 目录。README 描述的核心库 API 来自ml.h/ml_*约定,当前仓库中实际实现为:

  • 头文件:sdk/include/geniex.h,定义geniex_LLMgeniex_ModelConfiggeniex_GenerationConfiggeniex_llm_create/generate/destroy/reset等符号;
  • 实现:sdk/src/llm.cpp、sdk/src/vlm.cpp(以及 device.cpp、ml.cpp、registry.cpp 等);
  • 插件体系:推理后端通过插件动态注册,当前仓库内置 llama_cpp 与 qairt 两个插件,前者提供 CPU/GPU/HYBRID 计算单元,后者基于 Qualcomm QNN 提供 NPU 加速。

从源码结构可以推断,核心库通过插件注册表(registry)+ 运行时标识(runtime_id)做后端分发:上层只提交“模型路径 + 配置”,runtime_id决定由哪个插件实际执行,从而实现同一套 JNI 桥接层支持多种推理后端的可扩展设计。

六、LLM 完整调用链:从 Kotlin 一行调用到 C 推理

将四层串联起来,一次 LLM 流式生成的生命周期如下:

  1. 初始化GenieXSdk.getInstance().init(context),注册插件并初始化模型管理器;
  2. 创建LlmWrapper.builder().llmCreateInput(LlmCreateInput(...)).build()Llm.create(external)→Java_..._Llm_creategeniex_llm_create(),获得原生句柄;
  3. 生成generateStreamFlow(prompt, config)llm.generateJava_..._Llm_generategeniex_llm_generate(),C 侧逐 token 回调onToken,经 UTF-8→UTF-16 转换后由 Flow 发射给上层;
  4. 停止/重置stopStream()置位原子标志中断生成;reset()调用geniex_llm_reset
  5. 释放close()/destroy()geniex_llm_destroy(),句柄清零。

6.1 关键输入配置

LlmCreateInput.kt 定义模型创建输入:

data class LlmCreateInput( override val model_path: String, // 必填:模型路径 val tokenizer_path: String? = null, // tokenizer 路径(可选) override val config: ModelConfig, // 模型配置 override val runtime_id: String? = null, // 后端选择(llama_cpp / qairt) override val compute_unit: String? = null // 计算单元别名 ) : CreateInputBase

compute_unitnull时选择各后端默认值:llama_cpp默认HYBRIDqairt默认NPU;也可显式指定CPU/GPU/NPU/HYBRID

ModelConfig.kt 与原生geniex_ModelConfig结构体一一对应,核心字段:

字段默认值说明
nCtx2048文本上下文大小,0 = 使用模型默认
nThreads8文本生成线程数
nThreadsBatch8批量处理线程数
nBatch2048提交给llama_decode的最大逻辑批量
nUBatch512后端支持的最大物理批量
nSeqMax1最大并行序列数
nGpuLayers-1卸载到 GPU/NPU 的层数;-1 = 全部(CPU 计算单元下 JNI 会强制为 0)
spec_type""投机解码类型(llama_cpp 专属),如draft-mtpngram-*

GenerationConfig.kt 控制单次生成行为:

data class GenerationConfig( var maxTokens: Int = 32, var stopWords: Array<String>? = null, var stopCount: Int = 0, var samplerConfig: SamplerConfig? = null, var imagePaths: Array<String>? = null, // 多模态输入 var imageCount: Int = 0, var audioPaths: Array<String>? = null, var audioCount: Int = 0, var slidingWindow: Boolean = false, // 环缓冲上下文淘汰(qairt) var slidingWindowNKeep: Int = 0 // 0 = 插件默认(4) )

七、SDK 初始化、插件注册与原生库加载

GenieXSdk.kt 是 SDK 入口,其companion object中的静态初始化块负责加载 JNI 桥接库:

companion object { init { System.loadLibrary("npu_jni") // 加载 Layer 2 JNI 桥接 } }

init(context, callback)完成两层初始化,并通过@Volatile标志保证幂等(Activity 重建后重复调用是安全的):

  1. 插件注册:遍历RuntimeIdValue.LLAMA_CPPRuntimeIdValue.QAIRT,在nativeLibraryDir下查找libgeniex_plugin_<name>.so,存在则调用原生registerPlugin动态注册;
  2. 模型管理器初始化:在context.filesDir/geniex创建数据目录,调用 ModelManager 初始化模型管理 FFI(错误码 -100008 对应GENIEX_ERROR_COMMON_ALREADY_INITIALIZED,视为幂等成功)。

所有异常被收集后统一通过InitCallback.onFailure(reason)上报,无异常则回调onSuccess()。README 中提到的动态插件加载目标产物libgeniex_plugin.so(NPU 后端,内部基于 QNN)也由此机制完成注册,从而在不改动桥接层的前提下扩展新的推理后端。

八、构建系统与产物

8.1 原生库编译

Android 工程的 CMake 入口为 bindings/android/app/src/main/cpp/CMakeLists.txt。它并不在本地编译核心库,而是链接预编译的 SDK 包

# 使用预构建 SDK 包 sdk/pkg-geniex get_filename_component(PROJECT_ROOT "${CMAKE_SOURCE_DIR}/../../../../../.." ABSOLUTE) set(SDK_INSTALL_DIR "${PROJECT_ROOT}/sdk/pkg-geniex") set(geniex_bridge "${SDK_INSTALL_DIR}/lib/libgeniex.so") set(SDK_INCLUDE_DIR "${SDK_INSTALL_DIR}/include") add_subdirectory(qnn) # NPU 后端构建配置(QNN 库)

qnn/CMakeLists.txt 负责 NPU 后端的构建配置(QNN 库),仓库同时提供了 arm64-android-llvm.cmake 等交叉编译工具链,用于产出 arm64-v8a 目标(app/extLibs/arm64-v8a/libomp.so为预置的 OpenMP 运行时)。

最终构建产物包括:

产物说明
libnpu_jni.soJNI 桥接包装库,由GenieXSdk通过System.loadLibrary("npu_jni")加载
libgeniex.so核心 ML 库,被 JNI 包装库链接
libgeniex_plugin_<runtime>.so插件库,如 NPU 后端,供registerPlugin动态加载

8.2 发布到 Maven

README 记录的 Android SDK AAR 发布流程(在bindings/android目录下执行):

  1. 修改app/update.gradle中的tmpVersion为新版本号;
  2. 使用 Gradle 同步工程;
  3. 执行./gradlew assembleRelease构建 release AAR;
  4. 执行./gradlew publish生成 Maven 格式的repo目录。

仓库同时提供了完整的 Gradle 工程骨架:settings.gradle.kts、gradle/libs.versions.toml、gradle.properties 与 gradlew 包装器。README 还标注了“发布到 Maven Central”与“发布到 GitHub”两个 TODO 章节,说明这两条公网发布路径尚待补充详细文档。

九、目录结构与模块定位

bindings/android/ ├── app/ │ ├── src/main/ │ │ ├── cpp/ # Layer 2: JNI Bridge (C++) │ │ │ ├── llm_bridge_jni.cpp # LLM JNI 实现 │ │ │ ├── vlm_bridge_jni.cpp # VLM JNI 实现 │ │ │ ├── geniex_sdk.cpp # SDK 初始化与插件注册 │ │ │ ├── jniutils.cpp/.h # JNI 类型转换工具 │ │ │ ├── jni_cb.cpp/.h # 流式回调处理 │ │ │ ├── qnn/CMakeLists.txt # NPU 后端构建配置(QNN) │ │ │ └── CMakeLists.txt │ │ └── java/com/geniex/sdk/ │ │ ├── jni/ # Layer 3: JNI Interface │ │ │ ├── Llm.kt │ │ │ └── Vlm.kt │ │ ├── LlmWrapper.kt # Layer 4: LLM 公共 API │ │ ├── VlmWrapper.kt # Layer 4: VLM 公共 API │ │ ├── GenieXSdk.kt # SDK 入口 │ │ └── bean/ # 数据类(配置、输入、结果、回调) │ └── build.gradle.kts # 构建配置 └── README.md # 本文档

bean/目录下的数据类是 JNI 边界的“契约”——CreateInputBase.kt、ModelConfig.kt、GenerationConfig.kt、LlmGenerateResult.kt、LlmStreamResult.kt 等,它们的字段布局必须与 C++ 侧extract_*解析逻辑严格对应。

十、日志与调试

SDK 日志被统一路由到 logcat 的GenieXSdk标签,优先级与 Android 日志级别一一对应(TRACE→VERBOSEDEBUG→DEBUGINFO→INFOWARN→WARNERROR→ERROR)。桥接层源码中大量使用LOGd/LOGe输出句柄、错误码与调用过程,排查问题时可直接过滤:

adb logcat -s GenieXSdk

此外JNI_OnLoad会把 C 侧的stdout/stderr一并重定向到 logcat,因此原生层的打印也能在同一标签下看到,配合错误码(如geniex_get_error_message返回的描述)即可快速定位创建失败、生成中断等问题的根因。

小结

GenieX 的 Android 绑定用一套严谨的四层 JNI 桥接架构,把“Kotlin 协程 API”与“C 推理内核”干净地解耦:上层开发者只面对 LlmWrapper / VlmWrapper 的构建器与 Flow,原生层通过 llm_bridge_jni.cpp / vlm_bridge_jni.cpp 完成类型转换与回调桥接,最终由libgeniex.so调度 llama_cpp / qairt 插件在 CPU、GPU 或 Qualcomm NPU 上执行推理。理解这条从 Kotlin 到 C 的完整链路,是排查问题、定制后端和扩展多模态能力的基础。

【免费下载链接】GenieXRun frontier LLMs and VLMs locally on Qualcomm devices across NPU, GPU, and CPU with a few lines of code项目地址: https://gitcode.com/GitHub_Trending/ne/GenieX

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 10:11:09

论文降AI率手写论文AIGC率高怎么办?亲测嘎嘎降AI从62%压到5.8%

论文降AI率手写论文AIGC率高怎么办&#xff1f;亲测嘎嘎降AI从62%压到5.8% 核心结论&#xff1a; 论文降AI率最快的方法是用专业工具嘎嘎降AI——降AI率从60%降到5%左右&#xff0c;嘎嘎降AI双引擎驱动&#xff0c;降AI率99.26%达标&#xff0c;嘎嘎降AI不达标可退款&#xff0…

作者头像 李华
网站建设 2026/9/16 10:10:28

中文文本分析实战框架:从预处理到业务落地的四大关键环节

1. 这不是“调个库就能跑”的中文文本分析&#xff0c;而是要先搞懂中文到底有多难很多人点开“Python中文文本分析”这个标题&#xff0c;第一反应是&#xff1a;不就是jieba分词 sklearn向量化 pandas统计吗&#xff1f;我试过三次——第一次跑通了《论语》词频&#xff0c;第…

作者头像 李华
网站建设 2026/9/16 10:09:42

Node.js+Vue构建律师事务所管理系统实战

1. 项目背景与需求分析律师事务所管理系统是法律行业数字化转型的核心工具。随着案件数量激增和客户服务标准提升&#xff0c;传统纸质档案和Excel表格已无法满足现代律所的运营需求。我们团队基于Node.jsVue技术栈开发的这套系统&#xff0c;主要解决以下痛点&#xff1a;案件…

作者头像 李华
网站建设 2026/9/16 10:08:39

高压直流GIS/GIL多物理场耦合建模与绝缘优化

1. 项目背景与核心问题高压直流气体绝缘开关设备&#xff08;HDVS GIS&#xff09;和气体绝缘输电线路&#xff08;GIL&#xff09;作为现代电力系统中的关键设备&#xff0c;其可靠性直接影响电网安全运行。在实际运行中&#xff0c;气固界面处的电场畸变和电荷积聚现象是导致…

作者头像 李华
网站建设 2026/9/16 10:08:24

小数据集YOLO训练实战:从数据预处理到部署全流程解析

简介&#xff1a;面向YOLO系列目标检测算法训练与验证的食品图像数据集&#xff0c;覆盖鸡蛋、米饭等常见食材类别&#xff0c;适合目标检测初学者及算法调优开发者使用。压缩包共包含547个文件&#xff0c;包括273张jpg原图、273个对应txt标签文件及1个yaml配置文件&#xff0…

作者头像 李华
网站建设 2026/9/16 10:05:11

Win11安装MySQL 8.0避坑指南:从兼容配置到服务启动全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华