安卓端YOLOv8无训练识别新目标:标签伪装与模型部署实战 最近在尝试将最新的目标检测模型部署到移动端时发现了一个有趣的需求如何在不重新训练模型的情况下让一个现有的YOLO模型识别一个全新的、它从未见过的目标比如让一个预训练的YOLOv8模型去识别“超人强”这个动漫角色。这听起来像是“无训练识别”或“零样本学习”的范畴但在工程实践中我们往往可以通过一些巧妙的技巧来实现。本文将围绕这个目标详细拆解如何在安卓平台上利用YOLO模型以YOLOv8为例其架构常被社区称为YOLO26的改进基础实现“无训练识别超人强”的完整流程。从环境搭建、模型准备、推理优化到最终APK集成手把手带你走通整个链路无论是移动端AI入门还是项目落地都能直接复用。1. 背景与核心概念什么是“无训练识别”在深度学习领域要让一个模型识别新类别传统做法是收集新类别的数据重新标注然后进行微调Fine-tuning训练。这个过程耗时耗力尤其对于移动端开发训练环境和数据准备都是门槛。“无训练识别”在这里并非严格的学术定义而是一种工程实践思路。其核心思想是利用预训练模型强大的特征提取能力结合一些后处理或外部知识使其能够处理训练集中不存在的类别。对于目标检测常见的“无训练”思路有几种基于相似性的分类如果预训练模型输出特征向量Feature Vector我们可以计算新目标图像的特征与已知类别特征的相似度如余弦相似度如果与某个已知类高度相似则可以“映射”过去。例如超人强可能在某些特征上与“人”person类相似。提示词工程CLIP等模型对于多模态模型如CLIP可以直接输入“a photo of Superman Qiang”这样的文本提示与图像特征进行匹配实现开放词汇检测。但这需要模型本身支持。修改模型输出层最实用对于YOLO这类检测模型其输出层是固定的对应训练时的类别数。一个取巧但有效的方法是不修改模型结构而是修改推理后的标签映射文件。我们将模型对于“超人强”的检测在逻辑上“伪装”成模型已有的某个类别比如选择一个不常用的类别ID然后在后处理阶段将这个ID对应的标签名称替换为“超人强”。这要求“超人强”在视觉特征上能被模型检测为某个已有的边界框只是类别名不对。本文将采用第三种方法因为它不涉及模型重训练或复杂部署最适合在安卓端快速实现。我们的目标是让一个在COCO数据集上预训练的YOLOv8模型在安卓App中实时检测摄像头画面并将检测到的某个特定对象我们预设为“人”类但实际可能是任何物体的标签显示为“超人强”。2. 环境准备与版本说明在开始编码前需要准备好开发环境。我们的技术栈是Android Studio Java/Kotlin 预训练YOLO模型 推理引擎。核心环境清单操作系统Windows 10/11, macOS 或 Linux (Ubuntu 20.04)。本文演示以Windows为例。Android开发环境Android Studio推荐最新稳定版如Giraffe/2022.3.1。确保已安装Android SDKAPI Level 24即Android 7.0以上。JDKAndroid Studio捆绑的JDK 17或OpenJDK 11。模型准备环境PythonPython 3.8用于下载和验证模型。Ultralytics YOLOv8用于导出模型。安装命令pip install ultralytics模型格式我们将使用ONNX格式。它是开放的模型交换格式在移动端有较好的支持如使用ONNX Runtime或NCNN。安卓推理引擎选择为了平衡易用性和性能我们选择ONNX Runtime Mobile。它针对移动设备优化支持CPU/GPU/NNAPI且API简洁。版本说明以下版本在撰写时经过测试但深度学习领域更新快请根据你的实际情况调整。ultralytics8.0.xonnxruntime-android:1.15.1(在安卓项目中通过Gradle引入)项目结构预览AndroidYOLODemo/ ├── app/ │ ├── libs/ # 存放onnxruntime的AAR包如果手动引入 │ ├── src/main/ │ │ ├── assets/ # 存放模型文件.onnx和标签文件.txt │ │ ├── java/com/example/yolodemo/ │ │ │ ├── MainActivity.kt # 主界面和摄像头逻辑 │ │ │ ├── ObjectDetector.kt # 核心推理类 │ │ │ └── ... │ │ └── res/ # 布局文件等资源 │ └── build.gradle # 模块级构建配置 └── build.gradle # 项目级构建配置3. 核心原理与流程拆解在动手写代码前必须理解整个流程的骨架。我们的“无训练识别超人强”方案分为离线的模型准备阶段和在线的安卓推理阶段。3.1 离线阶段模型准备与“标签伪装”获取预训练模型从Ultralytics官方下载YOLOv8s小型号适合移动端的预训练权重.pt文件。它是在COCO数据集上训练的能识别80个类别其中第0类是“person”。导出为ONNX使用ultralytics库将.pt模型导出为ONNX格式同时可以指定输入尺寸如640x640。制定“伪装”策略这是我们实现“无训练识别”的关键。假设我们想让模型把检测到的“人”person都显示为“超人强”。那么我们只需要修改标签文件。COCO的标签列表是固定的person,bicycle,car, ...。我们创建一个新的标签文件labels.txt将第一行索引0的person替换为superman_qiang超人强。注意这并不会改变模型的判断逻辑模型依然认为那是“人”只是在App中显示时我们给了它一个新名字。如果要识别其他物体你需要找到模型最容易将其误检或正检为的COCO类别然后替换那个类别的标签。3.2 在线阶段安卓端推理流程图像预处理从摄像头获取一帧图像Bitmap将其缩放到模型输入尺寸如640x640并进行归一化像素值从0-255缩放到0-1有时还需要调整通道顺序RGB。模型推理将处理后的图像数据通常是一个FloatArray或ByteBuffer输入到ONNX Runtime中运行会话Session得到输出张量。输出解析YOLOv8的输出格式需要理解。对于检测模型其输出通常是一个形状为[1, 84, 8400]的张量以640输入为例。其中8400是模型在特征图上预测的框的数量anchor points。84是每个框的属性前4个是框的中心坐标和宽高cx, cy, w, h第5个是对象置信度objectness score后79个是COCO 80个类别的分类置信度但我们的模型是80类所以是85这里需要澄清YOLOv8输出是[1, 84, 8400]其中84 4框坐标 80类别概率没有单独的objectness分数它融合在类别概率里了。后处理过滤低置信度框设定一个置信度阈值如0.5过滤掉所有类别最大概率低于此值的框。非极大值抑制NMS对于重叠度IoU过高的框只保留置信度最高的一个避免一个物体被重复检测。映射标签将保留下来的框的类别ID去我们修改过的labels.txt文件中查找得到显示名称“超人强”。绘制结果将检测框和标签绘制到原始的摄像头预览画面上。4. 完整实战从模型导出到安卓App运行4.1 步骤一准备模型和标签文件首先在Python环境中操作。# 1. 安装ultralytics pip install ultralytics # 2. 导出YOLOv8s模型为ONNX格式 python -c from ultralytics import YOLO; model YOLO(yolov8s.pt); model.export(formatonnx, imgsz640)运行后你会得到yolov8s.onnx文件。接下来创建标签文件。默认COCO标签顺序可以在网上找到。我们创建一个coco_labels.txt但将第0行改为“超人强”。# 文件coco_labels.txt (修改后) 超人强 bicycle car motorcycle airplane bus train truck boat traffic light ... (后续类别保持不变)为什么可以这样模型输出第0个类别的概率对应它认为图像中是“人”的可能性。我们只是把“人”这个显示名换成了“超人强”模型的计算过程没有任何变化。这是一种低成本、快速的概念验证方法。如果“超人强”的特征与“人”差异巨大模型可能根本不会对其产生高置信度的响应这时就需要考虑微调或使用其他方法了。4.2 步骤二创建Android Studio项目并配置依赖打开Android Studio新建一个Empty Activity项目语言选择KotlinMinimum SDK选择API 24。在app/build.gradle.kts或app/build.gradle的dependencies块中添加ONNX Runtime依赖。// 文件app/build.gradle.kts dependencies { implementation(androidx.camera:camera-camera2:1.3.0) implementation(androidx.camera:camera-lifecycle:1.3.0) implementation(androidx.camera:camera-view:1.3.0) // ONNX Runtime for Android implementation(com.microsoft.onnxruntime:onnxruntime-android:1.15.1) // 用于图像处理等工具 implementation(org.tensorflow:tensorflow-lite-support:0.4.4) // ... 其他默认依赖 }将上一步得到的yolov8s.onnx和修改后的coco_labels.txt文件复制到项目的app/src/main/assets/目录下。如果assets文件夹不存在请手动创建。4.3 步骤三实现核心推理类ObjectDetector这是整个App的大脑负责加载模型、预处理、推理和后处理。// 文件app/src/main/java/com/example/yolodemo/ObjectDetector.kt package com.example.yolodemo import android.content.Context import android.graphics.Bitmap import android.graphics.Matrix import com.microsoft.onnxruntime.* import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.FloatBuffer import kotlin.math.exp class ObjectDetector(context: Context) { // 模型参数 private val inputWidth 640 private val inputHeight 640 private val modelName yolov8s.onnx private val labelName coco_labels.txt private var session: OrtSession? null private val labels mutableListOfString() // 后处理参数 private val scoreThreshold 0.5f private val nmsThreshold 0.45f data class DetectionResult( val box: FloatArray, // [x1, y1, x2, y2] 原始图像坐标 val score: Float, val labelId: Int, val labelName: String ) init { loadModel(context) loadLabels(context) } private fun loadModel(context: Context) { val env OrtEnvironment.getEnvironment() val sessionOptions OrtSession.SessionOptions() // 可以尝试启用NNAPI或CoreML后端如果设备支持 // sessionOptions.addNnapi() val modelStream context.assets.open(modelName) val modelBytes modelStream.readBytes() session env.createSession(modelBytes, sessionOptions) } private fun loadLabels(context: Context) { context.assets.open(labelName).bufferedReader().useLines { lines - labels.addAll(lines) } } // 核心推理函数 fun detect(bitmap: Bitmap): ListDetectionResult { val inputTensor preprocess(bitmap) val outputs runInference(inputTensor) return postprocess(outputs, bitmap.width, bitmap.height) } private fun preprocess(bitmap: Bitmap): FloatArray { // 1. 调整大小并保持宽高比填充到640x640 val scaledBitmap Bitmap.createScaledBitmap(bitmap, inputWidth, inputHeight, true) // 2. 将像素值归一化到[0, 1]并转换为CHW格式的FloatArray val inputValues FloatArray(3 * inputWidth * inputHeight) val intValues IntArray(inputWidth * inputHeight) scaledBitmap.getPixels(intValues, 0, inputWidth, 0, 0, inputWidth, inputHeight) var pixel 0 for (y in 0 until inputHeight) { for (x in 0 until inputWidth) { val color intValues[pixel] // 提取RGB并归一化 inputValues[y * inputWidth x] ((color shr 16 and 0xFF) / 255.0f) // R inputValues[inputWidth * inputHeight y * inputWidth x] ((color shr 8 and 0xFF) / 255.0f) // G inputValues[2 * inputWidth * inputHeight y * inputWidth x] ((color and 0xFF) / 255.0f) // B } } return inputValues } private fun runInference(inputValues: FloatArray): FloatArray { val session session ?: throw IllegalStateException(Model not loaded) // 创建输入Tensor val inputShape longArrayOf(1, 3, inputHeight.toLong(), inputWidth.toLong()) val inputTensor OnnxTensor.createTensor( OrtEnvironment.getEnvironment(), FloatBuffer.wrap(inputValues), inputShape ) // 运行推理输出名称为output0YOLOv8 ONNX导出默认 val output session.run(mapOf(images to inputTensor)) val outputTensor output[output0] as OnnxTensor val outputBuffer outputTensor.floatBuffer // 将输出转换为FloatArray val outputArray FloatArray(outputBuffer.remaining()) outputBuffer.get(outputArray) output.close() inputTensor.close() return outputArray } private fun postprocess( outputs: FloatArray, imgWidth: Int, imgHeight: Int ): ListDetectionResult { val results mutableListOfDetectionResult() // YOLOv8输出格式: [1, 84, 8400] val numClasses 80 val numBoxes 8400 for (i in 0 until numBoxes) { val boxOffset i * (4 numClasses) // 解析框坐标 (cx, cy, w, h)格式是相对于640x640的 val cx outputs[boxOffset] val cy outputs[boxOffset 1] val w outputs[boxOffset 2] val h outputs[boxOffset 3] // 找到最大类别分数 var maxScore 0f var maxClassId 0 for (c in 0 until numClasses) { val score outputs[boxOffset 4 c] if (score maxScore) { maxScore score maxClassId c } } // 应用置信度阈值 if (maxScore scoreThreshold) continue // 将中心坐标转换为角点坐标并缩放到原始图像尺寸 val x1 (cx - w / 2) * imgWidth / inputWidth val y1 (cy - h / 2) * imgHeight / inputHeight val x2 (cx w / 2) * imgWidth / inputWidth val y2 (cy h / 2) * imgHeight / inputHeight results.add( DetectionResult( box floatArrayOf(x1, y1, x2, y2), score maxScore, labelId maxClassId, labelName labels.getOrElse(maxClassId) { Unknown } // 这里会映射到“超人强” ) ) } // 应用非极大值抑制 (NMS) return nms(results) } private fun nms(detections: ListDetectionResult): ListDetectionResult { val sorted detections.sortedByDescending { it.score } val selected mutableListOfDetectionResult() for (det in sorted) { var overlap false for (sel in selected) { if (iou(det.box, sel.box) nmsThreshold) { overlap true break } } if (!overlap) { selected.add(det) } } return selected } private fun iou(box1: FloatArray, box2: FloatArray): Float { val x1 maxOf(box1[0], box2[0]) val y1 maxOf(box1[1], box2[1]) val x2 minOf(box1[2], box2[2]) val y2 minOf(box1[3], box2[3]) val interArea maxOf(0f, x2 - x1) * maxOf(0f, y2 - y1) val area1 (box1[2] - box1[0]) * (box1[3] - box1[1]) val area2 (box2[2] - box2[0]) * (box2[3] - box2[1]) return interArea / (area1 area2 - interArea) } fun close() { session?.close() } }4.4 步骤四实现主界面和摄像头逻辑MainActivity这个Activity负责控制摄像头并将每一帧图像送给ObjectDetector处理最后将结果绘制到屏幕上。// 文件app/src/main/java/com/example/yolodemo/MainActivity.kt package com.example.yolodemo import android.graphics.* import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.ExecutorService import java.util.concurrent.Executors class MainActivity : AppCompatActivity() { private lateinit var previewView: PreviewView private lateinit var cameraExecutor: ExecutorService private lateinit var objectDetector: ObjectDetector private lateinit var overlayView: OverlayView // 一个自定义View用于绘制检测框 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) previewView findViewById(R.id.preview_view) overlayView findViewById(R.id.overlay_view) cameraExecutor Executors.newSingleThreadExecutor() // 初始化检测器 objectDetector ObjectDetector(this) startCamera() } private fun startCamera() { val cameraProviderFuture ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider cameraProviderFuture.get() val preview Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } // 选择后置摄像头 val cameraSelector CameraSelector.DEFAULT_BACK_CAMERA // 图像分析在此处进行目标检测 val imageAnalysis ImageAnalysis.Builder() .setTargetResolution(Size(640, 480)) // 设置分析分辨率 .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { it.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer { imageProxy - analyzeImage(imageProxy) }) } try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageAnalysis ) } catch (e: Exception) { e.printStackTrace() } }, ContextCompat.getMainExecutor(this)) } private fun analyzeImage(imageProxy: ImageProxy) { // 将ImageProxy转换为Bitmap val bitmap imageProxy.toBitmap() // 需要实现toBitmap扩展函数 if (bitmap ! null) { lifecycleScope.launch(Dispatchers.Default) { val results objectDetector.detect(bitmap) withContext(Dispatchers.Main) { overlayView.setResults(results, bitmap.width, bitmap.height) overlayView.invalidate() } } } imageProxy.close() // 重要关闭ImageProxy释放资源 } // 简单的ImageProxy转Bitmap扩展函数 private fun ImageProxy.toBitmap(): Bitmap? { val planeProxy planes[0] val buffer planeProxy.buffer val pixelStride planeProxy.pixelStride val rowStride planeProxy.rowStride val rowPadding rowStride - pixelStride * width val bitmap Bitmap.createBitmap(width rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888) bitmap.copyPixelsFromBuffer(buffer) return Bitmap.createBitmap(bitmap, 0, 0, width, height) } override fun onDestroy() { super.onDestroy() cameraExecutor.shutdown() objectDetector.close() } }!-- 文件app/src/main/res/layout/activity_main.xml -- ?xml version1.0 encodingutf-8? FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.camera.view.PreviewView android:idid/preview_view android:layout_widthmatch_parent android:layout_heightmatch_parent / com.example.yolodemo.OverlayView android:idid/overlay_view android:layout_widthmatch_parent android:layout_heightmatch_parent / /FrameLayout// 文件app/src/main/java/com/example/yolodemo/OverlayView.kt package com.example.yolodemo import android.content.Context import android.graphics.* import android.util.AttributeSet import android.view.View class OverlayView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, defStyleAttr: Int 0 ) : View(context, attrs, defStyleAttr) { private val paintBox Paint().apply { color Color.RED style Paint.Style.STROKE strokeWidth 4f } private val paintText Paint().apply { color Color.WHITE textSize 40f style Paint.Style.FILL } private val textBgPaint Paint().apply { color Color.RED style Paint.Style.FILL } private var results: ListObjectDetector.DetectionResult emptyList() private var frameWidth: Int 1 private var frameHeight: Int 1 fun setResults( detectionResults: ListObjectDetector.DetectionResult, width: Int, height: Int ) { results detectionResults frameWidth width frameHeight height } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val scaleX width.toFloat() / frameWidth val scaleY height.toFloat() / frameHeight for (result in results) { val left result.box[0] * scaleX val top result.box[1] * scaleY val right result.box[2] * scaleX val bottom result.box[3] * scaleY // 绘制矩形框 canvas.drawRect(left, top, right, bottom, paintBox) // 绘制标签和置信度背景 val label ${result.labelName} ${String.format(%.2f, result.score)} val textWidth paintText.measureText(label) canvas.drawRect( left, top - 50f, left textWidth 20, top, textBgPaint ) // 绘制文本 canvas.drawText(label, left 10, top - 10, paintText) } } }4.5 步骤五配置权限并运行在AndroidManifest.xml中添加摄像头权限。!-- 文件app/src/main/AndroidManifest.xml -- ?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera.any / application ... activity ... ... /activity /application /manifest现在连接安卓设备或启动模拟器确保支持摄像头运行项目。将摄像头对准一个人或者一张“超人强”的图片你应该会看到检测框并且标签显示为“超人强”5. 常见问题与排查思路在实现过程中你可能会遇到以下问题问题现象可能原因解决思路App崩溃日志显示OrtException1. 模型文件未正确放入assets。2. 模型输入形状或类型不匹配。3. ONNX Runtime版本与模型不兼容。1. 检查assets文件夹下是否有yolov8s.onnx文件名是否拼写正确。2. 在preprocess函数中打印inputValues的尺寸确保与模型输入[1,3,640,640]一致。3. 尝试使用session.options.addNnapi()或session.options.addCpu()指定执行提供器。摄像头预览正常但无检测框1. 图像预处理缩放、归一化、通道顺序错误。2. 置信度阈值scoreThreshold设置过高。3. 模型输出解析逻辑错误如索引计算。1. 在preprocess后将处理后的图像保存下来查看是否正常。2. 将scoreThreshold暂时调低至0.3或0.2观察是否有框出现。3. 打印outputs数组的shape和部分值与Python端推理结果对比验证解析逻辑。检测框位置严重偏移1. 后处理中坐标缩放计算错误。2. 模型输出坐标格式理解有误是中心点还是角点。1. 检查postprocess函数中将cx,cy,w,h转换为x1,y1,x2,y2的公式。2. 确认YOLOv8 ONNX模型的输出格式。可以先用Python脚本推理一张图片打印出原始输出值进行比对。标签显示为“Unknown”1. 标签文件coco_labels.txt未加载成功或路径错误。2. 类别IDmaxClassId超出了标签列表范围。1. 在loadLabels函数后打印labels列表确认内容是否正确加载特别是第一行是否为“超人强”。2. 检查maxClassId的值确保它小于labels.size。推理速度非常慢1. 在CPU上运行未启用硬件加速。2. 图像分析分辨率过高。3. 未使用STRATEGY_KEEP_ONLY_LATEST策略导致帧堆积。1. 在sessionOptions中尝试添加.addNnapi()Android NNAPI或确保设备有GPU支持。2. 降低ImageAnalysis的TargetResolution如320x240。3. 确保analyzeImage中及时调用imageProxy.close()。在部分设备上无法安装APK体积过大或包含了不兼容的native库。在build.gradle中配置abiFilters只打包需要的架构如armeabi-v7a, arm64-v8a以减小APK体积。6. 最佳实践与工程建议将“无训练识别”的思路应用到真实项目时需要考虑更多工程细节。模型优化与选择模型轻量化YOLOv8s对于部分老旧手机可能仍有压力。可以考虑更小的模型如YOLOv8n或使用专门的移动端检测模型如MobileNet-SSD但需要重新训练。量化将FP32模型量化为INT8可以显著提升推理速度并减少体积。ONNX Runtime支持量化模型推理。模型格式除了ONNX可以考虑TFLite。它针对移动设备优化更彻底但转换流程可能稍复杂。性能优化异步处理如示例所示务必在子线程如Dispatchers.Default中进行推理避免阻塞UI线程。帧率控制不需要对每一帧都进行检测。可以设置一个合适的帧率如10 FPS或者只在检测到运动时才触发高频率检测。缓存与复用ObjectDetector和OrtSession应作为单例避免重复加载模型。“无训练识别”的进阶思路特征匹配如果“伪装”法不满足需求例如超人强不像任何COCO类别。可以提取模型中间层的特征feature embedding预先计算“超人强”参考图像的特征向量。在推理时计算当前检测区域的特征与参考特征的相似度如果超过阈值则判定为“超人强”。这需要模型支持特征提取并且计算量更大。使用支持开放词汇的模型直接使用Grounding DINO、OWL-ViT或CLIP等模型。这些模型设计上就能根据文本提示识别新物体是真正的零样本检测。但它们的模型通常更大部署到移动端挑战更大。工程健壮性错误处理网络请求、模型加载、推理过程都可能出错。务必添加try-catch块并给用户友好的提示。权限处理动态申请摄像头权限处理用户拒绝的情况。前后台管理当App退到后台时应释放摄像头和推理资源回到前台时再重新初始化。安全与隐私用户数据本示例中图像处理均在设备端完成没有数据上传这是保护隐私的最佳实践。在真实产品中应明确告知用户。模型安全确保使用的模型来源可信避免恶意代码。可以对assets中的模型文件进行完整性校验。通过以上步骤我们成功在安卓端实现了一个“伪装式”的无训练识别Demo。虽然方法取巧但它清晰地展示了移动端AI应用从模型准备到集成上线的完整链路并且为更复杂的零样本学习方案打下了基础。你可以尝试更换不同的标签或者用同样的流程集成其他有趣的模型探索移动AI的更多可能性。

相关新闻

最新新闻

VLM如何学会自主调用视觉工具?VTool-R1强化学习框架解析

VLM如何学会自主调用视觉工具?VTool-R1强化学习框架解析

1. 项目概述:当VLM学会“动手”最近在跟进视觉语言大模型(VLM)的应用落地,一个核心痛点越来越明显:这些模型“看”和“说”的能力很强,但“做”的能力几乎为零。它们能精准描述一张图片里有“一个红色的苹果…

2026/8/14 3:00:37
周三-自动愈合稳定层-工程化实战02

周三-自动愈合稳定层-工程化实战02

别再写 try/except 了——给浏览器自动化加一层"自动愈合"的稳定层「工程化实战」栏目 第 02 篇 与周一《从 IP 到 Browser Context》的关系:周一讲的是环境隔离(L1/L2/L3 指纹),这篇讲的是脚本自身的健壮性——环境隔离…

2026/8/14 3:00:37
深圳精洗专业度看设备流程

深圳精洗专业度看设备流程

在深圳,车主对车辆清洁养护的要求早就不满足于路边随便冲两下了,不少开了三五年以上的老车车主,都愿意花大几十做精洗,精洗也成了很多爱车人的日常选择。不过要说深圳哪家精洗做的专业,很多人都拿不准。其实精洗专不专…

2026/8/14 3:00:37
Windows 10更新失败终极解决方案:覆盖安装原理与实操指南

Windows 10更新失败终极解决方案:覆盖安装原理与实操指南

1. 项目概述:当Windows 10更新“卡死”时,覆盖安装为何是终极方案? 如果你也经历过Windows 10更新时那个令人绝望的“正在准备更新”或“正在下载更新”的无限循环,或者看到过“我们无法完成更新,正在撤销更改”的提示…

2026/8/14 3:00:37
水力发电站智能巡检与运维整体方案

水力发电站智能巡检与运维整体方案

国家能源局数据显示,截至2026年6月底,全国水电累计装机容量达4.54亿千瓦,其中常规水电3.85亿千瓦、抽水蓄能0.69亿千瓦。以一座240万千瓦水电站为例,传统人工巡检需跨越发电机层、母线层、水轮机层和蜗壳层4个作业面,单…

2026/8/14 3:00:37
深圳58同城网站建设一站式指南如何从零开始搭建高效转化的B2B营销平台

深圳58同城网站建设一站式指南如何从零开始搭建高效转化的B2B营销平台

深圳这座城市的节奏,就像被按了快进键。每天早晨,早高峰的地铁里挤满了渴望在这个超大城市扎根的年轻人;深夜的写字楼里,依然闪烁着不肯熄灭的灯光。在这里,机遇与压力并存,焦虑与希望同在。作为一名在深圳深耕互联网营销多年的老炮儿,我见过太多老板因为不懂网络,而让…

2026/8/14 2:55:36