基于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()
关键注意事项
- Uri≠文件路径:Android媒体库的Uri无法直接被
cv.imread读取,必须转换为字节数组或本地文件。 - Chaquopy类型兼容:字节数组、字符串、数字可直接传递;numpy数组建议转成列表返回,方便Kotlin处理。
- 权限合规:用ContentResolver读取Uri是符合Android权限规范的做法,不要直接读取外部存储绝对路径。
内容的提问来源于stack exchange,提问作者Annisa Septyana Ningrum
相关产品推荐
相关产品推荐

