You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Kotlin+Chaquopy的Android图像HSV检测跨语言数据传输问题

问题描述

我要开发一款基于Kotlin的Android程序,用来检测从图库导入图像的HSV值。我已经有了Python程序可以完成图像裁剪、阈值处理并提取各圆形区域的HSV值,现在只需要用Kotlin实现从手机图库获取图像,再解析Python返回的HSV数据。但我不熟悉Kotlin和Chaquopy,集成时卡在了图像数据向Python传输的环节,不知道该传什么格式才能被OpenCV识别。


我的尝试代码

Kotlin代码

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityImportImageBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.btnOpengallery.setOnClickListener {
        startGallery()
    }
}

private val launcherIntentGallery = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == RESULT_OK) {
        val selectedImg = result.data?.data as Uri
        selectedImg.let { uri ->
            binding.imageView.setImageURI(uri)
        }
        binding.btnNext.setOnClickListener {
            val bitmap = selectedImg.toString()
            val py = Python.getInstance()
            val module = py.getModule("pythonfile")
            val result = module.callAttr("main", bitmap)
            binding.textView.text = result.toString()
        }
    }
}
private fun startGallery() {
    val intent = Intent()
    intent.action = ACTION_GET_CONTENT
    intent.type = "image/*"
    val chooser = Intent.createChooser(intent, "Choose a Picture")
    launcherIntentGallery.launch(chooser)
}

Python代码

import numpy as np
import cv2 as cv
import os
import math

# 我试过写这个函数:
# def main (imgdir)
# return h,s,v,concresult

img = cv.imread(imgdir)
x,y=img.shape[1],img.shape[0]

# 裁剪区域计算
midpointx=x/2
midpointy=y/2
widthx=x/4
widthy=x/4
x1,y1,w1,h1 = round(midpointx-widthx), round(midpointy-widthy), round(widthx), round(widthy)
x2,y2,w2,h2 = round(midpointx), round(midpointy-widthy), round(widthx), round(widthy)
x3,y3,w3,h3 = round(midpointx-widthx), round(midpointy), round(widthx), round(widthy)
x4,y4,w4,h4 = round(midpointx), round(midpointy),round(widthx), round(widthy)

# 裁剪图像
cropped1 = img[y1 : y1 + h1, x1 : x1 + w1]
cropped2 = img[y2 : y2 + h2, x2 : x2 + w2]
cropped3 = img[y3 : y3 + h3, x3 : x3 + w3]
cropped4 = img[y4 : y4 + h4, x4 : x4 + w4]
cv.imwrite("circle1.png", cropped1)
cv.imwrite("circle2.png", cropped2)
cv.imwrite("circle3.png", cropped3)
cv.imwrite("circle4.png", cropped4)

# 主处理逻辑
colorRGB = [[],[],[],[]]
colorHSV = [[],[],[],[]]
concresult = np.array([])

for i in range(4):
    img = cv.imread('circle'+str(i+1)+'.png')
    hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)

    # HSV阈值掩码
    lower = np.array([0, 0, 70])
    upper = np.array([180, 255, 255])
    mask = cv.inRange(hsv, lower, upper)
    result = cv.bitwise_and(img, img, mask=mask)
    gray = cv.cvtColor(result, cv.COLOR_BGR2GRAY)
    _, alpha = cv.threshold(gray, 0, 255, cv.THRESH_BINARY)

    # 拆分RGB通道
    b, g, r = cv.split(result)

    # 合并为RGBA
    rgba = [b, g, r, alpha]
    dst = cv.merge(rgba, 4)

    b = np.mean(b[alpha != 0])
    g = np.mean(g[alpha != 0])
    r = np.mean(r[alpha != 0])

    # 转换为HSV
    r, g, b = r/255.0, g/255.0, b/255.0
    mx = max(r, g, b)
    mn = min(r, g, b)
    df = mx-mn

    if mx == mn:
        h = 0
    elif mx == r:
        h = (60 * ((g-b)/df) + 360) % 360
    elif mx == g:
        h = (60 * ((b-r)/df) + 120) % 360
    elif mx == b:
        h = (60 * ((r-g)/df) + 240) % 360

    if mx == 0:
        s = 0
    else:
        s = (df/mx)*100

    v = mx*100

    # 还原RGB值
    r, g, b = r*255.0, g*255.0, b*255.0

    # 计算浓度
    y = s
    a = 6.14823
    b = 2.04579
    conc = (y-a)/b
    concresult = np.append (concresult, conc)

    # 删除临时文件
    os.remove('circle'+str(i+1)+'.png')

# 调试输出结果
print(concresult)

解决方案

核心问题是:你传递的Uri字符串不是有效文件路径,OpenCV无法直接读取。下面提供两种可行的传输方式:

方式1:传递图像字节数组(推荐)

直接把图像转换成字节数组传给Python,避免文件IO操作,效率更高。

修改后的Kotlin代码

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityImportImageBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.btnOpengallery.setOnClickListener {
        startGallery()
    }
}

private val launcherIntentGallery = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == RESULT_OK) {
        val selectedImg = result.data?.data as Uri
        selectedImg.let { uri ->
            binding.imageView.setImageURI(uri)
        }
        binding.btnNext.setOnClickListener {
            // 读取Uri对应的图像字节数组
            val inputStream = contentResolver.openInputStream(selectedImg)
            val imgBytes = inputStream?.readBytes()
            inputStream?.close()
            
            imgBytes?.let {
                val py = Python.getInstance()
                val module = py.getModule("pythonfile")
                val result = module.callAttr("main", it)
                binding.textView.text = result.toString()
            }
        }
    }
}

private fun startGallery() {
    val intent = Intent()
    intent.action = ACTION_GET_CONTENT
    intent.type = "image/*"
    val chooser = Intent.createChooser(intent, "选择图片")
    launcherIntentGallery.launch(chooser)
}

修改后的Python代码

import numpy as np
import cv2 as cv
import math

def main(img_bytes):
    # 从字节数组解码图像
    nparr = np.frombuffer(img_bytes, np.uint8)
    img = cv.imdecode(nparr, cv.IMREAD_COLOR)
    
    x,y=img.shape[1],img.shape[0]

    # 裁剪区域计算
    midpointx=x/2
    midpointy=y/2
    widthx=x/4
    widthy=x/4
    x1,y1,w1,h1 = round(midpointx-widthx), round(midpointy-widthy), round(widthx), round(widthy)
    x2,y2,w2,h2 = round(midpointx), round(midpointy-widthy), round(widthx), round(widthy)
    x3,y3,w3,h3 = round(midpointx-widthx), round(midpointy), round(widthx), round(widthy)
    x4,y4,w4,h4 = round(midpointx), round(midpointy),round(widthx), round(widthy)

    # 裁剪图像
    cropped_imgs = [
        img[y1 : y1 + h1, x1 : x1 + w1],
        img[y2 : y2 + h2, x2 : x2 + w2],
        img[y3 : y3 + h3, x3 : x3 + w3],
        img[y4 : y4 + h4, x4 : x4 + w4]
    ]
    
    concresult = np.array([])

    for cropped_img in cropped_imgs:
        hsv = cv.cvtColor(cropped_img, cv.COLOR_BGR2HSV)

        # HSV阈值掩码
        lower = np.array([0, 0, 70])
        upper = np.array([180, 255, 255])
        mask = cv.inRange(hsv, lower, upper)
        result = cv.bitwise_and(cropped_img, cropped_img, mask=mask)
        gray = cv.cvtColor(result, cv.COLOR_BGR2GRAY)
        _, alpha = cv.threshold(gray, 0, 255, cv.THRESH_BINARY)

        # 拆分RGB通道并计算均值
        b, g, r = cv.split(result)
        b_mean = np.mean(b[alpha != 0])
        g_mean = np.mean(g[alpha != 0])
        r_mean = np.mean(r[alpha != 0])

        # 转换为HSV
        r_norm, g_norm, b_norm = r_mean/255.0, g_mean/255.0, b_mean/255.0
        mx = max(r_norm, g_norm, b_norm)
        mn = min(r_norm, g_norm, b_norm)
        df = mx - mn

        if mx == mn:
            h = 0
        elif mx == r_norm:
            h = (60 * ((g_norm - b_norm)/df) + 360) % 360
        elif mx == g_norm:
            h = (60 * ((b_norm - r_norm)/df) + 120) % 360
        elif mx == b_norm:
            h = (60 * ((r_norm - g_norm)/df) + 240) % 360

        s = 0 if mx == 0 else (df/mx)*100
        v = mx*100

        # 计算浓度
        y = s
        a = 6.14823
        b_val = 2.04579
        conc = (y - a)/b_val
        concresult = np.append(concresult, conc)

    # 转换为列表,方便Kotlin解析
    return concresult.tolist()

方式2:保存图像到应用私有目录

如果坚持用文件路径传递,可将Uri对应的图像复制到应用私有目录,再传递路径:

Kotlin代码修改部分

binding.btnNext.setOnClickListener {
    val tempFile = File(filesDir, "temp_img.jpg")
    // 复制Uri内容到临时文件
    contentResolver.openInputStream(selectedImg)?.use { input ->
        FileOutputStream(tempFile).use { output ->
            input.copyTo(output)
        }
    }
    
    val py = Python.getInstance()
    val module = py.getModule("pythonfile")
    val result = module.callAttr("main", tempFile.absolutePath)
    binding.textView.text = result.toString()
    
    // 清理临时文件
    tempFile.delete()
}

Python代码补全main函数

def main(imgdir):
    img = cv.imread(imgdir)
    # 后续逻辑和你原来的代码一致,最后返回concresult.tolist()
    return concresult.tolist()

关键注意事项

  1. Uri≠文件路径:Android媒体库的Uri无法直接被cv.imread读取,必须转换为字节数组或本地文件。
  2. Chaquopy类型兼容:字节数组、字符串、数字可直接传递;numpy数组建议转成列表返回,方便Kotlin处理。
  3. 权限合规:用ContentResolver读取Uri是符合Android权限规范的做法,不要直接读取外部存储绝对路径。

内容的提问来源于stack exchange,提问作者Annisa Septyana Ningrum

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 19:17:02