Skip to content

Commit 514e809

Browse files
committed
first version of object detection
1 parent aaa6ea5 commit 514e809

18 files changed

Lines changed: 605 additions & 36 deletions

app/build.gradle

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@ plugins {
33
id 'org.jetbrains.kotlin.android'
44
id 'kotlin-parcelize'
55
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
6-
76
}
87

98
android {
109
namespace 'com.nlinterface'
11-
compileSdk 33
10+
compileSdk 34
1211

1312
defaultConfig {
1413
applicationId "com.nlinterface"
1514
minSdk 24
1615
targetSdk 33
1716
versionCode 1
18-
versionName '0.0.1'
17+
versionName '0.0.3'
1918

2019
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2120
}
@@ -40,25 +39,40 @@ android {
4039

4140
dependencies {
4241

43-
implementation 'androidx.core:core-ktx:1.7.0'
42+
implementation 'androidx.core:core-ktx:1.12.0'
4443
implementation 'androidx.appcompat:appcompat:1.6.1'
45-
implementation 'com.google.android.material:material:1.5.0-alpha04'
44+
implementation 'com.google.android.material:material:1.11.0'
4645
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
47-
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
48-
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
46+
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
47+
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
4948
testImplementation 'junit:junit:4.13.2'
5049
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
5150
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
5251

5352
// Set up the location feature dependency
54-
implementation 'com.google.android.gms:play-services-location:21.0.1'
53+
implementation 'com.google.android.gms:play-services-location:21.1.0'
5554

5655
// GSON dependency
57-
implementation 'com.google.code.gson:gson:2.8.5'
56+
implementation 'com.google.code.gson:gson:2.10'
5857

5958
// Places API
60-
implementation 'com.google.android.libraries.places:places:3.2.0'
59+
implementation 'com.google.android.libraries.places:places:3.3.0'
60+
61+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
62+
63+
// Camera + Tensorflow
64+
implementation("androidx.camera:camera-core:1.3.1")
65+
implementation("androidx.camera:camera-camera2:1.3.1")
66+
implementation("androidx.camera:camera-lifecycle:1.3.1")
67+
implementation("androidx.camera:camera-video:1.3.1")
68+
69+
implementation("androidx.camera:camera-view:1.3.1")
70+
implementation("androidx.camera:camera-extensions:1.3.1")
6171

62-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
72+
implementation("org.tensorflow:tensorflow-lite-task-vision:0.4.0")
73+
implementation("org.tensorflow:tensorflow-lite-gpu-delegate-plugin:0.4.0")
74+
implementation("org.tensorflow:tensorflow-lite-gpu:2.9.0")
6375

76+
// Camera UI
77+
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
6478
}

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
88
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
9-
9+
<uses-feature
10+
android:name="android.hardware.camera"
11+
android:required="false" />
12+
<uses-permission android:name="android.permission.CAMERA" />
1013
<application
1114
android:allowBackup="true"
1215
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -43,6 +46,8 @@
4346
android:name=".activities.PlaceDetailsActivity" />
4447
<activity
4548
android:name=".activities.SettingsActivity"/>
49+
<activity
50+
android:name=".activities.ClassificationActivity"/>
4651

4752
</application>
4853

3.99 MB
Binary file not shown.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.nlinterface.activities
2+
3+
import android.content.pm.PackageManager
4+
import android.os.Bundle
5+
import androidx.appcompat.app.AppCompatActivity
6+
import androidx.core.content.ContextCompat
7+
import android.Manifest
8+
import android.content.ContentValues.TAG
9+
import android.util.Log
10+
import androidx.camera.core.AspectRatio
11+
import androidx.camera.core.CameraSelector
12+
import androidx.camera.core.ImageAnalysis
13+
import androidx.camera.core.Preview
14+
import androidx.camera.lifecycle.ProcessCameraProvider
15+
import androidx.core.app.ActivityCompat
16+
import com.nlinterface.databinding.ActivityClassificationBinding
17+
import com.nlinterface.databinding.FragmentCameraBinding
18+
import com.nlinterface.utility.ObjectDetectorHelper
19+
20+
class ClassificationActivity: AppCompatActivity() {
21+
22+
private lateinit var viewBinding: ActivityClassificationBinding
23+
override fun onCreate(savedInstanceState: Bundle?) {
24+
super.onCreate(savedInstanceState)
25+
if(!hasCameraPermission()) {
26+
ActivityCompat.requestPermissions(
27+
this, arrayOf(Manifest.permission.CAMERA), 0
28+
)
29+
}
30+
31+
viewBinding = ActivityClassificationBinding.inflate(layoutInflater)
32+
setContentView(viewBinding.root)
33+
}
34+
35+
private fun hasCameraPermission() = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
36+
37+
38+
}

app/src/main/java/com/nlinterface/activities/MainActivity.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ class MainActivity : AppCompatActivity() {
211211
navToActivity(this, ActivityType.PLACEDETAILS)
212212
}
213213

214+
// set up button to navigate to ClassificationActivity
215+
val classificationButton: Button = findViewById<View>(R.id.classification_bt) as Button
216+
classificationButton.setOnClickListener { _ ->
217+
navToActivity(this, ActivityType.CLASSIFICATION)
218+
}
219+
214220
// set up button to navigate to SettingsActivity
215221
val settingsActivityButton: Button = findViewById<View>(R.id.settings_bt) as Button
216222
settingsActivityButton.setOnClickListener { _ ->
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package com.nlinterface.fragments
2+
3+
import android.content.res.Configuration
4+
import android.graphics.Bitmap
5+
import android.os.Bundle
6+
import android.util.Log
7+
import android.view.LayoutInflater
8+
import android.view.View
9+
import android.view.ViewGroup
10+
import android.widget.AdapterView
11+
import android.widget.Toast
12+
import androidx.camera.core.AspectRatio
13+
import androidx.camera.core.Camera
14+
import androidx.camera.core.CameraSelector
15+
import androidx.camera.core.ImageAnalysis
16+
import androidx.camera.core.ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888
17+
import androidx.camera.core.ImageProxy
18+
import androidx.camera.core.Preview
19+
import androidx.camera.lifecycle.ProcessCameraProvider
20+
import androidx.core.content.ContextCompat
21+
import androidx.fragment.app.Fragment
22+
import com.nlinterface.databinding.FragmentCameraBinding
23+
import com.nlinterface.interfaces.DetectorListener
24+
import com.nlinterface.utility.ObjectDetectorHelper
25+
import org.tensorflow.lite.task.vision.detector.Detection
26+
import java.util.LinkedList
27+
import java.util.concurrent.ExecutorService
28+
import java.util.concurrent.Executors
29+
class CameraFragment : Fragment(), DetectorListener{
30+
31+
private val TAG = "ObjectDetection"
32+
33+
private var _fragmentCameraBinding: FragmentCameraBinding? = null
34+
35+
private val fragmentCameraBinding
36+
get() = _fragmentCameraBinding!!
37+
38+
private lateinit var objectDetectorHelper: ObjectDetectorHelper
39+
private lateinit var bitmapBuffer: Bitmap
40+
private var preview: Preview? = null
41+
private var imageAnalyzer: ImageAnalysis? = null
42+
private var camera: Camera? = null
43+
private var cameraProvider: ProcessCameraProvider? = null
44+
45+
/** Blocking camera operations are performed using this executor */
46+
private lateinit var cameraExecutor: ExecutorService
47+
48+
override fun onDestroyView() {
49+
_fragmentCameraBinding = null
50+
super.onDestroyView()
51+
52+
// Shut down our background executor
53+
cameraExecutor.shutdown()
54+
}
55+
56+
override fun onCreateView(
57+
inflater: LayoutInflater,
58+
container: ViewGroup?,
59+
savedInstanceState: Bundle?
60+
): View {
61+
_fragmentCameraBinding = FragmentCameraBinding.inflate(inflater, container, false)
62+
63+
return fragmentCameraBinding.root
64+
}
65+
66+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
67+
super.onViewCreated(view, savedInstanceState)
68+
69+
objectDetectorHelper = ObjectDetectorHelper(
70+
context = requireContext(),
71+
objectDetectorListener = this)
72+
73+
// Initialize our background executor
74+
cameraExecutor = Executors.newSingleThreadExecutor()
75+
76+
// Wait for the views to be properly laid out
77+
fragmentCameraBinding.viewFinder.post {
78+
// Set up the camera and its use cases
79+
setUpCamera()
80+
}
81+
82+
}
83+
84+
// Initialize CameraX, and prepare to bind the camera use cases
85+
private fun setUpCamera() {
86+
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
87+
cameraProviderFuture.addListener(
88+
{
89+
// CameraProvider
90+
cameraProvider = cameraProviderFuture.get()
91+
92+
// Build and bind the camera use cases
93+
bindCameraUseCases()
94+
},
95+
ContextCompat.getMainExecutor(requireContext())
96+
)
97+
}
98+
99+
// Declare and bind preview, capture and analysis use cases
100+
private fun bindCameraUseCases() {
101+
102+
// CameraProvider
103+
val cameraProvider =
104+
cameraProvider ?: throw IllegalStateException("Camera initialization failed.")
105+
106+
// CameraSelector - makes assumption that we're only using the back camera
107+
val cameraSelector =
108+
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
109+
110+
// Preview. Only using the 4:3 ratio because this is the closest to our models
111+
preview =
112+
Preview.Builder()
113+
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
114+
.setTargetRotation(fragmentCameraBinding.viewFinder.display.rotation)
115+
.build()
116+
117+
// ImageAnalysis. Using RGBA 8888 to match how our models work
118+
imageAnalyzer =
119+
ImageAnalysis.Builder()
120+
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
121+
.setTargetRotation(fragmentCameraBinding.viewFinder.display.rotation)
122+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
123+
.setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888)
124+
.build()
125+
// The analyzer can then be assigned to the instance
126+
.also {
127+
it.setAnalyzer(cameraExecutor) { image ->
128+
if (!::bitmapBuffer.isInitialized) {
129+
// The image rotation and RGB image buffer are initialized only once
130+
// the analyzer has started running
131+
bitmapBuffer = Bitmap.createBitmap(
132+
image.width,
133+
image.height,
134+
Bitmap.Config.ARGB_8888
135+
)
136+
}
137+
138+
detectObjects(image)
139+
}
140+
}
141+
142+
// Must unbind the use-cases before rebinding them
143+
cameraProvider.unbindAll()
144+
145+
try {
146+
// A variable number of use-cases can be passed here -
147+
// camera provides access to CameraControl & CameraInfo
148+
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer)
149+
150+
// Attach the viewfinder's surface provider to preview use case
151+
preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider)
152+
} catch (exc: Exception) {
153+
Log.e(TAG, "Use case binding failed", exc)
154+
}
155+
}
156+
157+
private fun detectObjects(image: ImageProxy) {
158+
// Copy out RGB bits to the shared bitmap buffer
159+
image.use { bitmapBuffer.copyPixelsFromBuffer(image.planes[0].buffer) }
160+
161+
val imageRotation = image.imageInfo.rotationDegrees
162+
// Pass Bitmap and rotation to the object detector helper for processing and detection
163+
objectDetectorHelper.detect(bitmapBuffer, imageRotation)
164+
}
165+
166+
override fun onConfigurationChanged(newConfig: Configuration) {
167+
super.onConfigurationChanged(newConfig)
168+
imageAnalyzer?.targetRotation = fragmentCameraBinding.viewFinder.display.rotation
169+
}
170+
171+
// Update UI after objects have been detected. Extracts original image height/width
172+
// to scale and place bounding boxes properly through OverlayView
173+
override fun onResults(
174+
results: MutableList<Detection>?,
175+
inferenceTime: Long,
176+
imageHeight: Int,
177+
imageWidth: Int
178+
) {
179+
activity?.runOnUiThread {
180+
// Pass necessary information to OverlayView for drawing on the canvas
181+
fragmentCameraBinding.overlay.setResults(
182+
results ?: LinkedList<Detection>(),
183+
imageHeight,
184+
imageWidth
185+
)
186+
187+
// Force a redraw
188+
fragmentCameraBinding.overlay.invalidate()
189+
}
190+
}
191+
192+
override fun onError(error: String) {
193+
activity?.runOnUiThread {
194+
Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show()
195+
}
196+
}
197+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.nlinterface.interfaces
2+
3+
import org.tensorflow.lite.task.vision.detector.Detection
4+
5+
interface DetectorListener {
6+
fun onError(error: String)
7+
fun onResults(results: MutableList<Detection>?, inferenceTime: Long, imageHeight: Int, imageWidth: Int)
8+
}

0 commit comments

Comments
 (0)