GLM-OCR在Android端集成指南:移动端文档扫描应用开发

如果你正在开发一款需要文字识别功能的Android应用,比如文档扫描、名片管理或者翻译工具,那么集成一个高效、准确的OCR引擎就是核心任务。今天,我们就来聊聊如何把GLM-OCR这个强大的模型搬到你的Android应用里,让它能实时“看懂”摄像头捕捉到的文字。

整个过程听起来可能有点复杂,但别担心,我会带你一步步走下来。我们会从怎么把模型“瘦身”到适合手机运行开始,讲到怎么用摄像头拍出清晰的图片,再到怎么调用模型识别文字,最后把识别出来的文字漂亮地展示出来。跟着这篇指南,你就能在自己的App里实现一个完整的文档扫描功能。

1. 环境准备与项目搭建

在开始写代码之前,我们需要先把“舞台”搭好。这包括准备好模型文件,以及配置好Android项目。

1.1 模型准备:让GLM-OCR“瘦身”上手机

直接用在服务器上的大模型,对于手机来说负担太重了。我们需要对它进行转换和优化,这个过程通常叫做模型轻量化或移动端部署。

首先,你需要获取GLM-OCR的原始模型文件(通常是.onnx.pt格式)。然后,使用专门的工具进行转换。这里推荐使用ONNX RuntimeTensorFlow Lite的转换工具,因为它们对移动端支持非常好。

一个典型的转换流程(以PyTorch模型转TFLite为例)可能像这样:

import torch
import torchvision
import onnx
from onnx_tf.backend import prepare
import tensorflow as tf

# 1. 加载你的PyTorch模型
model = YourGLMOCRModel()
model.load_state_dict(torch.load('glm-ocr.pth'))
model.eval()

# 2. 创建一个示例输入(模拟手机图片尺寸,例如 320x480)
dummy_input = torch.randn(1, 3, 480, 320)

# 3. 导出为ONNX格式
torch.onnx.export(model, dummy_input, "glm-ocr.onnx",
                  input_names=['input'], output_names=['output'],
                  dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}})

# 4. 将ONNX模型转换为TensorFlow格式(可选步骤,如需TFLite)
# ... (使用onnx-tf或类似工具)

# 5. 最终转换为TFLite格式
converter = tf.lite.TFLiteConverter.from_saved_model('tf_model_directory')
converter.optimizations = [tf.lite.Optimize.DEFAULT] # 启用优化
converter.target_spec.supported_types = [tf.float16] # 可选:使用FP16减少模型大小
tflite_model = converter.convert()

# 6. 保存最终模型
with open('glm-ocr.tflite', 'wb') as f:
    f.write(tflite_model)

转换完成后,你会得到一个.tflite.onnx文件。把它放到你Android项目的 app/src/main/assets/ 目录下。这样,应用在安装时就会把这个模型文件打包进去。

1.2 Android项目配置

打开你的Android Studio项目,我们需要在 app/build.gradle 文件里添加一些依赖。这里我们假设使用TFLite来运行模型。

android {
    ...
    // 确保你使用了足够新的NDK版本(模型可能用到一些新算子)
    ndkVersion "25.1.8937393"

    aaptOptions {
        noCompress "tflite" // 防止AAPT压缩我们的模型文件
    }
}

dependencies {
    ...
    // TensorFlow Lite 核心库
    implementation 'org.tensorflow:tensorflow-lite:2.14.0'
    // 可选:如果需要GPU加速
    implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'
    // 可选:支持库,提供一些工具类
    implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'

    // 相机X库,用于更便捷地处理相机
    def camerax_version = "1.3.0-rc01"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    implementation "androidx.camera:camera-view:${camerax_version}"

    // 用于图片处理和UI
    implementation 'com.github.bumptech.glide:glide:4.15.1'
}

别忘了在 AndroidManifest.xml 中添加相机权限:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />

2. 核心功能实现:从拍照到识别

环境搭好了,现在我们来构建核心功能。整个过程可以分解为三个主要步骤:用相机拍照并处理图片、调用模型识别文字、把识别结果显示出来。

2.1 相机图像采集与预处理

我们使用CameraX来简化相机操作。它的API现代且生命周期感知,能省去很多麻烦。预处理的目标是把相机拍到的画面,变成模型能“吃下去”的格式。

首先,创建一个用于预览和拍照的Fragment或Activity布局:

<!-- activity_scan.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!-- 可以加一个矩形框,提示用户对准文档 -->
    <View
        android:id="@+id/documentFrame"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:background="@android:color/transparent"
        android:backgroundTint="#4CAF50"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <Button
        android:id="@+id/captureButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="扫描"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginBottom="50dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

然后,在Activity中设置相机并处理图像:

// ScanActivity.kt
class ScanActivity : AppCompatActivity() {
    private lateinit var cameraExecutor: ExecutorService
    private lateinit var imageAnalyzer: ImageAnalysis

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_scan)
        cameraExecutor = Executors.newSingleThreadExecutor()

        // 请求相机权限(代码略,需使用ActivityResult API)
        // ...

        // 设置相机
        startCamera()
        
        captureButton.setOnClickListener {
            // 触发图像分析
            analyzeCurrentFrame()
        }
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().also {
                it.setSurfaceProvider(viewFinder.surfaceProvider)
            }

            // 构建图像分析用例,用于实时处理帧
            imageAnalyzer = ImageAnalysis.Builder()
                .setTargetResolution(Size(1080, 1920)) // 设置分析分辨率
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer { imageProxy ->
                        // 图像分析逻辑可以在这里实时进行,例如边缘检测提示用户对齐
                        // 为了省电和性能,我们只在点击按钮时分析
                        imageProxy.close()
                    })
                }

            // 选择后置摄像头
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // 解绑所有用例再重新绑定
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageAnalyzer)
            } catch(exc: Exception) {
                Log.e(TAG, "相机绑定失败", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    private fun analyzeCurrentFrame() {
        // 这里我们临时设置一个分析器来捕获当前帧
        imageAnalyzer.setAnalyzer(cameraExecutor) { imageProxy ->
            // 将ImageProxy转换为Bitmap,并进行预处理
            val bitmap = imageProxy.toBitmap() // 需要实现toBitmap扩展函数
            val processedBitmap = preprocessImageForOCR(bitmap)
            
            // 在后台线程运行OCR
            runOnUiThread {
                showLoading(true)
            }
            val ocrResult = runOcrOnImage(processedBitmap)
            
            runOnUiThread {
                showLoading(false)
                displayOcrResult(ocrResult)
            }
            
            imageProxy.close()
            // 分析一次后移除分析器,避免持续分析
            imageAnalyzer.clearAnalyzer()
        }
    }

    // 关键:图像预处理函数
    private fun preprocessImageForOCR(srcBitmap: Bitmap): Bitmap {
        // 1. 裁剪:只取中间文档框部分(假设documentFrame是屏幕上的矩形框)
        val cropRect = getDocumentFrameRect() // 获取框的屏幕坐标并转换为图片坐标
        val croppedBitmap = Bitmap.createBitmap(
            srcBitmap, 
            cropRect.left, 
            cropRect.top, 
            cropRect.width(), 
            cropRect.height()
        )
        
        // 2. 调整大小:缩放到模型输入尺寸,例如 320x480
        val scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, 320, 480, true)
        
        // 3. 增强:可以尝试增加对比度、二值化等,提升识别率
        val enhancedBitmap = applyContrastEnhancement(scaledBitmap)
        
        // 4. 转换为模型需要的输入格式(例如,归一化到[0,1]或[-1,1],并转换为RGB数组)
        return enhancedBitmap
    }
    
    // 其他辅助函数...
    private fun getDocumentFrameRect(): Rect { ... }
    private fun applyContrastEnhancement(bitmap: Bitmap): Bitmap { ... }
}

预处理是提升识别准确率的关键。好的预处理能让模糊、倾斜、光照不均的图片变得“清晰可读”。

2.2 调用OCR模型进行识别

现在,图片准备好了,该模型上场了。我们需要加载TFLite模型,并喂给它处理好的图片数据。

首先,创建一个OCR推理类:

// GLMOCRPredictor.kt
class GLMOCRPredictor(context: Context) {
    private var interpreter: Interpreter? = null
    private val modelInputWidth = 320
    private val modelInputHeight = 480
    private val modelInputChannel = 3
    
    init {
        try {
            // 1. 从assets加载模型文件
            val modelFile = loadModelFile(context, "glm-ocr.tflite")
            // 2. 创建Interpreter,可以添加选项如使用GPU
            val options = Interpreter.Options()
            // options.setUseNNAPI(true) // 使用NNAPI加速
            // 如果需要GPU委托(确保设备支持)
            // val gpuDelegate = GpuDelegate()
            // options.addDelegate(gpuDelegate)
            
            interpreter = Interpreter(modelFile, options)
            Log.d("OCR", "模型加载成功")
        } catch (e: Exception) {
            Log.e("OCR", "模型加载失败", e)
        }
    }
    
    private fun loadModelFile(context: Context, filename: String): MappedByteBuffer {
        val fileDescriptor = context.assets.openFd(filename)
        val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
        val fileChannel = inputStream.channel
        val startOffset = fileDescriptor.startOffset
        val declaredLength = fileDescriptor.declaredLength
        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
    }
    
    // 核心识别函数
    fun recognize(bitmap: Bitmap): OcrResult {
        if (interpreter == null) {
            return OcrResult(error = "模型未加载")
        }
        
        // 1. 将Bitmap转换为模型输入需要的Float数组
        val inputBuffer = preprocessBitmapToFloatArray(bitmap)
        
        // 2. 准备输出缓冲区(根据你的模型输出结构定义)
        // 假设模型输出两个东西:文本框坐标和识别文本
        val outputLocations = Array(1) { Array(100) { FloatArray(4) } } // 假设最多100个框,每个框4个坐标
        val outputTexts = Array(1) { Array(100) { FloatArray(字符集大小) } } // 假设文本用概率分布表示
        
        val outputsMap = HashMap<Int, Any>()
        outputsMap[0] = outputLocations
        outputsMap[1] = outputTexts
        
        // 3. 运行推理
        interpreter?.runForMultipleInputsOutputs(arrayOf<Any>(inputBuffer), outputsMap)
        
        // 4. 后处理:将模型输出转换为可读的文字和框
        val detectedBoxes = processOutputBoxes(outputLocations[0])
        val recognizedTexts = processOutputTexts(outputTexts[0])
        
        // 5. 将文本框和文字配对,并映射回原始图片坐标
        val finalResults = pairBoxesAndTexts(detectedBoxes, recognizedTexts, bitmap)
        
        return OcrResult(
            textBlocks = finalResults,
            success = true
        )
    }
    
    private fun preprocessBitmapToFloatArray(bitmap: Bitmap): FloatArray {
        // 确保Bitmap尺寸正确
        val resizedBitmap = Bitmap.createScaledBitmap(bitmap, modelInputWidth, modelInputHeight, true)
        val inputBuffer = FloatArray(modelInputWidth * modelInputHeight * modelInputChannel)
        
        val pixels = IntArray(modelInputWidth * modelInputHeight)
        resizedBitmap.getPixels(pixels, 0, modelInputWidth, 0, 0, modelInputWidth, modelInputHeight)
        
        // 将像素值归一化到模型需要的范围(例如,[0, 255] -> [0, 1])
        for (i in pixels.indices) {
            val pixel = pixels[i]
            inputBuffer[i * 3] = ((pixel shr 16) and 0xFF) / 255.0f // R
            inputBuffer[i * 3 + 1] = ((pixel shr 8) and 0xFF) / 255.0f // G
            inputBuffer[i * 3 + 2] = (pixel and 0xFF) / 255.0f // B
        }
        return inputBuffer
    }
    
    // 后处理函数(简化版,实际更复杂)
    private fun processOutputBoxes(boxes: Array<FloatArray>): List<RectF> { ... }
    private fun processOutputTexts(textProbs: Array<FloatArray>): List<String> { ... }
    private fun pairBoxesAndTexts(boxes: List<RectF>, texts: List<String>, originalBitmap: Bitmap): List<TextBlock> { ... }
    
    data class OcrResult(
        val textBlocks: List<TextBlock> = emptyList(),
        val success: Boolean = false,
        val error: String? = null
    )
    
    data class TextBlock(
        val text: String,
        val boundingBox: RectF, // 文本框在原图中的位置
        val confidence: Float
    )
}

这个类封装了模型加载和推理的全过程。recognize 函数是核心,它接收一个预处理好的Bitmap,运行模型,并返回结构化的识别结果。

2.3 识别结果的可视化与编辑

识别出文字和位置后,我们需要把它直观地展示给用户,并允许他们进行简单的编辑。

我们可以创建一个自定义的 OverlayView,在图片上绘制识别出的文本框和文字:

// OcrResultOverlayView.kt
class OcrResultOverlayView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    
    private var originalBitmap: Bitmap? = null
    private var textBlocks: List<TextBlock> = emptyList()
    private var selectedBlockIndex: Int = -1
    private val paint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.STROKE
        strokeWidth = 4f
    }
    private val textPaint = Paint().apply {
        isAntiAlias = true
        color = Color.WHITE
        textSize = 24f
        style = Paint.Style.FILL
    }
    private val selectedPaint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.STROKE
        strokeWidth = 6f
        color = Color.RED
    }
    
    fun setOcrResult(bitmap: Bitmap, blocks: List<TextBlock>) {
        originalBitmap = bitmap
        textBlocks = blocks
        invalidate() // 触发重绘
    }
    
    fun setSelectedBlock(index: Int) {
        selectedBlockIndex = index
        invalidate()
    }
    
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        originalBitmap?.let { bitmap ->
            // 1. 绘制原始图片(可能需要缩放以适应View)
            val scaleX = width.toFloat() / bitmap.width
            val scaleY = height.toFloat() / bitmap.height
            val scale = scaleX.coerceAtMost(scaleY)
            val scaledWidth = bitmap.width * scale
            val scaledHeight = bitmap.height * scale
            val left = (width - scaledWidth) / 2
            val top = (height - scaledHeight) / 2
            
            canvas.drawBitmap(bitmap, null, RectF(left, top, left + scaledWidth, top + scaledHeight), null)
            
            // 2. 绘制所有文本框
            textBlocks.forEachIndexed { index, block ->
                val box = block.boundingBox
                // 将框的坐标从原图坐标转换到当前View的绘制坐标
                val drawRect = RectF(
                    left + box.left * scale,
                    top + box.top * scale,
                    left + box.right * scale,
                    top + box.bottom * scale
                )
                
                // 根据是否被选中使用不同画笔
                val currentPaint = if (index == selectedBlockIndex) selectedPaint else paint.apply { color = Color.GREEN }
                canvas.drawRect(drawRect, currentPaint)
                
                // 3. 在框的上方绘制识别出的文字
                canvas.drawText(block.text, drawRect.left, drawRect.top - 10, textPaint)
            }
        }
    }
    
    // 处理触摸事件,让用户可以点击选择文本框进行编辑
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                originalBitmap?.let { bitmap ->
                    val scaleX = width.toFloat() / bitmap.width
                    val scaleY = height.toFloat() / bitmap.height
                    val scale = scaleX.coerceAtMost(scaleY)
                    val left = (width - bitmap.width * scale) / 2
                    val top = (height - bitmap.height * scale) / 2
                    
                    // 将触摸点坐标转换回原图坐标
                    val touchXInBitmap = (event.x - left) / scale
                    val touchYInBitmap = (event.y - top) / scale
                    
                    // 查找被点击的文本框
                    val clickedIndex = textBlocks.indexOfFirst { block ->
                        block.boundingBox.contains(touchXInBitmap, touchYInBitmap)
                    }
                    
                    if (clickedIndex != -1) {
                        selectedBlockIndex = clickedIndex
                        invalidate()
                        // 触发编辑事件
                        onTextBlockSelectedListener?.onSelected(textBlocks[clickedIndex], clickedIndex)
                        return true
                    }
                }
            }
        }
        return super.onTouchEvent(event)
    }
    
    var onTextBlockSelectedListener: OnTextBlockSelectedListener? = null
    interface OnTextBlockSelectedListener {
        fun onSelected(block: TextBlock, index: Int)
    }
}

在Activity中,我们可以这样使用这个OverlayView,并提供一个编辑界面:

// 在ScanActivity中
private fun displayOcrResult(result: GLMOCRPredictor.OcrResult) {
    if (!result.success) {
        Toast.makeText(this, "识别失败: ${result.error}", Toast.LENGTH_SHORT).show()
        return
    }
    
    // 1. 显示带标注的图片
    overlayView.setOcrResult(processedBitmap, result.textBlocks)
    
    // 2. 在侧边或底部显示所有识别出的文本,方便整体编辑
    val allText = result.textBlocks.joinToString("\n") { it.text }
    textResultTextView.text = allText
    
    // 3. 设置点击监听,点击某个文本框时弹出编辑对话框
    overlayView.onTextBlockSelectedListener = object : OcrResultOverlayView.OnTextBlockSelectedListener {
        override fun onSelected(block: GLMOCRPredictor.TextBlock, index: Int) {
            showTextEditDialog(block.text, index)
        }
    }
}

private fun showTextEditDialog(originalText: String, blockIndex: Int) {
    val editText = EditText(this).apply {
        setText(originalText)
    }
    
    AlertDialog.Builder(this)
        .setTitle("编辑识别文本")
        .setView(editText)
        .setPositiveButton("确认") { _, _ ->
            val newText = editText.text.toString()
            // 更新数据源和UI
            // (这里需要维护一个可变的textBlocks列表)
            updatedTextBlocks[blockIndex] = updatedTextBlocks[blockIndex].copy(text = newText)
            overlayView.setOcrResult(processedBitmap, updatedTextBlocks)
            updateAllTextDisplay()
        }
        .setNegativeButton("取消", null)
        .show()
}

这样,用户就能看到被绿色框框住的文字,点击某个框还能修改识别有误的内容,体验就完整了。

3. 优化与进阶技巧

基础功能跑通后,我们可以考虑一些优化,让应用更流畅、更准确、更好用。

性能优化

  • 异步处理:确保图像预处理和OCR推理都在后台线程进行,避免阻塞UI。
  • 模型量化:在转换模型时使用 tf.lite.Optimize.DEFAULTtf.float16,能显著减少模型体积和提升推理速度。
  • 缓存与复用Interpreter 的初始化比较耗时,应该作为单例全局复用。
  • 降低分析频率:在实时预览时,不要每帧都进行OCR,可以设置一个时间间隔(如每秒1-2次),或者只在用户点击“扫描”按钮时分析当前帧。

准确率提升

  • 图像预处理增强:尝试在预处理阶段加入更复杂的算法,比如透视变换矫正倾斜文档,自适应二值化应对光照不均,去噪滤波让文字更清晰。
  • 后处理优化:模型输出的文字可能有不连贯或错误字符。可以引入一个字典或语言模型进行纠错,或者对相似位置的文本块进行合理的段落合并
  • 多模型融合:对于复杂场景,可以尝试先用一个轻量模型检测文本区域,再针对每个区域用精度更高的模型进行识别。

功能扩展

  • 实时检测提示:在预览时,可以运行一个轻量的文本检测模型,实时在屏幕上画出文本区域,引导用户将文档对准。
  • 多语言支持:如果GLM-OCR支持,可以增加语言切换功能,提升多语言文档的识别率。
  • 结果导出:增加将识别结果导出为TXT、PDF或Word文件的功能,并保留排版格式。
  • 历史记录:将扫描记录保存到本地数据库,方便用户查看和管理。

4. 常见问题与调试

集成过程中,你可能会遇到下面这些问题:

  • 模型加载失败:检查模型文件是否正确放置在 assets 目录,并且 build.gradle 中设置了 aaptOptions { noCompress "tflite" }。同时确认模型格式与Interpreter兼容。
  • 推理速度慢:首先检查是否在后台线程运行。如果还慢,可以尝试降低输入图片的分辨率,或者启用GPU/NNAPI加速(需测试设备兼容性)。
  • 识别准确率低
    • 检查预处理:确保传递给模型的图片是清晰、方正、对比度足够的。可以在UI上先显示预处理后的图片看看效果。
    • 检查模型输入:确认图片缩放、颜色通道(RGB/BGR)和归一化范围(如[0,1][-1,1])与模型训练时完全一致。
    • 检查后处理:模型输出的坐标和文字序列可能需要复杂的解码(例如CTC解码),确保你的后处理逻辑正确。
  • 内存溢出(OOM):大尺寸Bitmap是内存杀手。务必及时回收不再使用的Bitmap(调用 recycle()),并在处理完成后及时关闭 ImageProxy

调试时,可以把预处理后的图片保存到手机相册,看看是不是你期望的样子。也可以把模型输出的原始数据(文本框坐标、字符概率)打印出来,验证后处理逻辑是否正确。


整体走下来,在Android里集成GLM-OCR的核心思路就是把模型转换好、把图片处理好、把结果展示好。一开始可能会在模型输入输出格式、坐标转换这些地方卡一下,多调试几次就顺了。实际用起来,识别效果很大程度上取决于图片预处理做得好不好,这块值得多花点心思。如果你刚开始做,建议先确保基础流程跑通,再慢慢加上实时预览、纠错这些进阶功能。代码里有些地方我做了简化,比如后处理逻辑,你需要根据GLM-OCR模型的实际输出格式来完善它。

获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐