完成多平台拍照软件

This commit is contained in:
BBIT-Kai
2025-08-13 09:17:51 +08:00
parent 02262e3a96
commit 1aa67280fe
24 changed files with 845 additions and 265 deletions
+7 -3
View File
@@ -14,7 +14,7 @@ kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
jvmTarget.set(JvmTarget.JVM_21)
}
}
@@ -36,6 +36,8 @@ kotlin {
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtimeCompose)
// 通用ViewModel
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")
}
commonTest.dependencies {
implementation(libs.kotlin.test)
@@ -43,6 +45,8 @@ kotlin {
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
// 摄像头
implementation("org.bytedeco:javacv-platform:1.5.9")
}
}
}
@@ -69,8 +73,8 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
@@ -6,6 +6,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -18,7 +19,7 @@ class MainActivity : ComponentActivity() {
}
}
@Preview
@Preview(widthDp = 1280, heightDp = 800)
@Composable
fun AppAndroidPreview() {
App()
@@ -1,9 +0,0 @@
package com.bbitcn.bbit_ai
import android.os.Build
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
actual fun getPlatform(): Platform = AndroidPlatform()
@@ -0,0 +1,33 @@
package com.bbitcn.bbit_ai.plantform.CommonInterface
import androidx.compose.ui.graphics.ImageBitmap
actual class MyCamera {
actual fun stopCamera() {
TODO("Not yet implemented")
}
actual fun listAvailableCameras(): List<String> {
return List(3) { "Camera $it" } // 假设有 3 个摄像头
}
actual suspend fun startCamera(index: Int) {
TODO("Not yet implemented")
}
actual suspend fun takePhoto(): ImageBitmap? {
TODO("Not yet implemented")
}
actual fun saveImageBitmapToFile(image: ImageBitmap) {
TODO("Not yet implemented")
}
actual fun loadImage(filePath: String): ImageBitmap? {
TODO("Not yet implemented")
}
}
actual fun getMyCamera(): MyCamera {
return com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera()
}
@@ -0,0 +1,12 @@
package com.bbitcn.bbit_ai.ui.mainFunction
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera
@Composable
actual fun CameraPreview(
modifier: Modifier,
controller: MyCamera
) {
}
@@ -1,44 +1,28 @@
package com.bbitcn.bbit_ai
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.Button
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import bbit_ai.composeapp.generated.resources.Res
import bbit_ai.composeapp.generated.resources.compose_multiplatform
import com.bbitcn.bbit_ai.ui.mainFunction.MainScreen
val M = Modifier
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
@Composable
@Preview
@Preview()
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting")
}
}
}
MainScreen()
}
}
}
@@ -1,9 +0,0 @@
package com.bbitcn.bbit_ai
class Greeting {
private val platform = getPlatform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
@@ -1,7 +0,0 @@
package com.bbitcn.bbit_ai
interface Platform {
val name: String
}
expect fun getPlatform(): Platform
@@ -0,0 +1,200 @@
package com.bbitcn.f8.pad.base
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.net.SocketException
import kotlin.coroutines.cancellation.CancellationException
open class BaseViewModel : ViewModel() {
protected fun doInUIThread(task: () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.Main) {
task()
}
}
}
protected fun <T> doInIoThreadThenUI(
loadingTips: String = "正在加载中",
showDialog: Boolean = true,
onError: (Throwable) -> Unit = { },
onIO: suspend () -> T,
onFinish: () -> Unit = { },
onUI: (T) -> Unit,
) {
viewModelScope.launch {
val result = kotlin.runCatching {
withContext(Dispatchers.IO) {
if (showDialog) {
// Toasty.showLoadingDialog(loadingTips)
}
onIO()
}
}
if (showDialog) {
// hideLoadingDialog()
}
withContext(Dispatchers.Main) {
result.onSuccess { data ->
onUI(data)
}.onFailure { exception ->
// ✅ 如果是协程取消,不处理,只记录日志
if (exception is CancellationException || exception.cause is SocketException && exception.cause?.message?.contains(
"Socket closed"
) == true
) {
// MyLog.test("协程被取消:${exception.javaClass.simpleName}message=${exception.message}")
return@onFailure
}
// 其他异常继续处理
exception.printStackTrace()
onError(exception)
exception.message?.let {
// Toasty.error(it)
}
}.also {
// ✅ 最终执行的操作
onFinish()
}
}
}
}
fun <T> doInIoThread(
loadingTips: String = "正在加载中",
showDialog: Boolean = true,
onError: (Throwable) -> Unit = { },
onFinish: () -> Unit = { },
doInIO: suspend () -> T,
) {
doInIoThreadThenUI(loadingTips, showDialog, onError, doInIO, onFinish) { }
}
fun <T> doInIoThreadNoDialog(
onError: (Throwable) -> Unit = { },
task: suspend () -> T,
) {
doInIoThread(showDialog = false, doInIO = task, onError = onError)
}
/**
* 在IO线程中执行任务,可选择是否显示加载对话框
*/
fun doInIoThreadWith(showLoading: Boolean, loadingTips: String, function: suspend () -> Unit) {
if (showLoading) {
doInIoThread(loadingTips) { function() }
} else {
doInIoThreadNoDialog { function() }
}
}
/**
* 启动一个无限轮询任务
*
* @param pollingInterval 轮询间隔时间(单位:秒)
* @param pollingTask 轮询任务的挂起函数
*/
fun polling(intervalSeconds: Long, task: suspend () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
// MyLog.test("开始轮询任务,间隔:$intervalSeconds 秒")
while (true) {
task()
delay(intervalSeconds * 1000L) // 转换秒为毫秒
}
}
}
}
/**
* 延迟开始轮询任务
*/
suspend fun delayPolling(delaySeconds: Long, intervalSeconds: Long, task: suspend () -> Unit) {
delay(delaySeconds * 1000L)
polling(intervalSeconds, task)
}
private val taskMap = mutableMapOf<String, Job>()
/**
* 放弃旧任务,执行新任务
*
* @param key 任务的唯一标识符
* @param block 任务的挂起函数,必须使用协程,不能开启新的协程,否则无法取消任务
*/
fun launchTaskNewFirst(
key: String,
block: suspend () -> Unit
) {
taskMap[key]?.cancel() // 取消旧任务
val job = viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
block()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
taskMap[key] = job
}
/**
* 有新任务时,取消。优先执行旧任务直到完成
*
* @param key 任务的唯一标识符
* @param block 任务函数,可以自由新建协程,但一定要在任务完成时调用 onFinished 回调,否则会导致后续任务永远无法执行
*/
@OptIn(ExperimentalCoroutinesApi::class)
fun launchTaskOldFirst(
key: String,
block: (onFinished: () -> Unit) -> Unit
) {
val existing = taskMap[key]
if (existing?.isActive == true) {
return
}
val job = viewModelScope.launch {
try {
suspendCancellableCoroutine<Unit> { continuation ->
block {
if (continuation.isActive) {
continuation.resume(Unit) { cause ->
println("resume 后任务被取消: $cause")
}
}
taskMap.remove(key)
}
}
} catch (e: Exception) {
e.printStackTrace()
taskMap.remove(key)
}
}
taskMap[key] = job
}
override fun onCleared() {
super.onCleared()
taskMap.values.forEach { it.cancel() }
taskMap.clear()
}
}
@@ -0,0 +1 @@
package com.bbitcn.bbit_ai.base
@@ -0,0 +1,10 @@
package com.bbitcn.bbit_ai.model
data class SaveData(
val id: String,
val projectName: String,
val createTime: String,
val rawImagePath: String,
val aiImagePath: String,
val extraJson: String
)
@@ -0,0 +1,16 @@
package com.bbitcn.bbit_ai.plantform.CommonInterface
import androidx.compose.ui.graphics.ImageBitmap
expect class MyCamera {
suspend fun startCamera(index: Int = 0)
fun stopCamera()
fun listAvailableCameras(): List<String>
suspend fun takePhoto(): ImageBitmap?
fun saveImageBitmapToFile(image: ImageBitmap)
fun loadImage(filePath: String): ImageBitmap?
}
expect fun getMyCamera(): MyCamera
@@ -0,0 +1,226 @@
package com.bbitcn.bbit_ai.ui.mainFunction
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import bbit_ai.composeapp.generated.resources.Res
import bbit_ai.composeapp.generated.resources.compose_multiplatform
import com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera
import com.bbitcn.bbit_ai.M
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview
// commonMain
@Composable
expect fun CameraPreview(modifier: Modifier = M, controller: MyCamera)
@Preview
@Composable
fun MainScreen(
mainViewModel: MainViewModel = viewModel(),
) {
Row(
modifier = Modifier
.safeContentPadding()
.padding(10.dp)
.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
var expanded by remember { mutableStateOf(false) }
val cameraList by mainViewModel.cameraList.collectAsState()
var selectedIndex by remember { mutableStateOf(-1) }
val detail by mainViewModel.curDetail.collectAsState()
LaunchedEffect(cameraList) {
if (cameraList.isNotEmpty() && selectedIndex == -1) {
selectedIndex = 0 // 默认选择第一个摄像头
mainViewModel.startCamera(selectedIndex) // 启动默认摄像头
}
}
Column(modifier = M.width(250.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "摄像头:",
modifier = M.padding(end = 10.dp),
style = MaterialTheme.typography.titleMedium
)
Column {
Text(
text = if (cameraList.isEmpty() || selectedIndex == -1) "未检测到摄像头" else cameraList[selectedIndex],
modifier = M
.padding(10.dp)
.clickable {
expanded = true
},
style = MaterialTheme.typography.titleMedium
)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
cameraList.forEachIndexed { index, label ->
DropdownMenuItem(text = {
Text(label)
}, onClick = {
selectedIndex = index
expanded = false
})
}
}
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
// 刷新摄像头按钮
Button(
modifier = M.padding(2.5.dp),
onClick = { mainViewModel.refreshCameraList() }
) {
Text("刷新")
}
Button(
modifier = M.padding(2.5.dp), onClick = {
mainViewModel.startCamera(selectedIndex)
}) {
Text("连接")
}
Button(
modifier = M.padding(2.5.dp), onClick = {
mainViewModel.takePhots()
}) {
Text("拍照")
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
// 刷新摄像头按钮
Button(
modifier = M.padding(2.5.dp),
onClick = { mainViewModel.initData() }
) {
Text("初始化数据")
}
Button(
modifier = M.padding(2.5.dp), onClick = {
mainViewModel.refreshDataList()
}) {
Text("刷新列表")
}
}
HorizontalDivider(modifier = M.padding(5.dp))
val list by mainViewModel.analyticsList.collectAsState()
LazyColumn(modifier = M.weight(1f)) {
items(list) {
Button(modifier = M.padding(5.dp).animateItem(), onClick = {
mainViewModel.setCurDetail(it)
}) {
Column {
Text(
text = it.projectName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
fontSize = MaterialTheme.typography.bodyLarge.fontSize,
modifier = M.padding(2.5.dp)
)
Text(
text = it.createTime,
maxLines = 1,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
modifier = M.padding(2.5.dp)
)
}
}
}
}
HorizontalDivider(modifier = M.padding(5.dp))
Box(modifier = M.fillMaxWidth().height(50.dp)) {
Text(
text = detail?.extraJson ?: "",
modifier = M.padding(5.dp).fillMaxSize(),
style = MaterialTheme.typography.bodyMedium
)
}
}
CameraPreview(M.fillMaxHeight().weight(2f).widthIn(min = 600.dp), mainViewModel.controller)
Column(
modifier = M.weight(1f).fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val picRaw by mainViewModel.picRaw.collectAsState()
if (picRaw == null) {
Column(
Modifier.fillMaxWidth().weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
MyArea()
}
} else {
Image(
modifier = Modifier.fillMaxWidth().weight(1f),
bitmap = picRaw!!,
contentDescription = null,
contentScale = ContentScale.FillHeight
)
}
val picAi by mainViewModel.picAi.collectAsState()
if (picAi == null) {
Column(
Modifier.fillMaxWidth().weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
MyArea()
}
} else {
Image(
modifier = Modifier.fillMaxWidth().weight(1f),
bitmap = picAi!!,
contentDescription = null,
contentScale = ContentScale.FillHeight
)
}
}
}
}
@Composable
fun MyArea(modifier: Modifier = M) {
Image(
modifier = modifier.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary
),
painter = painterResource(Res.drawable.compose_multiplatform),
contentDescription = null
)
}
@@ -0,0 +1,81 @@
package com.bbitcn.bbit_ai.ui.mainFunction
import androidx.compose.ui.graphics.ImageBitmap
import com.bbitcn.bbit_ai.model.SaveData
import com.bbitcn.bbit_ai.plantform.CommonInterface.getMyCamera
import com.bbitcn.bbit_ai.utils.DataFileUtil
import com.bbitcn.f8.pad.base.BaseViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.io.File
class MainViewModel : BaseViewModel() {
val controller = getMyCamera()
private val _cameraList = MutableStateFlow(listOf<String>())
val cameraList = _cameraList.asStateFlow()
private val _analyticsList = MutableStateFlow(listOf<SaveData>())
val analyticsList = _analyticsList.asStateFlow()
private val _curDetail = MutableStateFlow<SaveData?>(null)
val curDetail = _curDetail.asStateFlow()
private val _picRaw = MutableStateFlow<ImageBitmap?>(null)
val picRaw = _picRaw.asStateFlow()
private val _picAi = MutableStateFlow<ImageBitmap?>(null)
val picAi = _picAi.asStateFlow()
init {
refreshCameraList()
refreshDataList {
if (_curDetail.value == null && _analyticsList.value.isNotEmpty()) {
setCurDetail(_analyticsList.value.first())
}
}
}
fun refreshCameraList() {
doInIoThread {
_cameraList.value = controller.listAvailableCameras() // 获取摄像头列表(见下方)
}
}
fun startCamera(index: Int = 0) {
launchTaskNewFirst("启动摄像头") {
controller.startCamera(index)
}
}
fun takePhots() {
doInIoThread {
controller.takePhoto()?.let {
controller.saveImageBitmapToFile(it)
refreshDataList()
}
}
}
fun initData() {
doInIoThread {
DataFileUtil.init() // 初始化数据文件
refreshDataList()
_curDetail.value = null // 清空当前详情
_picRaw.value = null // 清空图片预览
_picAi.value = null // 清空图片预览
}
}
fun refreshDataList(onFinish: () -> Unit = {}) {
doInIoThread {
_analyticsList.value = DataFileUtil.readAll()
onFinish()
}
}
fun setCurDetail(it: SaveData) {
doInIoThread {
_curDetail.value = it
_picRaw.value = controller.loadImage(it.rawImagePath)
_picAi.value = controller.loadImage(it.aiImagePath)
}
}
}
@@ -0,0 +1,54 @@
package com.bbitcn.bbit_ai.utils
import com.bbitcn.bbit_ai.model.SaveData
import java.io.File
object DataFileUtil {
private val dataDir = File("data")
private val dataFile = File(dataDir, "data.txt")
val rawImage = File(dataDir,"image/raw")
val aiImage = File(dataDir,"image/ai")
fun init() {
if (!dataDir.exists()) {
dataDir.mkdirs()
}
if (!rawImage.exists()) {
rawImage.mkdirs()
}
if (!aiImage.exists()) {
aiImage.mkdirs()
}
dataFile.writeText("") // 清空内容
}
fun addData(
info: SaveData
) {
if (!dataDir.exists()) dataDir.mkdirs()
val line = info.id + "\t" +
info.projectName + "\t" +
info.createTime + "\t" +
info.rawImagePath + "\t" +
info.aiImagePath + "\t" +
info.extraJson
dataFile.appendText(line + "\n")
}
fun readAll(): List<SaveData> {
if (!dataFile.exists()) return emptyList()
return dataFile.readLines().mapNotNull { line ->
val parts = line.split("\t")
if (parts.size < 6) return@mapNotNull null
SaveData(
id = parts[0],
projectName = parts[1],
createTime = parts[2],
rawImagePath = parts[3],
aiImagePath = parts[4],
extraJson = parts[5]
)
}
}
}
@@ -0,0 +1,13 @@
package com.bbitcn.bbit_ai.utils
object MyUtils {
fun getCurrentTime(): String {
val currentTime = System.currentTimeMillis()
return java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(java.util.Date(currentTime))
}
fun getCurrentDate(): String {
val currentDate = System.currentTimeMillis()
return java.text.SimpleDateFormat("yyyy-MM-dd").format(java.util.Date(currentDate))
}
}
@@ -0,0 +1,123 @@
package com.bbitcn.bbit_ai.plantform.CommonInterface
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asSkiaBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import com.bbitcn.bbit_ai.model.SaveData
import com.bbitcn.bbit_ai.utils.DataFileUtil
import com.bbitcn.bbit_ai.utils.MyUtils
import kotlinx.coroutines.flow.MutableStateFlow
import org.bytedeco.javacv.Java2DFrameConverter
import org.bytedeco.javacv.OpenCVFrameGrabber
import org.bytedeco.opencv.opencv_videoio.VideoCapture
import org.jetbrains.skia.Image
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import javax.imageio.ImageIO
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
actual class MyCamera {
val frameFlow = MutableStateFlow<ImageBitmap?>(null)
private var running = false
private var curCameraId = 0
actual suspend fun startCamera(index: Int) {
curCameraId = index
running = false
val grabber = OpenCVFrameGrabber(index)
try {
grabber.start()
val converter = Java2DFrameConverter()
running = true
while (running) {
val frame = grabber.grab() ?: continue
val image = converter.getBufferedImage(frame) ?: continue
frameFlow.value = image.toComposeImageBitmap()
}
} catch (e: Exception) {
println("Camera grab error: ${e.message}")
} finally {
grabber.stop()
grabber.release()
}
}
actual fun stopCamera() {
running = false
}
private fun BufferedImage.toComposeImageBitmap(): ImageBitmap {
val bytes = ByteArrayOutputStream().use {
ImageIO.write(this, "png", it)
it.toByteArray()
}
val bitmap = Image.makeFromEncoded(bytes).toComposeImageBitmap()
return bitmap
}
actual fun listAvailableCameras(): List<String> {
val availableCameras = mutableListOf<String>()
for (i in 0..3) {
val capture = VideoCapture(i)
if (capture.isOpened) {
availableCameras.add("Camera $i")
capture.release()
}
}
return availableCameras
}
actual suspend fun takePhoto(): ImageBitmap? {
return frameFlow.value
}
@OptIn(ExperimentalUuidApi::class)
actual fun saveImageBitmapToFile(image: ImageBitmap) {
val bufferedImage = image.toBufferedImage()
val imageName = "photo_${System.currentTimeMillis()}.png"
val uuid = Uuid.random().toString()
DataFileUtil.addData(
SaveData(
Uuid.random().toString(),
imageName,
MyUtils.getCurrentTime(),
DataFileUtil.rawImage.absolutePath + File.separator + imageName,
DataFileUtil.aiImage.absolutePath + File.separator + imageName,
uuid + "额外信息"
)
)
ImageIO.write(
bufferedImage,
"png",
File(DataFileUtil.rawImage, imageName)
)
ImageIO.write(
bufferedImage,
"png",
File(DataFileUtil.aiImage, imageName)
)
}
actual fun loadImage(filePath: String): ImageBitmap? {
return try {
val bytes = File(filePath).readBytes()
val skiaImage = Image.makeFromEncoded(bytes)
skiaImage.toComposeImageBitmap()
} catch (e: Exception) {
println("Image decode error: ${e.message}")
null
}
}
fun ImageBitmap.toBufferedImage(): BufferedImage {
val skiaImage = Image.makeFromBitmap(this.asSkiaBitmap())
val bytes = skiaImage.encodeToData()?.bytes ?: throw Exception("Encode failed")
return ImageIO.read(bytes.inputStream())
}
}
actual fun getMyCamera(): MyCamera {
return MyCamera()
}
@@ -0,0 +1,40 @@
package com.bbitcn.bbit_ai.ui.mainFunction
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.bbitcn.bbit_ai.M
import com.bbitcn.bbit_ai.plantform.CommonInterface.MyCamera
@Composable
actual fun CameraPreview(
modifier: Modifier,
controller: MyCamera
) {
val imageBitmap = controller.frameFlow.collectAsState()
// 显示图像
Box(
modifier = modifier
.border(
width = 1.dp,
color = androidx.compose.material3.MaterialTheme.colorScheme.primary
)
.fillMaxSize()
) {
imageBitmap.value?.let {
Image(
modifier = M.fillMaxSize(),
bitmap = it,
contentDescription = null,
contentScale = ContentScale.FillHeight
)
}
}
}
@@ -1,7 +0,0 @@
package com.bbitcn.bbit_ai
class JVMPlatform: Platform {
override val name: String = "Java ${System.getProperty("java.version")}"
}
actual fun getPlatform(): Platform = JVMPlatform()