diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 36056fe..5eeb511 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -3,7 +3,9 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PartMap import retrofit2.http.QueryMap interface RetrofitService { @@ -62,4 +64,11 @@ @QueryMap limit: Map, @QueryMap offset: Map ): String + + /** + * 获取画面识别结果 + */ + @Multipart + @POST("detect") + suspend fun getRecognizeResult(@PartMap body: MutableMap): String } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 36056fe..5eeb511 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -3,7 +3,9 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PartMap import retrofit2.http.QueryMap interface RetrofitService { @@ -62,4 +64,11 @@ @QueryMap limit: Map, @QueryMap offset: Map ): String + + /** + * 获取画面识别结果 + */ + @Multipart + @POST("detect") + suspend fun getRecognizeResult(@PartMap body: MutableMap): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 2ddf452..26b2c8a 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,10 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody + object RetrofitServiceManager { private val api by lazy { @@ -17,6 +19,13 @@ RetrofitFactory.createRetrofit(httpConfig, timeout = 20) } + private val aiApi by lazy { + val httpConfig = SaveKeyValues.getValue( + LocaleConstant.AI_SERVER_CONFIG, LocaleConstant.AI_BASE_URL + ) as String + RetrofitFactory.createRetrofit(httpConfig, timeout = 20) + } + /** * 获取巡查记录 */ @@ -116,4 +125,14 @@ AuthenticationHelper.token, requestBody, limitMap, offsetMap ) } + + /** + * 获取画面识别结果 + * */ + suspend fun getRecognizeResult(base64: String, scene: String): String { + val map: MutableMap = mutableMapOf() + map["img"] = "data:image/png;base64,${base64}".toRequestBody() + map["scene"] = scene.toRequestBody() + return aiApi.getRecognizeResult(map) + } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 36056fe..5eeb511 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -3,7 +3,9 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PartMap import retrofit2.http.QueryMap interface RetrofitService { @@ -62,4 +64,11 @@ @QueryMap limit: Map, @QueryMap offset: Map ): String + + /** + * 获取画面识别结果 + */ + @Multipart + @POST("detect") + suspend fun getRecognizeResult(@PartMap body: MutableMap): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 2ddf452..26b2c8a 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,10 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody + object RetrofitServiceManager { private val api by lazy { @@ -17,6 +19,13 @@ RetrofitFactory.createRetrofit(httpConfig, timeout = 20) } + private val aiApi by lazy { + val httpConfig = SaveKeyValues.getValue( + LocaleConstant.AI_SERVER_CONFIG, LocaleConstant.AI_BASE_URL + ) as String + RetrofitFactory.createRetrofit(httpConfig, timeout = 20) + } + /** * 获取巡查记录 */ @@ -116,4 +125,14 @@ AuthenticationHelper.token, requestBody, limitMap, offsetMap ) } + + /** + * 获取画面识别结果 + * */ + suspend fun getRecognizeResult(base64: String, scene: String): String { + val map: MutableMap = mutableMapOf() + map["img"] = "data:image/png;base64,${base64}".toRequestBody() + map["scene"] = scene.toRequestBody() + return aiApi.getRecognizeResult(map) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt index 949492a..4b82f2d 100644 --- a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt @@ -55,5 +55,7 @@ const val ACCOUNT = "account" const val PASSWORD = "password" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" - const val SERVER_BASE_URL = "http://192.168.153.213:8080" + const val SERVER_BASE_URL = "http://192.168.137.213:8080" + const val AI_SERVER_CONFIG = "aiServerConfig" + const val AI_BASE_URL = "http://192.168.206.248:5000" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 36056fe..5eeb511 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -3,7 +3,9 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PartMap import retrofit2.http.QueryMap interface RetrofitService { @@ -62,4 +64,11 @@ @QueryMap limit: Map, @QueryMap offset: Map ): String + + /** + * 获取画面识别结果 + */ + @Multipart + @POST("detect") + suspend fun getRecognizeResult(@PartMap body: MutableMap): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 2ddf452..26b2c8a 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,10 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody + object RetrofitServiceManager { private val api by lazy { @@ -17,6 +19,13 @@ RetrofitFactory.createRetrofit(httpConfig, timeout = 20) } + private val aiApi by lazy { + val httpConfig = SaveKeyValues.getValue( + LocaleConstant.AI_SERVER_CONFIG, LocaleConstant.AI_BASE_URL + ) as String + RetrofitFactory.createRetrofit(httpConfig, timeout = 20) + } + /** * 获取巡查记录 */ @@ -116,4 +125,14 @@ AuthenticationHelper.token, requestBody, limitMap, offsetMap ) } + + /** + * 获取画面识别结果 + * */ + suspend fun getRecognizeResult(base64: String, scene: String): String { + val map: MutableMap = mutableMapOf() + map["img"] = "data:image/png;base64,${base64}".toRequestBody() + map["scene"] = scene.toRequestBody() + return aiApi.getRecognizeResult(map) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt index 949492a..4b82f2d 100644 --- a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt @@ -55,5 +55,7 @@ const val ACCOUNT = "account" const val PASSWORD = "password" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" - const val SERVER_BASE_URL = "http://192.168.153.213:8080" + const val SERVER_BASE_URL = "http://192.168.137.213:8080" + const val AI_SERVER_CONFIG = "aiServerConfig" + const val AI_BASE_URL = "http://192.168.206.248:5000" } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 88db066..4b6dabf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -1,10 +1,13 @@ package com.casic.br.app.view +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.graphics.ImageFormat +import android.graphics.Rect +import android.graphics.YuvImage import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Message +import android.util.Base64 import android.util.DisplayMetrics import android.util.Log import android.view.Surface @@ -13,20 +16,22 @@ import androidx.camera.core.CameraState import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageCapture -import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModelProvider import com.casic.br.app.R import com.casic.br.app.databinding.ActivityStartCheckBinding import com.casic.br.app.extensions.initImmersionBar +import com.casic.br.app.vm.FileUploadViewModel import com.casic.br.app.widgets.CheckResultDialog import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.toBitmap -import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView +import java.io.ByteArrayOutputStream +import java.io.IOException import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -35,7 +40,8 @@ import java.util.concurrent.TimeUnit import kotlin.math.abs -class StartCheckActivity : KotlinBaseActivity(), Handler.Callback { + +class StartCheckActivity : KotlinBaseActivity() { companion object { private const val RATIO_4_3_VALUE = 4.0 / 3.0 @@ -43,6 +49,7 @@ } private val kTag = "StartCheckActivity" + private val context = this private val executor = ThreadPoolExecutor( 16, 16, 0L, TimeUnit.MILLISECONDS, @@ -53,10 +60,19 @@ private lateinit var cameraProviderFuture: ListenableFuture private lateinit var imageCapture: ImageCapture private lateinit var imageAnalysis: ImageAnalysis - private lateinit var weakReferenceHandler: WeakReferenceHandler + private lateinit var fileUploadViewModel: FileUploadViewModel + private var isRecognizing = false override fun initOnCreate(savedInstanceState: Bundle?) { - weakReferenceHandler = WeakReferenceHandler(this) + fileUploadViewModel = ViewModelProvider(this)[FileUploadViewModel::class.java] + fileUploadViewModel.recognizeResult.observe(this) { + if (it.code == 200) { + binding.titleView.setTitle(it.scene) + binding.detectView.updateTargetPosition(it.result) + } + isRecognizing = false + } + // Initialize our background executor cameraExecutor = Executors.newSingleThreadExecutor() cameraProviderFuture = ProcessCameraProvider.getInstance(this) @@ -132,30 +148,65 @@ } @androidx.camera.core.ExperimentalGetImage - private val faceImageAnalyzer = object : ImageAnalysis.Analyzer { - override fun analyze(imageProxy: ImageProxy) { - if (imageProxy.format == ImageFormat.YUV_420_888) { - executor.execute { - val image = imageProxy.image - val bitmap = image?.toBitmap(ImageFormat.YUV_420_888) ?: return@execute - //TODO 上传至后台分析图片 - val message = weakReferenceHandler.obtainMessage() - message.obj = bitmap - message.what = 2024031901 - weakReferenceHandler.sendMessage(message) + private val faceImageAnalyzer = ImageAnalysis.Analyzer { imageProxy -> + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val image = imageProxy.image + image?.apply { + if (!isRecognizing) { + isRecognizing = true - //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 - imageProxy.close() + /** + * CameraX 原始预览Image数据(imageProxy.format == ImageFormat.YUV_420_888)转Bitmap + * */ + val yBuffer = this.planes[0].buffer + val uBuffer = this.planes[1].buffer + val vBuffer = this.planes[2].buffer + + val ySize = yBuffer.remaining() + val uSize = uBuffer.remaining() + val vSize = vBuffer.remaining() + + val nv21 = ByteArray(ySize + uSize + vSize) + yBuffer.get(nv21, 0, ySize) + vBuffer.get(nv21, ySize, vSize) + uBuffer.get(nv21, ySize + vSize, uSize) + + val yuvImage = YuvImage( + nv21, ImageFormat.NV21, this.width, this.height, null + ) + val out = ByteArrayOutputStream() + yuvImage.compressToJpeg( + Rect(0, 0, yuvImage.width, yuvImage.height), 100, out + ) + + val imageBytes = out.toByteArray() + val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + + val base64 = bitmap.rotateImage(90).encodeToBase64() + fileUploadViewModel.getRecognizeResult(context, base64, "建筑消防") + } } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() } } } - override fun handleMessage(msg: Message): Boolean { - if (msg.what == 2024031901) { + private fun Bitmap.encodeToBase64(): String { + try { + val outputStream = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + + outputStream.flush() + outputStream.close() + return Base64.encodeToString(byteArray, Base64.NO_WRAP) + } catch (e: IOException) { + e.printStackTrace() } - return true + return "" } override fun initEvent() { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 36056fe..5eeb511 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -3,7 +3,9 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PartMap import retrofit2.http.QueryMap interface RetrofitService { @@ -62,4 +64,11 @@ @QueryMap limit: Map, @QueryMap offset: Map ): String + + /** + * 获取画面识别结果 + */ + @Multipart + @POST("detect") + suspend fun getRecognizeResult(@PartMap body: MutableMap): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 2ddf452..26b2c8a 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,10 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody + object RetrofitServiceManager { private val api by lazy { @@ -17,6 +19,13 @@ RetrofitFactory.createRetrofit(httpConfig, timeout = 20) } + private val aiApi by lazy { + val httpConfig = SaveKeyValues.getValue( + LocaleConstant.AI_SERVER_CONFIG, LocaleConstant.AI_BASE_URL + ) as String + RetrofitFactory.createRetrofit(httpConfig, timeout = 20) + } + /** * 获取巡查记录 */ @@ -116,4 +125,14 @@ AuthenticationHelper.token, requestBody, limitMap, offsetMap ) } + + /** + * 获取画面识别结果 + * */ + suspend fun getRecognizeResult(base64: String, scene: String): String { + val map: MutableMap = mutableMapOf() + map["img"] = "data:image/png;base64,${base64}".toRequestBody() + map["scene"] = scene.toRequestBody() + return aiApi.getRecognizeResult(map) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt index 949492a..4b82f2d 100644 --- a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt @@ -55,5 +55,7 @@ const val ACCOUNT = "account" const val PASSWORD = "password" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" - const val SERVER_BASE_URL = "http://192.168.153.213:8080" + const val SERVER_BASE_URL = "http://192.168.137.213:8080" + const val AI_SERVER_CONFIG = "aiServerConfig" + const val AI_BASE_URL = "http://192.168.206.248:5000" } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 88db066..4b6dabf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -1,10 +1,13 @@ package com.casic.br.app.view +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.graphics.ImageFormat +import android.graphics.Rect +import android.graphics.YuvImage import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Message +import android.util.Base64 import android.util.DisplayMetrics import android.util.Log import android.view.Surface @@ -13,20 +16,22 @@ import androidx.camera.core.CameraState import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageCapture -import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModelProvider import com.casic.br.app.R import com.casic.br.app.databinding.ActivityStartCheckBinding import com.casic.br.app.extensions.initImmersionBar +import com.casic.br.app.vm.FileUploadViewModel import com.casic.br.app.widgets.CheckResultDialog import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.toBitmap -import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView +import java.io.ByteArrayOutputStream +import java.io.IOException import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -35,7 +40,8 @@ import java.util.concurrent.TimeUnit import kotlin.math.abs -class StartCheckActivity : KotlinBaseActivity(), Handler.Callback { + +class StartCheckActivity : KotlinBaseActivity() { companion object { private const val RATIO_4_3_VALUE = 4.0 / 3.0 @@ -43,6 +49,7 @@ } private val kTag = "StartCheckActivity" + private val context = this private val executor = ThreadPoolExecutor( 16, 16, 0L, TimeUnit.MILLISECONDS, @@ -53,10 +60,19 @@ private lateinit var cameraProviderFuture: ListenableFuture private lateinit var imageCapture: ImageCapture private lateinit var imageAnalysis: ImageAnalysis - private lateinit var weakReferenceHandler: WeakReferenceHandler + private lateinit var fileUploadViewModel: FileUploadViewModel + private var isRecognizing = false override fun initOnCreate(savedInstanceState: Bundle?) { - weakReferenceHandler = WeakReferenceHandler(this) + fileUploadViewModel = ViewModelProvider(this)[FileUploadViewModel::class.java] + fileUploadViewModel.recognizeResult.observe(this) { + if (it.code == 200) { + binding.titleView.setTitle(it.scene) + binding.detectView.updateTargetPosition(it.result) + } + isRecognizing = false + } + // Initialize our background executor cameraExecutor = Executors.newSingleThreadExecutor() cameraProviderFuture = ProcessCameraProvider.getInstance(this) @@ -132,30 +148,65 @@ } @androidx.camera.core.ExperimentalGetImage - private val faceImageAnalyzer = object : ImageAnalysis.Analyzer { - override fun analyze(imageProxy: ImageProxy) { - if (imageProxy.format == ImageFormat.YUV_420_888) { - executor.execute { - val image = imageProxy.image - val bitmap = image?.toBitmap(ImageFormat.YUV_420_888) ?: return@execute - //TODO 上传至后台分析图片 - val message = weakReferenceHandler.obtainMessage() - message.obj = bitmap - message.what = 2024031901 - weakReferenceHandler.sendMessage(message) + private val faceImageAnalyzer = ImageAnalysis.Analyzer { imageProxy -> + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val image = imageProxy.image + image?.apply { + if (!isRecognizing) { + isRecognizing = true - //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 - imageProxy.close() + /** + * CameraX 原始预览Image数据(imageProxy.format == ImageFormat.YUV_420_888)转Bitmap + * */ + val yBuffer = this.planes[0].buffer + val uBuffer = this.planes[1].buffer + val vBuffer = this.planes[2].buffer + + val ySize = yBuffer.remaining() + val uSize = uBuffer.remaining() + val vSize = vBuffer.remaining() + + val nv21 = ByteArray(ySize + uSize + vSize) + yBuffer.get(nv21, 0, ySize) + vBuffer.get(nv21, ySize, vSize) + uBuffer.get(nv21, ySize + vSize, uSize) + + val yuvImage = YuvImage( + nv21, ImageFormat.NV21, this.width, this.height, null + ) + val out = ByteArrayOutputStream() + yuvImage.compressToJpeg( + Rect(0, 0, yuvImage.width, yuvImage.height), 100, out + ) + + val imageBytes = out.toByteArray() + val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + + val base64 = bitmap.rotateImage(90).encodeToBase64() + fileUploadViewModel.getRecognizeResult(context, base64, "建筑消防") + } } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() } } } - override fun handleMessage(msg: Message): Boolean { - if (msg.what == 2024031901) { + private fun Bitmap.encodeToBase64(): String { + try { + val outputStream = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + + outputStream.flush() + outputStream.close() + return Base64.encodeToString(byteArray, Base64.NO_WRAP) + } catch (e: IOException) { + e.printStackTrace() } - return true + return "" } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt b/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt new file mode 100644 index 0000000..29d12ae --- /dev/null +++ b/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt @@ -0,0 +1,43 @@ +package com.casic.br.app.vm + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import com.casic.br.app.extensions.getResponseCode +import com.casic.br.app.model.RecognizeResultModel +import com.casic.br.app.retrofit.RetrofitServiceManager +import com.google.gson.Gson +import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken +import com.pengxh.kt.lite.base.BaseViewModel +import com.pengxh.kt.lite.extensions.launch +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.LoadState + +class FileUploadViewModel : BaseViewModel() { + + private val gson by lazy { Gson() } + val recognizeResult = MutableLiveData() + + fun getRecognizeResult(context: Context, base64: String, scene: String) = launch({ + loadState.value = LoadState.Loading + val response = RetrofitServiceManager.getRecognizeResult(base64, scene) + when (response.getResponseCode()) { + 200 -> { + loadState.value = LoadState.Success + recognizeResult.value = gson.fromJson( + response, object : TypeToken() {}.type + ) + } + + else -> { + val element = JsonParser.parseString(response) + val jsonObject = element.asJsonObject + jsonObject.get("result").asString.show(context) + loadState.value = LoadState.Fail + } + } + }, { + loadState.value = LoadState.Fail + it.localizedMessage?.show(context) + }) +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 36056fe..5eeb511 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -3,7 +3,9 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PartMap import retrofit2.http.QueryMap interface RetrofitService { @@ -62,4 +64,11 @@ @QueryMap limit: Map, @QueryMap offset: Map ): String + + /** + * 获取画面识别结果 + */ + @Multipart + @POST("detect") + suspend fun getRecognizeResult(@PartMap body: MutableMap): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 2ddf452..26b2c8a 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,10 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody + object RetrofitServiceManager { private val api by lazy { @@ -17,6 +19,13 @@ RetrofitFactory.createRetrofit(httpConfig, timeout = 20) } + private val aiApi by lazy { + val httpConfig = SaveKeyValues.getValue( + LocaleConstant.AI_SERVER_CONFIG, LocaleConstant.AI_BASE_URL + ) as String + RetrofitFactory.createRetrofit(httpConfig, timeout = 20) + } + /** * 获取巡查记录 */ @@ -116,4 +125,14 @@ AuthenticationHelper.token, requestBody, limitMap, offsetMap ) } + + /** + * 获取画面识别结果 + * */ + suspend fun getRecognizeResult(base64: String, scene: String): String { + val map: MutableMap = mutableMapOf() + map["img"] = "data:image/png;base64,${base64}".toRequestBody() + map["scene"] = scene.toRequestBody() + return aiApi.getRecognizeResult(map) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt index 949492a..4b82f2d 100644 --- a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt @@ -55,5 +55,7 @@ const val ACCOUNT = "account" const val PASSWORD = "password" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" - const val SERVER_BASE_URL = "http://192.168.153.213:8080" + const val SERVER_BASE_URL = "http://192.168.137.213:8080" + const val AI_SERVER_CONFIG = "aiServerConfig" + const val AI_BASE_URL = "http://192.168.206.248:5000" } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 88db066..4b6dabf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -1,10 +1,13 @@ package com.casic.br.app.view +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.graphics.ImageFormat +import android.graphics.Rect +import android.graphics.YuvImage import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Message +import android.util.Base64 import android.util.DisplayMetrics import android.util.Log import android.view.Surface @@ -13,20 +16,22 @@ import androidx.camera.core.CameraState import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageCapture -import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModelProvider import com.casic.br.app.R import com.casic.br.app.databinding.ActivityStartCheckBinding import com.casic.br.app.extensions.initImmersionBar +import com.casic.br.app.vm.FileUploadViewModel import com.casic.br.app.widgets.CheckResultDialog import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.toBitmap -import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView +import java.io.ByteArrayOutputStream +import java.io.IOException import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -35,7 +40,8 @@ import java.util.concurrent.TimeUnit import kotlin.math.abs -class StartCheckActivity : KotlinBaseActivity(), Handler.Callback { + +class StartCheckActivity : KotlinBaseActivity() { companion object { private const val RATIO_4_3_VALUE = 4.0 / 3.0 @@ -43,6 +49,7 @@ } private val kTag = "StartCheckActivity" + private val context = this private val executor = ThreadPoolExecutor( 16, 16, 0L, TimeUnit.MILLISECONDS, @@ -53,10 +60,19 @@ private lateinit var cameraProviderFuture: ListenableFuture private lateinit var imageCapture: ImageCapture private lateinit var imageAnalysis: ImageAnalysis - private lateinit var weakReferenceHandler: WeakReferenceHandler + private lateinit var fileUploadViewModel: FileUploadViewModel + private var isRecognizing = false override fun initOnCreate(savedInstanceState: Bundle?) { - weakReferenceHandler = WeakReferenceHandler(this) + fileUploadViewModel = ViewModelProvider(this)[FileUploadViewModel::class.java] + fileUploadViewModel.recognizeResult.observe(this) { + if (it.code == 200) { + binding.titleView.setTitle(it.scene) + binding.detectView.updateTargetPosition(it.result) + } + isRecognizing = false + } + // Initialize our background executor cameraExecutor = Executors.newSingleThreadExecutor() cameraProviderFuture = ProcessCameraProvider.getInstance(this) @@ -132,30 +148,65 @@ } @androidx.camera.core.ExperimentalGetImage - private val faceImageAnalyzer = object : ImageAnalysis.Analyzer { - override fun analyze(imageProxy: ImageProxy) { - if (imageProxy.format == ImageFormat.YUV_420_888) { - executor.execute { - val image = imageProxy.image - val bitmap = image?.toBitmap(ImageFormat.YUV_420_888) ?: return@execute - //TODO 上传至后台分析图片 - val message = weakReferenceHandler.obtainMessage() - message.obj = bitmap - message.what = 2024031901 - weakReferenceHandler.sendMessage(message) + private val faceImageAnalyzer = ImageAnalysis.Analyzer { imageProxy -> + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val image = imageProxy.image + image?.apply { + if (!isRecognizing) { + isRecognizing = true - //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 - imageProxy.close() + /** + * CameraX 原始预览Image数据(imageProxy.format == ImageFormat.YUV_420_888)转Bitmap + * */ + val yBuffer = this.planes[0].buffer + val uBuffer = this.planes[1].buffer + val vBuffer = this.planes[2].buffer + + val ySize = yBuffer.remaining() + val uSize = uBuffer.remaining() + val vSize = vBuffer.remaining() + + val nv21 = ByteArray(ySize + uSize + vSize) + yBuffer.get(nv21, 0, ySize) + vBuffer.get(nv21, ySize, vSize) + uBuffer.get(nv21, ySize + vSize, uSize) + + val yuvImage = YuvImage( + nv21, ImageFormat.NV21, this.width, this.height, null + ) + val out = ByteArrayOutputStream() + yuvImage.compressToJpeg( + Rect(0, 0, yuvImage.width, yuvImage.height), 100, out + ) + + val imageBytes = out.toByteArray() + val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + + val base64 = bitmap.rotateImage(90).encodeToBase64() + fileUploadViewModel.getRecognizeResult(context, base64, "建筑消防") + } } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() } } } - override fun handleMessage(msg: Message): Boolean { - if (msg.what == 2024031901) { + private fun Bitmap.encodeToBase64(): String { + try { + val outputStream = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + + outputStream.flush() + outputStream.close() + return Base64.encodeToString(byteArray, Base64.NO_WRAP) + } catch (e: IOException) { + e.printStackTrace() } - return true + return "" } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt b/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt new file mode 100644 index 0000000..29d12ae --- /dev/null +++ b/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt @@ -0,0 +1,43 @@ +package com.casic.br.app.vm + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import com.casic.br.app.extensions.getResponseCode +import com.casic.br.app.model.RecognizeResultModel +import com.casic.br.app.retrofit.RetrofitServiceManager +import com.google.gson.Gson +import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken +import com.pengxh.kt.lite.base.BaseViewModel +import com.pengxh.kt.lite.extensions.launch +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.LoadState + +class FileUploadViewModel : BaseViewModel() { + + private val gson by lazy { Gson() } + val recognizeResult = MutableLiveData() + + fun getRecognizeResult(context: Context, base64: String, scene: String) = launch({ + loadState.value = LoadState.Loading + val response = RetrofitServiceManager.getRecognizeResult(base64, scene) + when (response.getResponseCode()) { + 200 -> { + loadState.value = LoadState.Success + recognizeResult.value = gson.fromJson( + response, object : TypeToken() {}.type + ) + } + + else -> { + val element = JsonParser.parseString(response) + val jsonObject = element.asJsonObject + jsonObject.get("result").asString.show(context) + loadState.value = LoadState.Fail + } + } + }, { + loadState.value = LoadState.Fail + it.localizedMessage?.show(context) + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/widgets/AITargetDetectView.kt b/app/src/main/java/com/casic/br/app/widgets/AITargetDetectView.kt new file mode 100644 index 0000000..0745937 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/widgets/AITargetDetectView.kt @@ -0,0 +1,72 @@ +package com.casic.br.app.widgets + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.text.TextPaint +import android.util.AttributeSet +import android.view.View +import com.casic.br.app.model.RecognizeResultModel +import com.pengxh.kt.lite.extensions.dp2px +import com.pengxh.kt.lite.extensions.getScreenHeight +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getStatusBarHeight +import com.pengxh.kt.lite.extensions.sp2px + +class AITargetDetectView constructor(context: Context, attrs: AttributeSet) : View(context, attrs) { + + private val kTag = "FaceDetectView" + private val textPaint by lazy { TextPaint() } + private val borderPaint by lazy { Paint() } + private val rect by lazy { Rect() } + private var viewWidth = 0 + private var viewHeight = 0 + private var resultModels: MutableList? = null + + init { + textPaint.color = Color.RED + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint.textSize = 14f.sp2px(context) + + borderPaint.color = Color.RED + borderPaint.style = Paint.Style.STROKE + borderPaint.strokeWidth = 2f.dp2px(context) //设置线宽 + borderPaint.isAntiAlias = true + + viewWidth = context.getScreenWidth() + //屏幕整体高度-状态栏高度-标题栏高度 + viewHeight = context.getScreenHeight() - context.getStatusBarHeight() - 44.dp2px(context) + } + + fun updateTargetPosition(resultModels: MutableList?) { + this.resultModels = resultModels + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + resultModels?.forEach { + //画文字 + val textLength = textPaint.measureText(it.label) + canvas.drawText( + it.label, + (it.box[0] * viewWidth + textLength / 2).toFloat(), + (it.box[1] * viewHeight - 10).toFloat(), + textPaint + ) + + //画框 + val box = it.box + rect.set( + (box[0] * viewWidth).toInt(), + (box[1] * viewHeight).toInt(), + (box[2] * viewWidth).toInt(), + (box[3] * viewHeight).toInt() + ) + canvas.drawRect(rect, borderPaint) + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4015c8c..d1c273b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SafetyAuxiliary" diff --git a/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java new file mode 100644 index 0000000..1d52d3f --- /dev/null +++ b/app/src/main/java/com/casic/br/app/model/RecognizeResultModel.java @@ -0,0 +1,91 @@ +package com.casic.br.app.model; + +import java.util.List; + +public class RecognizeResultModel { + + private String scene; + private String inferneceStart; + private String inferneceEnd; + private String inferneceInterval; + private List result; + private Integer code; + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getInferneceStart() { + return inferneceStart; + } + + public void setInferneceStart(String inferneceStart) { + this.inferneceStart = inferneceStart; + } + + public String getInferneceEnd() { + return inferneceEnd; + } + + public void setInferneceEnd(String inferneceEnd) { + this.inferneceEnd = inferneceEnd; + } + + public String getInferneceInterval() { + return inferneceInterval; + } + + public void setInferneceInterval(String inferneceInterval) { + this.inferneceInterval = inferneceInterval; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public static class ResultModel { + private String label; + private Double conf; + private List box; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Double getConf() { + return conf; + } + + public void setConf(Double conf) { + this.conf = conf; + } + + public List getBox() { + return box; + } + + public void setBox(List box) { + this.box = box; + } + } +} diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 36056fe..5eeb511 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -3,7 +3,9 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PartMap import retrofit2.http.QueryMap interface RetrofitService { @@ -62,4 +64,11 @@ @QueryMap limit: Map, @QueryMap offset: Map ): String + + /** + * 获取画面识别结果 + */ + @Multipart + @POST("detect") + suspend fun getRecognizeResult(@PartMap body: MutableMap): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 2ddf452..26b2c8a 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,10 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody + object RetrofitServiceManager { private val api by lazy { @@ -17,6 +19,13 @@ RetrofitFactory.createRetrofit(httpConfig, timeout = 20) } + private val aiApi by lazy { + val httpConfig = SaveKeyValues.getValue( + LocaleConstant.AI_SERVER_CONFIG, LocaleConstant.AI_BASE_URL + ) as String + RetrofitFactory.createRetrofit(httpConfig, timeout = 20) + } + /** * 获取巡查记录 */ @@ -116,4 +125,14 @@ AuthenticationHelper.token, requestBody, limitMap, offsetMap ) } + + /** + * 获取画面识别结果 + * */ + suspend fun getRecognizeResult(base64: String, scene: String): String { + val map: MutableMap = mutableMapOf() + map["img"] = "data:image/png;base64,${base64}".toRequestBody() + map["scene"] = scene.toRequestBody() + return aiApi.getRecognizeResult(map) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt index 949492a..4b82f2d 100644 --- a/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt +++ b/app/src/main/java/com/casic/br/app/utils/LocaleConstant.kt @@ -55,5 +55,7 @@ const val ACCOUNT = "account" const val PASSWORD = "password" const val DEFAULT_SERVER_CONFIG = "defaultServerConfig" - const val SERVER_BASE_URL = "http://192.168.153.213:8080" + const val SERVER_BASE_URL = "http://192.168.137.213:8080" + const val AI_SERVER_CONFIG = "aiServerConfig" + const val AI_BASE_URL = "http://192.168.206.248:5000" } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 88db066..4b6dabf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -1,10 +1,13 @@ package com.casic.br.app.view +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.graphics.ImageFormat +import android.graphics.Rect +import android.graphics.YuvImage import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Message +import android.util.Base64 import android.util.DisplayMetrics import android.util.Log import android.view.Surface @@ -13,20 +16,22 @@ import androidx.camera.core.CameraState import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageCapture -import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModelProvider import com.casic.br.app.R import com.casic.br.app.databinding.ActivityStartCheckBinding import com.casic.br.app.extensions.initImmersionBar +import com.casic.br.app.vm.FileUploadViewModel import com.casic.br.app.widgets.CheckResultDialog import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity -import com.pengxh.kt.lite.extensions.toBitmap -import com.pengxh.kt.lite.utils.WeakReferenceHandler +import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView +import java.io.ByteArrayOutputStream +import java.io.IOException import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -35,7 +40,8 @@ import java.util.concurrent.TimeUnit import kotlin.math.abs -class StartCheckActivity : KotlinBaseActivity(), Handler.Callback { + +class StartCheckActivity : KotlinBaseActivity() { companion object { private const val RATIO_4_3_VALUE = 4.0 / 3.0 @@ -43,6 +49,7 @@ } private val kTag = "StartCheckActivity" + private val context = this private val executor = ThreadPoolExecutor( 16, 16, 0L, TimeUnit.MILLISECONDS, @@ -53,10 +60,19 @@ private lateinit var cameraProviderFuture: ListenableFuture private lateinit var imageCapture: ImageCapture private lateinit var imageAnalysis: ImageAnalysis - private lateinit var weakReferenceHandler: WeakReferenceHandler + private lateinit var fileUploadViewModel: FileUploadViewModel + private var isRecognizing = false override fun initOnCreate(savedInstanceState: Bundle?) { - weakReferenceHandler = WeakReferenceHandler(this) + fileUploadViewModel = ViewModelProvider(this)[FileUploadViewModel::class.java] + fileUploadViewModel.recognizeResult.observe(this) { + if (it.code == 200) { + binding.titleView.setTitle(it.scene) + binding.detectView.updateTargetPosition(it.result) + } + isRecognizing = false + } + // Initialize our background executor cameraExecutor = Executors.newSingleThreadExecutor() cameraProviderFuture = ProcessCameraProvider.getInstance(this) @@ -132,30 +148,65 @@ } @androidx.camera.core.ExperimentalGetImage - private val faceImageAnalyzer = object : ImageAnalysis.Analyzer { - override fun analyze(imageProxy: ImageProxy) { - if (imageProxy.format == ImageFormat.YUV_420_888) { - executor.execute { - val image = imageProxy.image - val bitmap = image?.toBitmap(ImageFormat.YUV_420_888) ?: return@execute - //TODO 上传至后台分析图片 - val message = weakReferenceHandler.obtainMessage() - message.obj = bitmap - message.what = 2024031901 - weakReferenceHandler.sendMessage(message) + private val faceImageAnalyzer = ImageAnalysis.Analyzer { imageProxy -> + if (imageProxy.format == ImageFormat.YUV_420_888) { + executor.execute { + val image = imageProxy.image + image?.apply { + if (!isRecognizing) { + isRecognizing = true - //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 - imageProxy.close() + /** + * CameraX 原始预览Image数据(imageProxy.format == ImageFormat.YUV_420_888)转Bitmap + * */ + val yBuffer = this.planes[0].buffer + val uBuffer = this.planes[1].buffer + val vBuffer = this.planes[2].buffer + + val ySize = yBuffer.remaining() + val uSize = uBuffer.remaining() + val vSize = vBuffer.remaining() + + val nv21 = ByteArray(ySize + uSize + vSize) + yBuffer.get(nv21, 0, ySize) + vBuffer.get(nv21, ySize, vSize) + uBuffer.get(nv21, ySize + vSize, uSize) + + val yuvImage = YuvImage( + nv21, ImageFormat.NV21, this.width, this.height, null + ) + val out = ByteArrayOutputStream() + yuvImage.compressToJpeg( + Rect(0, 0, yuvImage.width, yuvImage.height), 100, out + ) + + val imageBytes = out.toByteArray() + val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + + val base64 = bitmap.rotateImage(90).encodeToBase64() + fileUploadViewModel.getRecognizeResult(context, base64, "建筑消防") + } } + //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 + imageProxy.close() } } } - override fun handleMessage(msg: Message): Boolean { - if (msg.what == 2024031901) { + private fun Bitmap.encodeToBase64(): String { + try { + val outputStream = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + + outputStream.flush() + outputStream.close() + return Base64.encodeToString(byteArray, Base64.NO_WRAP) + } catch (e: IOException) { + e.printStackTrace() } - return true + return "" } override fun initEvent() { diff --git a/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt b/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt new file mode 100644 index 0000000..29d12ae --- /dev/null +++ b/app/src/main/java/com/casic/br/app/vm/FileUploadViewModel.kt @@ -0,0 +1,43 @@ +package com.casic.br.app.vm + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import com.casic.br.app.extensions.getResponseCode +import com.casic.br.app.model.RecognizeResultModel +import com.casic.br.app.retrofit.RetrofitServiceManager +import com.google.gson.Gson +import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken +import com.pengxh.kt.lite.base.BaseViewModel +import com.pengxh.kt.lite.extensions.launch +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.LoadState + +class FileUploadViewModel : BaseViewModel() { + + private val gson by lazy { Gson() } + val recognizeResult = MutableLiveData() + + fun getRecognizeResult(context: Context, base64: String, scene: String) = launch({ + loadState.value = LoadState.Loading + val response = RetrofitServiceManager.getRecognizeResult(base64, scene) + when (response.getResponseCode()) { + 200 -> { + loadState.value = LoadState.Success + recognizeResult.value = gson.fromJson( + response, object : TypeToken() {}.type + ) + } + + else -> { + val element = JsonParser.parseString(response) + val jsonObject = element.asJsonObject + jsonObject.get("result").asString.show(context) + loadState.value = LoadState.Fail + } + } + }, { + loadState.value = LoadState.Fail + it.localizedMessage?.show(context) + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/widgets/AITargetDetectView.kt b/app/src/main/java/com/casic/br/app/widgets/AITargetDetectView.kt new file mode 100644 index 0000000..0745937 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/widgets/AITargetDetectView.kt @@ -0,0 +1,72 @@ +package com.casic.br.app.widgets + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.text.TextPaint +import android.util.AttributeSet +import android.view.View +import com.casic.br.app.model.RecognizeResultModel +import com.pengxh.kt.lite.extensions.dp2px +import com.pengxh.kt.lite.extensions.getScreenHeight +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getStatusBarHeight +import com.pengxh.kt.lite.extensions.sp2px + +class AITargetDetectView constructor(context: Context, attrs: AttributeSet) : View(context, attrs) { + + private val kTag = "FaceDetectView" + private val textPaint by lazy { TextPaint() } + private val borderPaint by lazy { Paint() } + private val rect by lazy { Rect() } + private var viewWidth = 0 + private var viewHeight = 0 + private var resultModels: MutableList? = null + + init { + textPaint.color = Color.RED + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint.textSize = 14f.sp2px(context) + + borderPaint.color = Color.RED + borderPaint.style = Paint.Style.STROKE + borderPaint.strokeWidth = 2f.dp2px(context) //设置线宽 + borderPaint.isAntiAlias = true + + viewWidth = context.getScreenWidth() + //屏幕整体高度-状态栏高度-标题栏高度 + viewHeight = context.getScreenHeight() - context.getStatusBarHeight() - 44.dp2px(context) + } + + fun updateTargetPosition(resultModels: MutableList?) { + this.resultModels = resultModels + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + resultModels?.forEach { + //画文字 + val textLength = textPaint.measureText(it.label) + canvas.drawText( + it.label, + (it.box[0] * viewWidth + textLength / 2).toFloat(), + (it.box[1] * viewHeight - 10).toFloat(), + textPaint + ) + + //画框 + val box = it.box + rect.set( + (box[0] * viewWidth).toInt(), + (box[1] * viewHeight).toInt(), + (box[2] * viewWidth).toInt(), + (box[3] * viewHeight).toInt() + ) + canvas.drawRect(rect, borderPaint) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_start_check.xml b/app/src/main/res/layout/activity_start_check.xml index 0f1a45d..0bb99b8 100644 --- a/app/src/main/res/layout/activity_start_check.xml +++ b/app/src/main/res/layout/activity_start_check.xml @@ -30,6 +30,11 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + +