Showing
6 changed files
with
510 additions
and
4 deletions
1 | apply plugin: 'com.android.application' | 1 | apply plugin: 'com.android.application' |
2 | +apply plugin: 'kotlin-android-extensions' | ||
3 | +apply plugin: 'kotlin-android' | ||
2 | 4 | ||
3 | android { | 5 | android { |
4 | compileSdkVersion 29 | 6 | compileSdkVersion 29 |
... | @@ -17,6 +19,9 @@ android { | ... | @@ -17,6 +19,9 @@ android { |
17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
18 | } | 20 | } |
19 | } | 21 | } |
22 | + aaptOptions { | ||
23 | + noCompress "tflite" | ||
24 | + } | ||
20 | } | 25 | } |
21 | 26 | ||
22 | dependencies { | 27 | dependencies { |
... | @@ -26,4 +31,11 @@ dependencies { | ... | @@ -26,4 +31,11 @@ dependencies { |
26 | testImplementation 'junit:junit:4.12' | 31 | testImplementation 'junit:junit:4.12' |
27 | androidTestImplementation 'androidx.test.ext:junit:1.1.0' | 32 | androidTestImplementation 'androidx.test.ext:junit:1.1.0' |
28 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' | 33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' |
34 | + compile "androidx.core:core-ktx:+" | ||
35 | + implementation 'org.tensorflow:tensorflow-lite:2.2.0' | ||
36 | + implementation 'org.tensorflow:tensorflow-lite-gpu:2.2.0' | ||
37 | + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | ||
38 | +} | ||
39 | +repositories { | ||
40 | + mavenCentral() | ||
29 | } | 41 | } | ... | ... |
This file is too large to display.
... | @@ -7,25 +7,39 @@ import androidx.core.app.ActivityCompat; | ... | @@ -7,25 +7,39 @@ import androidx.core.app.ActivityCompat; |
7 | import android.Manifest; | 7 | import android.Manifest; |
8 | import android.content.Context; | 8 | import android.content.Context; |
9 | import android.content.pm.PackageManager; | 9 | import android.content.pm.PackageManager; |
10 | +import android.graphics.Bitmap; | ||
11 | +import android.graphics.Canvas; | ||
12 | +import android.graphics.Color; | ||
13 | +import android.graphics.ImageFormat; | ||
14 | +import android.graphics.Matrix; | ||
15 | +import android.graphics.Paint; | ||
16 | +import android.graphics.Rect; | ||
10 | import android.hardware.camera2.CameraAccessException; | 17 | import android.hardware.camera2.CameraAccessException; |
11 | import android.hardware.camera2.CameraCaptureSession; | 18 | import android.hardware.camera2.CameraCaptureSession; |
12 | import android.hardware.camera2.CameraDevice; | 19 | import android.hardware.camera2.CameraDevice; |
13 | import android.hardware.camera2.CameraManager; | 20 | import android.hardware.camera2.CameraManager; |
14 | import android.hardware.camera2.CaptureRequest; | 21 | import android.hardware.camera2.CaptureRequest; |
22 | +import android.media.Image; | ||
23 | +import android.media.ImageReader; | ||
15 | import android.os.Bundle; | 24 | import android.os.Bundle; |
25 | +import android.util.Log; | ||
16 | import android.view.SurfaceHolder; | 26 | import android.view.SurfaceHolder; |
17 | import android.view.SurfaceView; | 27 | import android.view.SurfaceView; |
18 | import android.widget.Toast; | 28 | import android.widget.Toast; |
19 | 29 | ||
30 | +import java.nio.ByteBuffer; | ||
20 | import java.util.Arrays; | 31 | import java.util.Arrays; |
21 | 32 | ||
22 | -public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder.Callback { | 33 | +public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder.Callback, ImageReader.OnImageAvailableListener { |
23 | 34 | ||
24 | private static final int REQUEST_CAMERA = 1000; | 35 | private static final int REQUEST_CAMERA = 1000; |
25 | 36 | ||
26 | private SurfaceView surfaceView; | 37 | private SurfaceView surfaceView; |
27 | private CameraDevice camera; | 38 | private CameraDevice camera; |
28 | private CaptureRequest.Builder previewBuilder; | 39 | private CaptureRequest.Builder previewBuilder; |
40 | + private Posenet posenet; | ||
41 | + private ImageReader imageReader; | ||
42 | + private byte[][] yuvBytes = new byte[3][]; | ||
29 | 43 | ||
30 | @Override | 44 | @Override |
31 | protected void onCreate(Bundle savedInstanceState) { | 45 | protected void onCreate(Bundle savedInstanceState) { |
... | @@ -36,6 +50,18 @@ public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder | ... | @@ -36,6 +50,18 @@ public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder |
36 | surfaceView.getHolder().addCallback(this); | 50 | surfaceView.getHolder().addCallback(this); |
37 | } | 51 | } |
38 | 52 | ||
53 | + @Override | ||
54 | + protected void onStart() { | ||
55 | + super.onStart(); | ||
56 | + posenet = new Posenet(this, "posenet_model.tflite", Device.GPU); | ||
57 | + } | ||
58 | + | ||
59 | + @Override | ||
60 | + protected void onDestroy() { | ||
61 | + super.onDestroy(); | ||
62 | + posenet.close(); | ||
63 | + } | ||
64 | + | ||
39 | // 카메라 활성화 | 65 | // 카메라 활성화 |
40 | private void openCamera() { | 66 | private void openCamera() { |
41 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { | 67 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { |
... | @@ -99,10 +125,13 @@ public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder | ... | @@ -99,10 +125,13 @@ public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder |
99 | // 카메라 Capture 시작 | 125 | // 카메라 Capture 시작 |
100 | private void startCapture() { | 126 | private void startCapture() { |
101 | try { | 127 | try { |
128 | + imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2); | ||
129 | + imageReader.setOnImageAvailableListener(this, null); | ||
130 | + | ||
102 | previewBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); | 131 | previewBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); |
103 | - previewBuilder.addTarget(surfaceView.getHolder().getSurface()); | 132 | + previewBuilder.addTarget(imageReader.getSurface()); |
104 | 133 | ||
105 | - camera.createCaptureSession(Arrays.asList(surfaceView.getHolder().getSurface()), new CameraCaptureSession.StateCallback() { | 134 | + camera.createCaptureSession(Arrays.asList(imageReader.getSurface()), new CameraCaptureSession.StateCallback() { |
106 | @Override | 135 | @Override |
107 | public void onConfigured(@NonNull CameraCaptureSession session) { | 136 | public void onConfigured(@NonNull CameraCaptureSession session) { |
108 | try { | 137 | try { |
... | @@ -125,4 +154,110 @@ public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder | ... | @@ -125,4 +154,110 @@ public class ExerciseActivity extends AppCompatActivity implements SurfaceHolder |
125 | e.printStackTrace(); | 154 | e.printStackTrace(); |
126 | } | 155 | } |
127 | } | 156 | } |
157 | + | ||
158 | + @Override | ||
159 | + public void onImageAvailable(ImageReader reader) { | ||
160 | + Image image = reader.acquireLatestImage(); | ||
161 | + if(image == null) | ||
162 | + return; | ||
163 | + | ||
164 | + fillBytes(image.getPlanes(), yuvBytes); | ||
165 | + int[] rgbBytes = new int[640 * 480]; | ||
166 | + | ||
167 | + ImageUtils.INSTANCE.convertYUV420ToARGB8888(yuvBytes[0], yuvBytes[1], yuvBytes[2], | ||
168 | + 640, 480, | ||
169 | + image.getPlanes()[0].getRowStride(), | ||
170 | + image.getPlanes()[1].getRowStride(), | ||
171 | + image.getPlanes()[1].getPixelStride(), rgbBytes); | ||
172 | + | ||
173 | + Bitmap imageBitmap = Bitmap.createBitmap(rgbBytes, 640, 480, Bitmap.Config.ARGB_8888); | ||
174 | + | ||
175 | + Matrix rotateMatrix = new Matrix(); | ||
176 | + rotateMatrix.postRotate(90); | ||
177 | + | ||
178 | + Bitmap rotatedBitmap = Bitmap.createBitmap(imageBitmap, | ||
179 | + 0, 0, 640, 480, rotateMatrix, true); | ||
180 | + image.close(); | ||
181 | + | ||
182 | + processImage(rotatedBitmap); | ||
183 | + } | ||
184 | + | ||
185 | + private void fillBytes(Image.Plane[] planes, byte[][] yuvBytes) { | ||
186 | + // Row stride is the total number of bytes occupied in memory by a row of an image. | ||
187 | + // Because of the variable row stride it's not possible to know in | ||
188 | + // advance the actual necessary dimensions of the yuv planes. | ||
189 | + for (int i = 0; i < planes.length; i++) { | ||
190 | + ByteBuffer buffer = planes[i].getBuffer(); | ||
191 | + if (yuvBytes[i] == null) { | ||
192 | + yuvBytes[i] = new byte[buffer.capacity()]; | ||
193 | + } | ||
194 | + buffer.get(yuvBytes[i]); | ||
195 | + } | ||
196 | + } | ||
197 | + | ||
198 | + private Bitmap cropBitmap(Bitmap bitmap) { | ||
199 | + float bitmapRatio = (float) bitmap.getHeight() / bitmap.getWidth(); | ||
200 | + float modelInputRatio = 257.0f / 257.0f; | ||
201 | + Bitmap croppedBitmap = bitmap; | ||
202 | + | ||
203 | + // Acceptable difference between the modelInputRatio and bitmapRatio to skip cropping. | ||
204 | + double maxDifference = 1e-5; | ||
205 | + | ||
206 | + // Checks if the bitmap has similar aspect ratio as the required model input. | ||
207 | + if(Math.abs(modelInputRatio - bitmapRatio) < maxDifference) | ||
208 | + return croppedBitmap; | ||
209 | + | ||
210 | + if(modelInputRatio < bitmapRatio) { | ||
211 | + // New image is taller so we are height constrained. | ||
212 | + float cropHeight = bitmap.getHeight() - bitmap.getWidth() / modelInputRatio; | ||
213 | + croppedBitmap = Bitmap.createBitmap( | ||
214 | + bitmap, | ||
215 | + 0, | ||
216 | + (int) cropHeight / 2, | ||
217 | + bitmap.getWidth(), | ||
218 | + (int) (bitmap.getHeight() - cropHeight) | ||
219 | + ); | ||
220 | + } | ||
221 | + else { | ||
222 | + float cropWidth = bitmap.getWidth() - bitmap.getHeight() * modelInputRatio; | ||
223 | + croppedBitmap = Bitmap.createBitmap( | ||
224 | + bitmap, | ||
225 | + (int) (cropWidth / 2), | ||
226 | + 0, | ||
227 | + (int) (bitmap.getWidth() - cropWidth), | ||
228 | + bitmap.getHeight() | ||
229 | + ); | ||
230 | + } | ||
231 | + return croppedBitmap; | ||
232 | + } | ||
233 | + | ||
234 | + private void processImage(Bitmap bitmap) { | ||
235 | + Log.d("Capture", "Process"); | ||
236 | + | ||
237 | + // Crop bitmap. | ||
238 | + Bitmap croppedBitmap = cropBitmap(bitmap); | ||
239 | + | ||
240 | + // Created scaled version of bitmap for model input. | ||
241 | + Bitmap scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, 257, 257, true); | ||
242 | + | ||
243 | + // Perform inference. | ||
244 | + Person person = posenet.estimateSinglePose(scaledBitmap); | ||
245 | + | ||
246 | + Paint paint = new Paint(); | ||
247 | + Canvas canvas = surfaceView.getHolder().lockCanvas(); | ||
248 | + | ||
249 | + // 이미지 그리기 | ||
250 | + canvas.drawBitmap(croppedBitmap, new Rect(0, 0, croppedBitmap.getWidth(), croppedBitmap.getHeight()), new Rect(0, 0, canvas.getWidth(), canvas.getWidth()), paint); | ||
251 | + | ||
252 | + // Key points 그리기 | ||
253 | + paint.setColor(Color.RED); | ||
254 | + for(KeyPoint keyPoint : person.getKeyPoints()) { | ||
255 | + if(keyPoint.getScore() < 0.7) | ||
256 | + continue; | ||
257 | + | ||
258 | + canvas.drawCircle((float) keyPoint.getPosition().getX() / scaledBitmap.getWidth() * canvas.getWidth(), (float) keyPoint.getPosition().getY() / scaledBitmap.getWidth() * canvas.getWidth(), 5, paint); | ||
259 | + } | ||
260 | + | ||
261 | + surfaceView.getHolder().unlockCanvasAndPost(canvas); | ||
262 | + } | ||
128 | } | 263 | } | ... | ... |
1 | +/* Copyright 2019 The TensorFlow Authors. All Rights Reserved. | ||
2 | + | ||
3 | +Licensed under the Apache License, Version 2.0 (the "License"); | ||
4 | +you may not use this file except in compliance with the License. | ||
5 | +You may obtain a copy of the License at | ||
6 | + | ||
7 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
8 | + | ||
9 | +Unless required by applicable law or agreed to in writing, software | ||
10 | +distributed under the License is distributed on an "AS IS" BASIS, | ||
11 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
12 | +See the License for the specific language governing permissions and | ||
13 | +limitations under the License. | ||
14 | +==============================================================================*/ | ||
15 | + | ||
16 | +package com.khuhacker.pocketgym | ||
17 | + | ||
18 | +/** Utility class for manipulating images. */ | ||
19 | +object ImageUtils { | ||
20 | + // This value is 2 ^ 18 - 1, and is used to hold the RGB values together before their ranges | ||
21 | + // are normalized to eight bits. | ||
22 | + private const val MAX_CHANNEL_VALUE = 262143 | ||
23 | + | ||
24 | + /** Helper function to convert y,u,v integer values to RGB format */ | ||
25 | + private fun convertYUVToRGB(y: Int, u: Int, v: Int): Int { | ||
26 | + // Adjust and check YUV values | ||
27 | + val yNew = if (y - 16 < 0) 0 else y - 16 | ||
28 | + val uNew = u - 128 | ||
29 | + val vNew = v - 128 | ||
30 | + val expandY = 1192 * yNew | ||
31 | + var r = expandY + 1634 * vNew | ||
32 | + var g = expandY - 833 * vNew - 400 * uNew | ||
33 | + var b = expandY + 2066 * uNew | ||
34 | + | ||
35 | + // Clipping RGB values to be inside boundaries [ 0 , MAX_CHANNEL_VALUE ] | ||
36 | + val checkBoundaries = { x: Int -> | ||
37 | + when { | ||
38 | + x > MAX_CHANNEL_VALUE -> MAX_CHANNEL_VALUE | ||
39 | + x < 0 -> 0 | ||
40 | + else -> x | ||
41 | + } | ||
42 | + } | ||
43 | + r = checkBoundaries(r) | ||
44 | + g = checkBoundaries(g) | ||
45 | + b = checkBoundaries(b) | ||
46 | + return -0x1000000 or (r shl 6 and 0xff0000) or (g shr 2 and 0xff00) or (b shr 10 and 0xff) | ||
47 | + } | ||
48 | + | ||
49 | + /** Converts YUV420 format image data (ByteArray) into ARGB8888 format with IntArray as output. */ | ||
50 | + fun convertYUV420ToARGB8888( | ||
51 | + yData: ByteArray, | ||
52 | + uData: ByteArray, | ||
53 | + vData: ByteArray, | ||
54 | + width: Int, | ||
55 | + height: Int, | ||
56 | + yRowStride: Int, | ||
57 | + uvRowStride: Int, | ||
58 | + uvPixelStride: Int, | ||
59 | + out: IntArray | ||
60 | + ) { | ||
61 | + var outputIndex = 0 | ||
62 | + for (j in 0 until height) { | ||
63 | + val positionY = yRowStride * j | ||
64 | + val positionUV = uvRowStride * (j shr 1) | ||
65 | + | ||
66 | + for (i in 0 until width) { | ||
67 | + val uvOffset = positionUV + (i shr 1) * uvPixelStride | ||
68 | + | ||
69 | + // "0xff and" is used to cut off bits from following value that are higher than | ||
70 | + // the low 8 bits | ||
71 | + out[outputIndex] = convertYUVToRGB( | ||
72 | + 0xff and yData[positionY + i].toInt(), 0xff and uData[uvOffset].toInt(), | ||
73 | + 0xff and vData[uvOffset].toInt() | ||
74 | + ) | ||
75 | + outputIndex += 1 | ||
76 | + } | ||
77 | + } | ||
78 | + } | ||
79 | +} |
1 | +/* | ||
2 | + * Copyright 2019 The TensorFlow Authors. All Rights Reserved. | ||
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 | + * http://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 | +package com.khuhacker.pocketgym | ||
17 | + | ||
18 | +import android.content.Context | ||
19 | +import android.graphics.Bitmap | ||
20 | +import android.os.SystemClock | ||
21 | +import android.util.Log | ||
22 | +import java.io.FileInputStream | ||
23 | +import java.nio.ByteBuffer | ||
24 | +import java.nio.ByteOrder | ||
25 | +import java.nio.MappedByteBuffer | ||
26 | +import java.nio.channels.FileChannel | ||
27 | +import kotlin.math.exp | ||
28 | +import org.tensorflow.lite.Interpreter | ||
29 | +import org.tensorflow.lite.gpu.GpuDelegate | ||
30 | + | ||
31 | +enum class BodyPart { | ||
32 | + NOSE, | ||
33 | + LEFT_EYE, | ||
34 | + RIGHT_EYE, | ||
35 | + LEFT_EAR, | ||
36 | + RIGHT_EAR, | ||
37 | + LEFT_SHOULDER, | ||
38 | + RIGHT_SHOULDER, | ||
39 | + LEFT_ELBOW, | ||
40 | + RIGHT_ELBOW, | ||
41 | + LEFT_WRIST, | ||
42 | + RIGHT_WRIST, | ||
43 | + LEFT_HIP, | ||
44 | + RIGHT_HIP, | ||
45 | + LEFT_KNEE, | ||
46 | + RIGHT_KNEE, | ||
47 | + LEFT_ANKLE, | ||
48 | + RIGHT_ANKLE | ||
49 | +} | ||
50 | + | ||
51 | +class Position { | ||
52 | + var x: Int = 0 | ||
53 | + var y: Int = 0 | ||
54 | +} | ||
55 | + | ||
56 | +class KeyPoint { | ||
57 | + var bodyPart: BodyPart = BodyPart.NOSE | ||
58 | + var position: Position = Position() | ||
59 | + var score: Float = 0.0f | ||
60 | +} | ||
61 | + | ||
62 | +class Person { | ||
63 | + var keyPoints = listOf<KeyPoint>() | ||
64 | + var score: Float = 0.0f | ||
65 | +} | ||
66 | + | ||
67 | +enum class Device { | ||
68 | + CPU, | ||
69 | + NNAPI, | ||
70 | + GPU | ||
71 | +} | ||
72 | + | ||
73 | +class Posenet( | ||
74 | + val context: Context, | ||
75 | + val filename: String = "posenet_model.tflite", | ||
76 | + val device: Device = Device.GPU | ||
77 | +) : AutoCloseable { | ||
78 | + var lastInferenceTimeNanos: Long = -1 | ||
79 | + private set | ||
80 | + | ||
81 | + /** An Interpreter for the TFLite model. */ | ||
82 | + private var interpreter: Interpreter? = null | ||
83 | + private var gpuDelegate: GpuDelegate? = null | ||
84 | + private val NUM_LITE_THREADS = 4 | ||
85 | + | ||
86 | + private fun getInterpreter(): Interpreter { | ||
87 | + if (interpreter != null) { | ||
88 | + return interpreter!! | ||
89 | + } | ||
90 | + val options = Interpreter.Options() | ||
91 | + options.setNumThreads(NUM_LITE_THREADS) | ||
92 | + when (device) { | ||
93 | + Device.CPU -> { } | ||
94 | + Device.GPU -> { | ||
95 | + gpuDelegate = GpuDelegate() | ||
96 | + options.addDelegate(gpuDelegate) | ||
97 | + } | ||
98 | + Device.NNAPI -> options.setUseNNAPI(true) | ||
99 | + } | ||
100 | + interpreter = Interpreter(loadModelFile(filename, context), options) | ||
101 | + return interpreter!! | ||
102 | + } | ||
103 | + | ||
104 | + override fun close() { | ||
105 | + interpreter?.close() | ||
106 | + interpreter = null | ||
107 | + gpuDelegate?.close() | ||
108 | + gpuDelegate = null | ||
109 | + } | ||
110 | + | ||
111 | + /** Returns value within [0,1]. */ | ||
112 | + private fun sigmoid(x: Float): Float { | ||
113 | + return (1.0f / (1.0f + exp(-x))) | ||
114 | + } | ||
115 | + | ||
116 | + /** | ||
117 | + * Scale the image to a byteBuffer of [-1,1] values. | ||
118 | + */ | ||
119 | + private fun initInputArray(bitmap: Bitmap): ByteBuffer { | ||
120 | + val bytesPerChannel = 4 | ||
121 | + val inputChannels = 3 | ||
122 | + val batchSize = 1 | ||
123 | + val inputBuffer = ByteBuffer.allocateDirect( | ||
124 | + batchSize * bytesPerChannel * bitmap.height * bitmap.width * inputChannels | ||
125 | + ) | ||
126 | + inputBuffer.order(ByteOrder.nativeOrder()) | ||
127 | + inputBuffer.rewind() | ||
128 | + | ||
129 | + val mean = 128.0f | ||
130 | + val std = 128.0f | ||
131 | + for (row in 0 until bitmap.height) { | ||
132 | + for (col in 0 until bitmap.width) { | ||
133 | + val pixelValue = bitmap.getPixel(col, row) | ||
134 | + inputBuffer.putFloat(((pixelValue shr 16 and 0xFF) - mean) / std) | ||
135 | + inputBuffer.putFloat(((pixelValue shr 8 and 0xFF) - mean) / std) | ||
136 | + inputBuffer.putFloat(((pixelValue and 0xFF) - mean) / std) | ||
137 | + } | ||
138 | + } | ||
139 | + return inputBuffer | ||
140 | + } | ||
141 | + | ||
142 | + /** Preload and memory map the model file, returning a MappedByteBuffer containing the model. */ | ||
143 | + private fun loadModelFile(path: String, context: Context): MappedByteBuffer { | ||
144 | + val fileDescriptor = context.assets.openFd(path) | ||
145 | + val inputStream = FileInputStream(fileDescriptor.fileDescriptor) | ||
146 | + return inputStream.channel.map( | ||
147 | + FileChannel.MapMode.READ_ONLY, fileDescriptor.startOffset, fileDescriptor.declaredLength | ||
148 | + ) | ||
149 | + } | ||
150 | + | ||
151 | + /** | ||
152 | + * Initializes an outputMap of 1 * x * y * z FloatArrays for the model processing to populate. | ||
153 | + */ | ||
154 | + private fun initOutputMap(interpreter: Interpreter): HashMap<Int, Any> { | ||
155 | + val outputMap = HashMap<Int, Any>() | ||
156 | + | ||
157 | + // 1 * 9 * 9 * 17 contains heatmaps | ||
158 | + val heatmapsShape = interpreter.getOutputTensor(0).shape() | ||
159 | + outputMap[0] = Array(heatmapsShape[0]) { | ||
160 | + Array(heatmapsShape[1]) { | ||
161 | + Array(heatmapsShape[2]) { FloatArray(heatmapsShape[3]) } | ||
162 | + } | ||
163 | + } | ||
164 | + | ||
165 | + // 1 * 9 * 9 * 34 contains offsets | ||
166 | + val offsetsShape = interpreter.getOutputTensor(1).shape() | ||
167 | + outputMap[1] = Array(offsetsShape[0]) { | ||
168 | + Array(offsetsShape[1]) { Array(offsetsShape[2]) { FloatArray(offsetsShape[3]) } } | ||
169 | + } | ||
170 | + | ||
171 | + // 1 * 9 * 9 * 32 contains forward displacements | ||
172 | + val displacementsFwdShape = interpreter.getOutputTensor(2).shape() | ||
173 | + outputMap[2] = Array(offsetsShape[0]) { | ||
174 | + Array(displacementsFwdShape[1]) { | ||
175 | + Array(displacementsFwdShape[2]) { FloatArray(displacementsFwdShape[3]) } | ||
176 | + } | ||
177 | + } | ||
178 | + | ||
179 | + // 1 * 9 * 9 * 32 contains backward displacements | ||
180 | + val displacementsBwdShape = interpreter.getOutputTensor(3).shape() | ||
181 | + outputMap[3] = Array(displacementsBwdShape[0]) { | ||
182 | + Array(displacementsBwdShape[1]) { | ||
183 | + Array(displacementsBwdShape[2]) { FloatArray(displacementsBwdShape[3]) } | ||
184 | + } | ||
185 | + } | ||
186 | + | ||
187 | + return outputMap | ||
188 | + } | ||
189 | + | ||
190 | + /** | ||
191 | + * Estimates the pose for a single person. | ||
192 | + * args: | ||
193 | + * bitmap: image bitmap of frame that should be processed | ||
194 | + * returns: | ||
195 | + * person: a Person object containing data about keypoint locations and confidence scores | ||
196 | + */ | ||
197 | + fun estimateSinglePose(bitmap: Bitmap): Person { | ||
198 | + val estimationStartTimeNanos = SystemClock.elapsedRealtimeNanos() | ||
199 | + val inputArray = arrayOf(initInputArray(bitmap)) | ||
200 | + Log.i( | ||
201 | + "posenet", | ||
202 | + String.format( | ||
203 | + "Scaling to [-1,1] took %.2f ms", | ||
204 | + 1.0f * (SystemClock.elapsedRealtimeNanos() - estimationStartTimeNanos) / 1_000_000 | ||
205 | + ) | ||
206 | + ) | ||
207 | + | ||
208 | + val outputMap = initOutputMap(getInterpreter()) | ||
209 | + | ||
210 | + val inferenceStartTimeNanos = SystemClock.elapsedRealtimeNanos() | ||
211 | + getInterpreter().runForMultipleInputsOutputs(inputArray, outputMap) | ||
212 | + lastInferenceTimeNanos = SystemClock.elapsedRealtimeNanos() - inferenceStartTimeNanos | ||
213 | + Log.i( | ||
214 | + "posenet", | ||
215 | + String.format("Interpreter took %.2f ms", 1.0f * lastInferenceTimeNanos / 1_000_000) | ||
216 | + ) | ||
217 | + | ||
218 | + val heatmaps = outputMap[0] as Array<Array<Array<FloatArray>>> | ||
219 | + val offsets = outputMap[1] as Array<Array<Array<FloatArray>>> | ||
220 | + | ||
221 | + val height = heatmaps[0].size | ||
222 | + val width = heatmaps[0][0].size | ||
223 | + val numKeypoints = heatmaps[0][0][0].size | ||
224 | + | ||
225 | + // Finds the (row, col) locations of where the keypoints are most likely to be. | ||
226 | + val keypointPositions = Array(numKeypoints) { Pair(0, 0) } | ||
227 | + for (keypoint in 0 until numKeypoints) { | ||
228 | + var maxVal = heatmaps[0][0][0][keypoint] | ||
229 | + var maxRow = 0 | ||
230 | + var maxCol = 0 | ||
231 | + for (row in 0 until height) { | ||
232 | + for (col in 0 until width) { | ||
233 | + if (heatmaps[0][row][col][keypoint] > maxVal) { | ||
234 | + maxVal = heatmaps[0][row][col][keypoint] | ||
235 | + maxRow = row | ||
236 | + maxCol = col | ||
237 | + } | ||
238 | + } | ||
239 | + } | ||
240 | + keypointPositions[keypoint] = Pair(maxRow, maxCol) | ||
241 | + } | ||
242 | + | ||
243 | + // Calculating the x and y coordinates of the keypoints with offset adjustment. | ||
244 | + val xCoords = IntArray(numKeypoints) | ||
245 | + val yCoords = IntArray(numKeypoints) | ||
246 | + val confidenceScores = FloatArray(numKeypoints) | ||
247 | + keypointPositions.forEachIndexed { idx, position -> | ||
248 | + val positionY = keypointPositions[idx].first | ||
249 | + val positionX = keypointPositions[idx].second | ||
250 | + yCoords[idx] = ( | ||
251 | + position.first / (height - 1).toFloat() * bitmap.height + | ||
252 | + offsets[0][positionY][positionX][idx] | ||
253 | + ).toInt() | ||
254 | + xCoords[idx] = ( | ||
255 | + position.second / (width - 1).toFloat() * bitmap.width + | ||
256 | + offsets[0][positionY] | ||
257 | + [positionX][idx + numKeypoints] | ||
258 | + ).toInt() | ||
259 | + confidenceScores[idx] = sigmoid(heatmaps[0][positionY][positionX][idx]) | ||
260 | + } | ||
261 | + | ||
262 | + val person = Person() | ||
263 | + val keypointList = Array(numKeypoints) { KeyPoint() } | ||
264 | + var totalScore = 0.0f | ||
265 | + enumValues<BodyPart>().forEachIndexed { idx, it -> | ||
266 | + keypointList[idx].bodyPart = it | ||
267 | + keypointList[idx].position.x = xCoords[idx] | ||
268 | + keypointList[idx].position.y = yCoords[idx] | ||
269 | + keypointList[idx].score = confidenceScores[idx] | ||
270 | + totalScore += confidenceScores[idx] | ||
271 | + } | ||
272 | + | ||
273 | + person.keyPoints = keypointList.toList() | ||
274 | + person.score = totalScore / numKeypoints | ||
275 | + | ||
276 | + return person | ||
277 | + } | ||
278 | +} |
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. | 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. |
2 | 2 | ||
3 | buildscript { | 3 | buildscript { |
4 | + ext.kotlin_version = '1.3.72' | ||
4 | repositories { | 5 | repositories { |
5 | google() | 6 | google() |
6 | jcenter() | 7 | jcenter() |
... | @@ -8,7 +9,8 @@ buildscript { | ... | @@ -8,7 +9,8 @@ buildscript { |
8 | } | 9 | } |
9 | dependencies { | 10 | dependencies { |
10 | classpath 'com.android.tools.build:gradle:3.5.1' | 11 | classpath 'com.android.tools.build:gradle:3.5.1' |
11 | - | 12 | + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |
13 | + | ||
12 | // NOTE: Do not place your application dependencies here; they belong | 14 | // NOTE: Do not place your application dependencies here; they belong |
13 | // in the individual module build.gradle files | 15 | // in the individual module build.gradle files |
14 | } | 16 | } | ... | ... |
-
Please register or login to post a comment