├── .gitignore
├── README.md
├── build.gradle
├── camera
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── de
│ │ └── crysxd
│ │ └── cameraXTracker
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── de
│ │ │ └── crysxd
│ │ │ └── cameraXTracker
│ │ │ ├── AutoPreviewBuilder.kt
│ │ │ ├── CameraFragment.kt
│ │ │ ├── CameraPermissionHelper.kt
│ │ │ ├── FrontFacingCameraFragment.kt
│ │ │ ├── ThreadedImageAnalyzer.kt
│ │ │ └── ar
│ │ │ ├── ArObject.kt
│ │ │ ├── ArObjectTracker.kt
│ │ │ ├── ArOverlay.kt
│ │ │ ├── ArOverlayView.kt
│ │ │ ├── BoundingBoxArOverlay.kt
│ │ │ ├── PathInterpolator.kt
│ │ │ ├── PositionTranslator.kt
│ │ │ └── ViewArOverlay.kt
│ └── res
│ │ ├── layout
│ │ └── fragment_camera.xml
│ │ └── values
│ │ ├── dimens.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── de
│ └── crysxd
│ └── cameraXTracker
│ └── ExampleUnitTest.java
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About
2 | A small library allowing you to analyze images and track objects on interpolated paths.
3 |
4 | See this Medium article for more details: https://medium.com/@cwurthner/object-detection-and-tracking-with-firebase-ml-kit-and-camerax-ml-product-search-part-3-8bd138257101
5 | See this sample app: https://github.com/crysxd/Object-Tracking-Demo/
6 | See this youtube video of the sample app: https://youtu.be/ME9iE0CYHeY
7 |
8 | # How to use
9 |
10 | Add this in module's `build.gradle`
11 |
12 | allprojects {
13 | repositories {
14 | maven { url 'https://jitpack.io' }
15 | }
16 | }
17 |
18 | Add this is app's `build.gralde`
19 |
20 | implementation 'com.github.crysxd:CameraX-Object-Tracking:latest-version'
21 |
22 | Add a instance of `de.crysxd.cameraXTracker.CameraFragment` to your layout:
23 |
24 |
29 |
30 | `CameraFragment` will handle the entire camera for you and display a preview image. In your `Activity` or `Fragment` use this code to set eveything up:
31 |
32 | private lateinit var imageAnalyzer: MyImageAnalyzer
33 |
34 | private val camera
35 | get() = supportFragmentManager.findFragmentById(R.id.cameraFragment) as CameraFragment
36 |
37 | override fun onCreate(savedInstanceState: Bundle?) {
38 | super.onCreate(savedInstanceState)
39 | setContentView(R.layout.activity_main)
40 |
41 | val boundingBoxArOverlay = BoundingBoxArOverlay(this, BuildConfig.DEBUG)
42 | imageAnalyzer = ViewModelProviders.of(this).get(MyImageAnalyzer::class.java)
43 |
44 | camera.imageAnalyzer = imageAnalyzer
45 | camera.arOverlayView.observe(camera, Observer {
46 | it.doOnLayout { view ->
47 | imageAnalyzer.arObjectTracker
48 | .pipe(PositionTranslator(view.width, view.height))
49 | .pipe(PathInterpolator())
50 | .addTrackingListener(boundingBoxArOverlay)
51 | }
52 |
53 | it.add(boundingBoxArOverlay)
54 | })
55 | }
56 |
57 | And implement `ThreadedImageAnalyzer` like this:
58 |
59 | class MyImageAnalyzer : ViewModel(), ThreadedImageAnalyzer {
60 |
61 | val arObjectTracker = ArObjectTracker()
62 | private val uiHandler = Handler(Looper.getMainLooper())
63 | private val handlerThread = HandlerThread("MyImageAnalyzer").apply { start() }
64 |
65 | override fun getHandler() = Handler(handlerThread.looper)
66 |
67 | override fun analyze(image: ImageProxy, rotationDegrees: Int) {
68 | val imageSize = Size(image.width, image.height)
69 |
70 | // Fancy computation with image here
71 |
72 | uiHandler.post {
73 | arObjectTracker.processObject(
74 | if (fancyComputationWasSuccessfull) {
75 | ArObject(
76 | trackingId = o.trackingId ?: -1 /* An ID of the tracked object, e.g. from Firebase ML Kit*/,
77 | boundingBox = o.boundingBox.toRectF() /* The bounding box */,
78 | sourceSize = imageSize /* See above, the size of the input image */,
79 | sourceRotationDegrees = rotationDegrees /* See above, the roation of the input image */
80 | )
81 | } else {
82 | null
83 | }
84 | )
85 | }
86 | }
87 | }
88 |
89 | # Use front facing camera
90 |
91 | Use `FrontFacingCameraFragment` instead:
92 |
93 |
98 |
99 | Tell PositionTranslator that we use the front facing camera:
100 |
101 | .pipe(PositionTranslator(view.width, view.height))
102 |
103 | # Custom image sizes and option
104 |
105 | You can subclass `CameraFragment` and override `onCreateAnalyzerConfigBuilder` and/or `onCreatePreivewConfigBuilder`
106 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.30'
5 | repositories {
6 | google()
7 | jcenter()
8 |
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.4.0'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 |
23 | }
24 | }
25 |
26 | task clean(type: Delete) {
27 | delete rootProject.buildDir
28 | }
29 |
--------------------------------------------------------------------------------
/camera/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/camera/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android-extensions'
3 | apply plugin: 'kotlin-android'
4 |
5 | android {
6 | compileSdkVersion 28
7 |
8 |
9 | defaultConfig {
10 | minSdkVersion 21
11 | targetSdkVersion 28
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 |
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 |
26 | }
27 |
28 | dependencies {
29 | def camerax_version = "1.0.0-alpha01"
30 |
31 | implementation 'androidx.appcompat:appcompat:1.0.2'
32 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
33 | implementation "androidx.camera:camera-camera2:${camerax_version}"
34 | implementation 'com.jakewharton.timber:timber:4.7.1'
35 | api "androidx.camera:camera-core:${camerax_version}"
36 |
37 |
38 | testImplementation 'junit:junit:4.12'
39 | androidTestImplementation 'androidx.test:runner:1.1.1'
40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
41 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
42 | }
43 | repositories {
44 | mavenCentral()
45 | }
46 |
--------------------------------------------------------------------------------
/camera/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/camera/src/androidTest/java/de/crysxd/cameraXTracker/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("de.crysxd.camera.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/camera/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/AutoPreviewBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package de.crysxd.cameraXTracker
18 |
19 | import android.content.Context
20 | import android.graphics.Matrix
21 | import android.hardware.display.DisplayManager
22 | import android.util.Size
23 | import android.view.Display
24 | import android.view.Surface
25 | import android.view.TextureView
26 | import android.view.View
27 | import android.view.ViewGroup
28 | import androidx.camera.core.Preview
29 | import androidx.camera.core.PreviewConfig
30 | import java.lang.IllegalArgumentException
31 | import java.lang.ref.WeakReference
32 | import java.util.Objects
33 |
34 | /**
35 | * Builder for [Preview] that takes in a [WeakReference] of the view finder and
36 | * [PreviewConfig], then instantiates a [Preview] which automatically
37 | * resizes and rotates reacting to config changes.
38 | */
39 | class AutoFitPreviewBuilder private constructor(config: PreviewConfig,
40 | viewFinderRef: WeakReference) {
41 | /** Public instance of preview use-case which can be used by consumers of this adapter */
42 | val useCase: Preview
43 |
44 | /** Internal variable used to keep track of the use-case's output rotation */
45 | private var bufferRotation: Int = 0
46 | /** Internal variable used to keep track of the view's rotation */
47 | private var viewFinderRotation: Int? = null
48 | /** Internal variable used to keep track of the use-case's output dimension */
49 | private var bufferDimens: Size = Size(0, 0)
50 | /** Internal variable used to keep track of the view's dimension */
51 | private var viewFinderDimens: Size = Size(0, 0)
52 | /** Internal variable used to keep track of the view's display */
53 | private var viewFinderDisplay: Int = -1
54 |
55 | private lateinit var displayManager: DisplayManager
56 | /** We need a display listener for 180 degree device orientation changes */
57 | private val displayListener = object : DisplayManager.DisplayListener {
58 | override fun onDisplayAdded(displayId: Int) = Unit
59 | override fun onDisplayRemoved(displayId: Int) = Unit
60 | override fun onDisplayChanged(displayId: Int) {
61 | val viewFinder = viewFinderRef.get() ?: return
62 | if (displayId != viewFinderDisplay) {
63 | val display = displayManager.getDisplay(displayId)
64 | val rotation = getDisplaySurfaceRotation(display)
65 | updateTransform(viewFinder, rotation, bufferDimens, viewFinderDimens)
66 | }
67 | }
68 | }
69 |
70 | init {
71 | // Make sure that the view finder reference is valid
72 | val viewFinder = viewFinderRef.get() ?: throw IllegalArgumentException(
73 | "Invalid reference to view finder used")
74 |
75 | // Initialize the display and rotation from texture view information
76 | viewFinderDisplay = viewFinder.display.displayId
77 | viewFinderRotation = getDisplaySurfaceRotation(viewFinder.display) ?: 0
78 |
79 | // Initialize public use-case with the given config
80 | useCase = Preview(config)
81 |
82 | // Every time the view finder is updated, recompute layout
83 | useCase.onPreviewOutputUpdateListener = Preview.OnPreviewOutputUpdateListener {
84 | val viewFinder =
85 | viewFinderRef.get() ?: return@OnPreviewOutputUpdateListener
86 |
87 | // To update the SurfaceTexture, we have to remove it and re-add it
88 | val parent = viewFinder.parent as ViewGroup
89 | parent.removeView(viewFinder)
90 | parent.addView(viewFinder, 0)
91 |
92 | viewFinder.surfaceTexture = it.surfaceTexture
93 | bufferRotation = it.rotationDegrees
94 | val rotation = getDisplaySurfaceRotation(viewFinder.display)
95 | updateTransform(viewFinder, rotation, it.textureSize, viewFinderDimens)
96 | }
97 |
98 | // Every time the provided texture view changes, recompute layout
99 | viewFinder.addOnLayoutChangeListener { view, left, top, right, bottom, _, _, _, _ ->
100 | val viewFinder = view as TextureView
101 | val newViewFinderDimens = Size(right - left, bottom - top)
102 | val rotation = getDisplaySurfaceRotation(viewFinder.display)
103 | updateTransform(viewFinder, rotation, bufferDimens, newViewFinderDimens)
104 | }
105 |
106 | // Every time the orientation of device changes, recompute layout
107 | displayManager = viewFinder.context
108 | .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
109 | displayManager.registerDisplayListener(displayListener, null)
110 |
111 | // Remove the display listeners when the view is detached to avoid
112 | // holding a reference to the View outside of a Fragment.
113 | // NOTE: Even though using a weak reference should take care of this,
114 | // we still try to avoid unnecessary calls to the listener this way.
115 | viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
116 | override fun onViewAttachedToWindow(view: View?) = Unit
117 | override fun onViewDetachedFromWindow(view: View?) {
118 | displayManager.unregisterDisplayListener(displayListener)
119 | }
120 |
121 | })
122 | }
123 |
124 | /** Helper function that fits a camera preview into the given [TextureView] */
125 | private fun updateTransform(textureView: TextureView?, rotation: Int?, newBufferDimens: Size,
126 | newViewFinderDimens: Size) {
127 | // This should happen anyway, but now the linter knows
128 | val textureView = textureView ?: return
129 |
130 | if (rotation == viewFinderRotation &&
131 | Objects.equals(newBufferDimens, bufferDimens) &&
132 | Objects.equals(newViewFinderDimens, viewFinderDimens)) {
133 | // Nothing has changed, no need to transform output again
134 | return
135 | }
136 |
137 | if (rotation == null) {
138 | // Invalid rotation - wait for valid inputs before setting matrix
139 | return
140 | } else {
141 | // Update internal field with new inputs
142 | viewFinderRotation = rotation
143 | }
144 |
145 | if (newBufferDimens.width == 0 || newBufferDimens.height == 0) {
146 | // Invalid buffer dimens - wait for valid inputs before setting matrix
147 | return
148 | } else {
149 | // Update internal field with new inputs
150 | bufferDimens = newBufferDimens
151 | }
152 |
153 | if (newViewFinderDimens.width == 0 || newViewFinderDimens.height == 0) {
154 | // Invalid view finder dimens - wait for valid inputs before setting matrix
155 | return
156 | } else {
157 | // Update internal field with new inputs
158 | viewFinderDimens = newViewFinderDimens
159 | }
160 |
161 | val matrix = Matrix()
162 |
163 | // Compute the center of the view finder
164 | val centerX = viewFinderDimens.width / 2f
165 | val centerY = viewFinderDimens.height / 2f
166 |
167 | // Correct preview output to account for display rotation
168 | matrix.postRotate(-viewFinderRotation!!.toFloat(), centerX, centerY)
169 |
170 | // Buffers are rotated relative to the device's 'natural' orientation: swap width and height
171 | val bufferRatio = bufferDimens.height / bufferDimens.width.toFloat()
172 |
173 | val scaledWidth: Int
174 | val scaledHeight: Int
175 | // Match longest sides together -- i.e. apply center-crop transformation
176 | if (viewFinderDimens.width > viewFinderDimens.height) {
177 | scaledHeight = viewFinderDimens.width
178 | scaledWidth = Math.round(viewFinderDimens.width * bufferRatio)
179 | } else {
180 | scaledHeight = viewFinderDimens.height
181 | scaledWidth = Math.round(viewFinderDimens.height * bufferRatio)
182 | }
183 |
184 | // Compute the relative scale value
185 | val xScale = scaledWidth / viewFinderDimens.width.toFloat()
186 | val yScale = scaledHeight / viewFinderDimens.height.toFloat()
187 |
188 | // Scale input buffers to fill the view finder
189 | matrix.preScale(xScale, yScale, centerX, centerY)
190 |
191 | // Finally, apply transformations to our TextureView
192 | textureView.setTransform(matrix)
193 | }
194 |
195 | companion object {
196 | /** Helper function that gets the rotation of a [Display] in degrees */
197 | fun getDisplaySurfaceRotation(display: Display?) = when(display?.rotation) {
198 | Surface.ROTATION_0 -> 0
199 | Surface.ROTATION_90 -> 90
200 | Surface.ROTATION_180 -> 180
201 | Surface.ROTATION_270 -> 270
202 | else -> null
203 | }
204 |
205 | /**
206 | * Main entrypoint for users of this class: instantiates the adapter and returns an instance
207 | * of [Preview] which automatically adjusts in size and rotation to compensate for
208 | * config changes.
209 | */
210 | fun build(config: PreviewConfig, viewFinder: TextureView) =
211 | AutoFitPreviewBuilder(config, WeakReference(viewFinder)).useCase
212 | }
213 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/CameraFragment.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker
2 |
3 | import android.app.AlertDialog
4 | import android.os.Bundle
5 | import android.util.Rational
6 | import android.util.Size
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import androidx.camera.core.*
11 | import androidx.fragment.app.Fragment
12 | import androidx.lifecycle.LiveData
13 | import androidx.lifecycle.MutableLiveData
14 | import de.crysxd.cameraXTracker.ar.ArOverlayView
15 | import kotlinx.android.synthetic.main.fragment_camera.*
16 | import timber.log.Timber
17 |
18 | @Suppress("unused")
19 | open class CameraFragment : Fragment() {
20 |
21 | private val mutableArOverlayView = MutableLiveData()
22 | val arOverlayView: LiveData = mutableArOverlayView
23 | var cameraRunning = false
24 | private set
25 | var imageAnalyzer: ThreadedImageAnalyzer? = null
26 | set(value) {
27 | field = value
28 | if (cameraRunning) {
29 | startCamera()
30 | }
31 | }
32 |
33 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
34 | inflater.inflate(R.layout.fragment_camera, container, false)
35 |
36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
37 | super.onViewCreated(view, savedInstanceState)
38 | mutableArOverlayView.postValue(arOverlays)
39 |
40 | CameraPermissionHelper().requestCameraPermission(childFragmentManager) {
41 | if (it) {
42 | startCamera()
43 | } else {
44 | activity?.finish()
45 | }
46 | }
47 | }
48 |
49 | override fun onDestroyView() {
50 | super.onDestroyView()
51 |
52 | if (cameraRunning) {
53 | CameraX.unbindAll()
54 | cameraRunning = false
55 | Timber.i("Stopping camera")
56 | }
57 | }
58 |
59 | private fun startCamera() {
60 | preview.post {
61 | try {
62 | val usesCases = mutableListOf()
63 |
64 | // Make sure that there are no other use cases bound to CameraX
65 | CameraX.unbindAll()
66 |
67 | // Create configuration object for the viewfinder use case
68 | val previewConfig = onCreatePreivewConfigBuilder().build()
69 | usesCases.add(AutoFitPreviewBuilder.build(previewConfig, preview))
70 |
71 | // Setup image analysis pipeline that computes average pixel luminance in real time
72 | if (imageAnalyzer != null) {
73 | val analyzerConfig = onCreateAnalyzerConfigBuilder().build()
74 | usesCases.add(ImageAnalysis(analyzerConfig).apply {
75 | analyzer = imageAnalyzer
76 | })
77 | }
78 |
79 | // Bind use cases to lifecycle
80 | CameraX.bindToLifecycle(this, *usesCases.toTypedArray())
81 | cameraRunning = true
82 | Timber.i("Started camera with useCases=$usesCases")
83 | } catch (e: Exception) {
84 | Timber.e(e)
85 | AlertDialog.Builder(context)
86 | .setMessage(getString(R.string.camera_error))
87 | .setPositiveButton(android.R.string.ok) { _, _ ->
88 | activity?.finish()
89 | }
90 | .create()
91 | }
92 | }
93 | }
94 |
95 | @Suppress("MemberVisibilityCanBePrivate")
96 | protected open fun onCreateAnalyzerConfigBuilder() = ImageAnalysisConfig.Builder().apply {
97 | // Use a worker thread for image analysis to prevent preview glitches
98 | setCallbackHandler(imageAnalyzer!!.getHandler())
99 | // In our analysis, we care more about the latest image than analyzing *every* image
100 | setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
101 | setTargetResolution(Size(preview.width / 2, preview.height / 2))
102 | }
103 |
104 | @Suppress("MemberVisibilityCanBePrivate")
105 | protected open fun onCreatePreivewConfigBuilder() = PreviewConfig.Builder().apply {
106 | setTargetAspectRatio(Rational(16, 9))
107 | setTargetResolution(Size(preview.width, preview.height))
108 | }
109 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/CameraPermissionHelper.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.pm.PackageManager
6 | import androidx.appcompat.app.AlertDialog
7 | import androidx.core.app.ActivityCompat
8 | import androidx.core.content.ContextCompat
9 | import androidx.fragment.app.Fragment
10 | import androidx.fragment.app.FragmentManager
11 |
12 | class CameraPermissionHelper : Fragment() {
13 |
14 | companion object {
15 | private const val TAG = "CameraPermissionHelper"
16 | private const val REQUEST_CODE = 2378
17 | }
18 |
19 | private var callback: ((Boolean) -> Unit)? = null
20 |
21 | fun requestCameraPermission(fm: FragmentManager, callback: (Boolean) -> Unit) {
22 | fm.beginTransaction().apply {
23 | fm.findFragmentByTag(TAG)?.let {
24 | remove(it)
25 | }
26 | add(this@CameraPermissionHelper, TAG)
27 | }.commitAllowingStateLoss()
28 | this.callback = callback
29 | }
30 |
31 | override fun onStart() {
32 | super.onStart()
33 | when {
34 | isCameraPermissionGranted(context!!) -> dispatchCallback(true)
35 | shouldShowRationale(activity!!) -> showRationale(context!!)
36 | else -> requestCameraPermission(context!!)
37 | }
38 | }
39 |
40 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
41 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
42 | if (REQUEST_CODE == requestCode) {
43 | when {
44 | isCameraPermissionGranted(context!!) -> dispatchCallback(true)
45 | shouldShowRationale(activity!!) -> showRationale(context!!)
46 | else -> dispatchCallback(false)
47 | }
48 | }
49 | }
50 |
51 | private fun dispatchCallback(result: Boolean) {
52 | callback?.invoke(result)
53 | callback = null
54 | }
55 |
56 | private fun showRationale(context: Context) = AlertDialog.Builder(context)
57 | .setMessage(getString(R.string.camera_usage_rationale))
58 | .setPositiveButton(getString(R.string.allow_camera_usage)) { _, _ ->
59 | requestCameraPermission(context)
60 | }
61 | .setCancelable(false)
62 | .show()
63 |
64 | private fun requestCameraPermission(context: Context) {
65 | requestPermissions(arrayOf(android.Manifest.permission.CAMERA), REQUEST_CODE)
66 | }
67 |
68 | private fun shouldShowRationale(activity: Activity) = ActivityCompat.shouldShowRequestPermissionRationale(
69 | activity,
70 | android.Manifest.permission.CAMERA
71 | )
72 |
73 | private fun isCameraPermissionGranted(context: Context) = ContextCompat.checkSelfPermission(
74 | context,
75 | android.Manifest.permission.CAMERA
76 | ) == PackageManager.PERMISSION_GRANTED
77 |
78 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/FrontFacingCameraFragment.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker
2 |
3 | import androidx.camera.core.CameraX
4 | import androidx.camera.core.ImageAnalysisConfig
5 | import androidx.camera.core.PreviewConfig
6 |
7 | class FrontFacingCameraFragment : CameraFragment() {
8 |
9 | override fun onCreateAnalyzerConfigBuilder(): ImageAnalysisConfig.Builder = super.onCreateAnalyzerConfigBuilder().apply {
10 | setLensFacing(CameraX.LensFacing.FRONT)
11 | }
12 |
13 | override fun onCreatePreivewConfigBuilder(): PreviewConfig.Builder = super.onCreatePreivewConfigBuilder().apply {
14 | setLensFacing(CameraX.LensFacing.FRONT)
15 | }
16 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ThreadedImageAnalyzer.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker
2 |
3 | import android.os.Handler
4 | import androidx.camera.core.ImageAnalysis
5 |
6 | interface ThreadedImageAnalyzer : ImageAnalysis.Analyzer {
7 |
8 | fun getHandler(): Handler
9 |
10 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/ArObject.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | import android.graphics.RectF
4 | import android.util.Size
5 |
6 | data class ArObject(val trackingId: Int, val boundingBox: RectF, val sourceSize: Size, val sourceRotationDegrees: Int)
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/ArObjectTracker.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | open class ArObjectTracker() {
4 |
5 | private val trackingListeners = mutableListOf()
6 | private var pipedTracked: ArObjectTracker? = null
7 |
8 | fun addTrackingListener(listener: ArObjectTrackingListener): ArObjectTracker {
9 | trackingListeners.add(listener)
10 | return this
11 | }
12 |
13 | fun addTrackingListener(listener: (ArObject?) -> Unit): ArObjectTracker {
14 | addTrackingListener(object : ArObjectTrackingListener {
15 | override fun onObjectTracked(arObject: ArObject?) {
16 | listener(arObject)
17 | }
18 | })
19 | return this
20 | }
21 |
22 | fun removeTrackingListener(listener: ArObjectTrackingListener) = trackingListeners.remove(listener)
23 |
24 | fun pipe(otherTracker: ArObjectTracker): ArObjectTracker {
25 | pipedTracked = otherTracker
26 | return otherTracker
27 | }
28 |
29 | fun clearListenersAndPipe() {
30 | trackingListeners.clear()
31 | pipedTracked?.clearListenersAndPipe()
32 | pipedTracked = null
33 | }
34 |
35 | open fun processObject(arObject: ArObject?) {
36 | publish(arObject)
37 | }
38 |
39 | protected fun publish(arObject: ArObject?) {
40 | // Pipe
41 | pipedTracked?.processObject(arObject)
42 |
43 | // Notify
44 | trackingListeners.forEach {
45 | it.onObjectTracked(arObject)
46 | }
47 | }
48 |
49 | interface ArObjectTrackingListener {
50 | fun onObjectTracked(arObject: ArObject?)
51 | }
52 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/ArOverlay.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | import android.graphics.Canvas
4 |
5 | abstract class ArOverlay() {
6 |
7 | protected var host: ArOverlayView? = null
8 |
9 | abstract fun onDraw(canvas: Canvas)
10 |
11 | open fun onAttached(view: ArOverlayView) {
12 | this.host = view
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/ArOverlayView.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.util.AttributeSet
6 | import android.widget.FrameLayout
7 | import androidx.annotation.GuardedBy
8 |
9 | class ArOverlayView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, style: Int = 0) :
10 | FrameLayout(context, attributeSet, style) {
11 |
12 | private val overlays = mutableListOf()
13 | private val objectLock = Object()
14 |
15 | init {
16 | setWillNotDraw(false)
17 | }
18 |
19 | @GuardedBy("objectLock")
20 | fun remove(overlay: ArOverlay) {
21 | overlays.remove(overlay)
22 | }
23 |
24 | @GuardedBy("objectLock")
25 | fun add(overlay: ArOverlay) {
26 | remove(overlay)
27 | overlays.add(overlay)
28 | overlay.onAttached(this)
29 | invalidate()
30 | }
31 |
32 | @GuardedBy("objectLock")
33 | fun clear() {
34 | overlays.clear()
35 | invalidate()
36 | }
37 |
38 | @GuardedBy("objectLock")
39 | override fun onDraw(canvas: Canvas) {
40 | super.onDraw(canvas)
41 | overlays.forEach {
42 | it.onDraw(canvas)
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/BoundingBoxArOverlay.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.graphics.RectF
8 | import androidx.annotation.ColorInt
9 | import de.crysxd.cameraXTracker.BuildConfig
10 | import de.crysxd.cameraXTracker.R
11 |
12 |
13 | class BoundingBoxArOverlay(private val context: Context, private val debugMode: Boolean = false) : ArOverlay(), ArObjectTracker.ArObjectTrackingListener {
14 |
15 | private val cornerRadius = context.resources.getDimension(R.dimen.ar_bounding_box_corner_radius)
16 | private val boxPadding = context.resources.getDimension(R.dimen.ar_bounding_box_padding)
17 | private var currentBoundingBox: RectF? = null
18 | private var currentTrackingId: Int? = null
19 |
20 | private val boundingBoxBorderPaint = Paint().apply {
21 | strokeWidth = context.resources.getDimension(R.dimen.ar_bounding_rect_stroke_width)
22 | style = Paint.Style.STROKE
23 | isAntiAlias = true
24 | }
25 |
26 | private val boundingBoxFillPaint = Paint().apply {
27 | style = Paint.Style.FILL
28 | isAntiAlias = true
29 | }
30 |
31 | private val textPaint = Paint().apply {
32 | color = Color.WHITE
33 | textSize = cornerRadius
34 | isAntiAlias = true
35 | }
36 |
37 | init {
38 | setBoundingBoxColor(Color.WHITE)
39 | }
40 |
41 | override fun onObjectTracked(arObject: ArObject?) {
42 | currentBoundingBox = arObject?.boundingBox
43 | currentTrackingId = arObject?.trackingId
44 |
45 | // Add padding in all directions to the bounding box
46 | if (currentBoundingBox != null) {
47 | currentBoundingBox!!.left -= boxPadding
48 | currentBoundingBox!!.top -= boxPadding
49 | currentBoundingBox!!.right += boxPadding
50 | currentBoundingBox!!.bottom += boxPadding
51 | }
52 |
53 | host?.invalidate()
54 | }
55 |
56 | fun setBoundingBoxColor(@ColorInt color: Int) {
57 | boundingBoxBorderPaint.color = color
58 | boundingBoxBorderPaint.alpha = 153
59 | boundingBoxFillPaint.color = color
60 | boundingBoxFillPaint.alpha = 51
61 | host?.invalidate()
62 | }
63 |
64 | override fun onDraw(canvas: Canvas) {
65 | if (currentBoundingBox != null) {
66 | canvas.drawRoundRect(currentBoundingBox!!, cornerRadius, cornerRadius, boundingBoxFillPaint)
67 | canvas.drawRoundRect(currentBoundingBox!!, cornerRadius, cornerRadius, boundingBoxBorderPaint)
68 |
69 | if (debugMode) {
70 | canvas.drawText(
71 | "$currentTrackingId",
72 | currentBoundingBox!!.left + cornerRadius,
73 | currentBoundingBox!!.bottom - cornerRadius,
74 | textPaint
75 | )
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/PathInterpolator.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | import android.animation.Animator
4 | import android.animation.ObjectAnimator
5 | import android.graphics.RectF
6 | import android.os.Handler
7 | import android.os.Looper
8 | import android.view.animation.LinearInterpolator
9 | import timber.log.Timber
10 |
11 | /**
12 | * This class can be used to interpolate the path of an ArObject. We are always chasing the AR object, thus the result might
13 | * be slightly delayed but missing intermediary positions will be added
14 | */
15 | class PathInterpolator(private val lostObjectCacheDurationMs: Long = 200, private val animationDurationMs: Long = 100) :
16 | ArObjectTracker() {
17 |
18 | private val handler = Handler(Looper.getMainLooper())
19 | private var lastArObject: ArObject? = null
20 | private var currentBoundingBox: RectF? = null
21 | private var currentAnimator: Animator? = null
22 |
23 | override fun processObject(arObject: ArObject?) {
24 | // We lost track of the object. We schedule to remove it after the cache duration. If the Firebase
25 | // finds the object again (most likely with a new tracking id) we can animate the bounding box to the new
26 | // (most very similar) position
27 | if (arObject == null) {
28 | if (currentBoundingBox != null) {
29 | handler.postDelayed(this::clearTracking, lostObjectCacheDurationMs)
30 | }
31 | }
32 |
33 | // We did track an object and got a new one
34 | // We will animate the current bounding box to the new bounding box
35 | // This will make the transition very smooth
36 | else {
37 | // If we got a non-null object, cancel the clear request (if any)
38 | handler.removeCallbacks(this::clearTracking)
39 |
40 | val start = currentBoundingBox?.toEdges() ?: arObject.boundingBox.toEdges()
41 | val end = arObject.boundingBox.toEdges()
42 | Timber.d("Animating from ${currentBoundingBox?.toEdges()?.toList()} to ${end.toList()}")
43 |
44 | lastArObject = arObject
45 | currentAnimator?.cancel()
46 | currentAnimator = ObjectAnimator.ofMultiFloat(this, "boundingBox", arrayOf(start, end)).apply {
47 | duration = animationDurationMs
48 | interpolator = LinearInterpolator()
49 | addUpdateListener {
50 | // The automated call to setBoundingBox does not work...Kotlin?
51 | setBoundingBox(it.animatedValue as FloatArray)
52 | }
53 | start()
54 | }
55 | }
56 | }
57 |
58 | private fun clearTracking() {
59 | Timber.d("Clearing bounding box")
60 | lastArObject = null
61 | setBoundingBox(null)
62 | }
63 |
64 | private fun setBoundingBox(edges: FloatArray?) {
65 | Timber.d("Set bounding box ${edges?.toList()}")
66 | currentBoundingBox = edges?.toRectF()
67 |
68 | publish(
69 | if (currentBoundingBox != null) {
70 | lastArObject?.copy(boundingBox = currentBoundingBox!!.copy())
71 | } else {
72 | null
73 | }
74 | )
75 | }
76 |
77 | private fun RectF.copy() = RectF(left, top, right, bottom)
78 |
79 | private fun RectF.toEdges() = arrayOf(left, top, right, bottom).toFloatArray()
80 |
81 | private fun FloatArray.toRectF() = RectF().also {
82 | it.left = this[0]
83 | it.top = this[1]
84 | it.right = this[2]
85 | it.bottom = this[3]
86 | }
87 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/PositionTranslator.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | import android.graphics.Matrix
4 | import android.graphics.RectF
5 | import android.util.Size
6 | import timber.log.Timber
7 |
8 | /**
9 | * Allows the tracked ArObject to be mapped to a other coordinate system, e.g. a view.
10 | *
11 | * We use this to map the coordinate system of the preview image we received from the camera API to the ArOverlayView's
12 | * coordinate system which will have an other size.
13 | */
14 | class PositionTranslator(
15 | private val targetWidth: Int,
16 | private val targetHeight: Int,
17 | private val frontFacing: Boolean = false
18 | ) : ArObjectTracker() {
19 |
20 | override fun processObject(arObject: ArObject?) {
21 | if (arObject != null) {
22 | Timber.i("boundingBoxStart = ${arObject.boundingBox}")
23 |
24 | // Rotate Size
25 | val rotatedSize = when (arObject.sourceRotationDegrees) {
26 | 90, 270 -> Size(arObject.sourceSize.height, arObject.sourceSize.width)
27 | 0, 180 -> arObject.sourceSize
28 | else -> throw IllegalArgumentException("Unsupported rotation. Must be 0, 90, 180 or 270")
29 | }
30 | Timber.d("Mapping from source ${rotatedSize.width}x${rotatedSize.height} to ${targetWidth}x$targetHeight")
31 |
32 |
33 | // Calculate scale
34 | val scaleX = targetWidth / rotatedSize.width.toDouble()
35 | val scaleY = targetHeight / rotatedSize.height.toDouble()
36 | val scale = Math.max(scaleX, scaleY)
37 | val scaleF = scale.toFloat()
38 | val scaledSize = Size(Math.ceil(rotatedSize.width * scale).toInt(), Math.ceil(rotatedSize.height * scale).toInt())
39 | Timber.d("Use scale=$scale, scaledSize: ${scaledSize.width}x${scaledSize.height}")
40 |
41 |
42 | // Calculate offset (we need to center the overlay on the target)
43 | val offsetX = (targetWidth - scaledSize.width) / 2
44 | val offsetY = (targetHeight - scaledSize.height) / 2
45 | Timber.d("Use offsetX=$offsetX, offsetY=$offsetY")
46 |
47 | // Map bounding box
48 | val mappedBoundingBox = RectF().apply {
49 | left = arObject.boundingBox.right * scaleF + offsetX
50 | top = arObject.boundingBox.top * scaleF + offsetY
51 | right = arObject.boundingBox.left * scaleF + offsetX
52 | bottom = arObject.boundingBox.bottom * scaleF + offsetY
53 | }
54 |
55 | // The front facing image is flipped, so we need to mirrow the positions on the vertical axis (centerX)
56 | if (frontFacing) {
57 | val centerX = targetWidth / 2
58 | mappedBoundingBox.left = centerX + (centerX - mappedBoundingBox.left)
59 | mappedBoundingBox.right = centerX - (mappedBoundingBox.right - centerX)
60 | }
61 |
62 | Timber.d("Mapped bounding box=$mappedBoundingBox")
63 |
64 | super.processObject(
65 | arObject.copy(
66 | boundingBox = mappedBoundingBox,
67 | sourceSize = Size(targetWidth, targetHeight)
68 | )
69 | )
70 | } else {
71 | super.processObject(null)
72 | }
73 | }
74 |
75 | private fun RectF.toSize() = Size(width().toInt(), height().toInt())
76 | }
--------------------------------------------------------------------------------
/camera/src/main/java/de/crysxd/cameraXTracker/ar/ViewArOverlay.kt:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker.ar
2 |
3 | import android.graphics.Canvas
4 | import android.view.View
5 |
6 | class ViewArOverlay(private val view: View) : ArOverlay(), ArObjectTracker.ArObjectTrackingListener {
7 |
8 | override fun onAttached(view: ArOverlayView) {
9 | host?.removeView(this.view)
10 | super.onAttached(view)
11 | host?.addView(this.view)
12 | }
13 |
14 | override fun onObjectTracked(arObject: ArObject?) {
15 | if (arObject == null) {
16 | view.visibility = View.GONE
17 | } else {
18 | view.visibility = View.VISIBLE
19 | view.translationX = arObject.boundingBox.centerX() - view.width / 2
20 | view.translationY = arObject.boundingBox.centerY() - view.height / 2
21 | }
22 | }
23 |
24 | override fun onDraw(canvas: Canvas) = Unit
25 |
26 | }
--------------------------------------------------------------------------------
/camera/src/main/res/layout/fragment_camera.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
16 |
17 |
--------------------------------------------------------------------------------
/camera/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1dp
4 | 8dp
5 | 8dp
6 | 2dp
7 |
--------------------------------------------------------------------------------
/camera/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | This app requires access to your camera to work.
3 | Allow camera usage
4 | The camera is not responding. Please try again later.
5 |
6 |
--------------------------------------------------------------------------------
/camera/src/test/java/de/crysxd/cameraXTracker/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package de.crysxd.cameraXTracker;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crysxd/CameraX-Object-Tracking/14a940ac9877aa9cfe60569528e4c89213c1ac3d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun May 19 10:46:40 CEST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':camera'
2 |
--------------------------------------------------------------------------------