diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..d2b70a8 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +PiDroid \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d5d35ec --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..309a5d5 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + signingConfigs { + pidroid { + keyAlias 'androiddebugkey' + keyPassword 'android' + storeFile file('/home/adrian/.android/debug.keystore') + storePassword 'android' + } + } + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.suaro.pidroidapp" + minSdkVersion 23 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-build.pro' + signingConfig signingConfigs.pidroid + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation project(path: ':pidroid') + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/suaro/pidroid/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/suaro/pidroid/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..f67ac82 --- /dev/null +++ b/app/src/androidTest/java/com/suaro/pidroid/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.suaro.pidroidapp + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.suaro.pidroidapp", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ec6eee2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/ImageActivity.kt b/app/src/main/java/com/suaro/pidroidapp/ImageActivity.kt new file mode 100644 index 0000000..912f874 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/ImageActivity.kt @@ -0,0 +1,94 @@ +package com.suaro.pidroidapp + +import android.content.res.AssetManager +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.os.Bundle +import android.os.Handler +import android.util.Log +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentTransaction +import com.suaro.pidroid.Pidroid +import com.suaro.pidroid.core.PidroidConfig +import com.suaro.pidroidapp.camera.Camera +import com.suaro.pidroidapp.capturer.Capturer +import com.suaro.pidroidapp.capturer.view.CapturerFragment +import com.suaro.pidroidapp.core.CameraPreviewListener +import java.util.* + + +class ImageActivity : AppCompatActivity(), Camera.View, Capturer.View{ + + private lateinit var capturerLayout: FrameLayout + private lateinit var imageTest: ImageView + + private lateinit var capturerFragment: CapturerFragment + + private var cameraPreviewListenerList: ArrayList? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(com.suaro.pidroidapp.R.layout.activity_image) + + capturerLayout = findViewById(R.id.capturer_fragment) + imageTest = findViewById(R.id.photo) + setupNative() + } + + private fun setupNative() { + val pidroidConfig = PidroidConfig() + Pidroid.setup(this, pidroidConfig) + } + + override fun onPause() { + super.onPause() + } + + override fun onResume() { + super.onResume() + + val handler: Handler = Handler(); + onCameraStart() + handler.postDelayed({ + val drawable: BitmapDrawable = imageTest.getDrawable() as BitmapDrawable + val bitmap: Bitmap = drawable.bitmap + onPreviewFrame(bitmap) + }, 1000) + + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onCameraStart() { + loadCapturerFragment() + } + + override fun onPreviewFrame(bitmap: Bitmap) { + cameraPreviewListenerList!!.forEach { + it.onPreviewFrame(bitmap) + } + } + + override fun onCapturerFinish() { + Log.i("Capturer", "Finish") + } + + + fun loadCapturerFragment() { + capturerFragment = CapturerFragment.newInstance(); + val transaction: FragmentTransaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.capturer_fragment, capturerFragment) + transaction.commit() + } + + fun addCameraPreviewListener(listener: CameraPreviewListener) { + if (cameraPreviewListenerList == null) { + cameraPreviewListenerList = ArrayList() + } + cameraPreviewListenerList!!.add(listener) + } +} diff --git a/app/src/main/java/com/suaro/pidroidapp/MainActivity.kt b/app/src/main/java/com/suaro/pidroidapp/MainActivity.kt new file mode 100644 index 0000000..ccec0ae --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/MainActivity.kt @@ -0,0 +1,92 @@ +package com.suaro.pidroidapp + +import android.content.res.AssetManager +import android.graphics.Bitmap +import android.os.Bundle +import android.util.Log +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentTransaction +import com.suaro.pidroid.Pidroid +import com.suaro.pidroid.core.PidroidConfig +import com.suaro.pidroidapp.camera.Camera +import com.suaro.pidroidapp.camera.view.CameraFragment +import com.suaro.pidroidapp.capturer.Capturer +import com.suaro.pidroidapp.capturer.view.CapturerFragment +import com.suaro.pidroidapp.core.CameraPreviewListener +import java.util.* + + +class MainActivity : AppCompatActivity(), Camera.View, Capturer.View{ + + private lateinit var capturerLayout: FrameLayout + private lateinit var cameraLayout: FrameLayout + + private lateinit var capturerFragment: CapturerFragment + private lateinit var cameraFragment: CameraFragment + + private var cameraPreviewListenerList: ArrayList? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + cameraLayout = findViewById(R.id.camera_fragment) + capturerLayout = findViewById(R.id.capturer_fragment) + + setupNative() + loadCameraFragment() + } + + private fun setupNative() { + val pidroidConfig = PidroidConfig() + Pidroid.setup(this, pidroidConfig) + } + + override fun onPause() { + super.onPause() + } + + override fun onResume() { + super.onResume() + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onCameraStart() { + loadCapturerFragment() + } + + override fun onPreviewFrame(bitmap: Bitmap) { + cameraPreviewListenerList!!.forEach { + it.onPreviewFrame(bitmap) + } + } + + override fun onCapturerFinish() { + Log.i("Capturer", "Finish") + } + + fun loadCameraFragment() { + cameraFragment = CameraFragment.newInstance(); + val transaction: FragmentTransaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.camera_fragment, cameraFragment) + transaction.commit() + } + + fun loadCapturerFragment() { + capturerFragment = CapturerFragment.newInstance(); + val transaction: FragmentTransaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.capturer_fragment, capturerFragment) + transaction.commit() + } + + fun addCameraPreviewListener(listener: CameraPreviewListener) { + if (cameraPreviewListenerList == null) { + cameraPreviewListenerList = ArrayList() + } + cameraPreviewListenerList!!.add(listener) + } +} diff --git a/app/src/main/java/com/suaro/pidroidapp/camera/Camera.kt b/app/src/main/java/com/suaro/pidroidapp/camera/Camera.kt new file mode 100644 index 0000000..b887c1d --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/camera/Camera.kt @@ -0,0 +1,14 @@ +package com.suaro.pidroidapp.camera + +import android.graphics.Bitmap + +interface Camera { + interface View{ + fun onCameraStart() + fun onPreviewFrame(bitmap: Bitmap) + } + + interface Presenter { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/camera/entity/Flash.kt b/app/src/main/java/com/suaro/pidroidapp/camera/entity/Flash.kt new file mode 100644 index 0000000..59d8f2b --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/camera/entity/Flash.kt @@ -0,0 +1,5 @@ +package com.suaro.pidroidapp.camera.entity + +enum class Flash { + ON, OFF +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/camera/view/AutoFitTextureView.kt b/app/src/main/java/com/suaro/pidroidapp/camera/view/AutoFitTextureView.kt new file mode 100644 index 0000000..a0ccbfe --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/camera/view/AutoFitTextureView.kt @@ -0,0 +1,49 @@ +package com.suaro.pidroidapp.camera.view + +import android.content.Context +import android.util.AttributeSet +import android.view.TextureView + +class AutoFitTextureView : TextureView { + + private var mRatioWidth = 0 + private var mRatioHeight = 0 + + constructor(context: Context) : this(context, null) + + constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0) + + constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context, attributeSet, defStyle) + + /** + * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio + * calculated from the parameters. Note that the actual sizes of parameters don't matter, that + * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result. + * + * @param width Relative horizontal size + * @param height Relative vertical size + */ + fun setAspectRatio(width: Int, height: Int) { + if (width < 0 || height < 0) { + throw IllegalArgumentException("Size cannot be negative.") + } + mRatioWidth = width + mRatioHeight = height + requestLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + if (0 == mRatioWidth || 0 == mRatioHeight) { + setMeasuredDimension(width, height) + } else { + if (width < height * mRatioWidth / mRatioHeight) { + setMeasuredDimension(width, width * mRatioHeight / mRatioWidth) + } else { + setMeasuredDimension(height * mRatioWidth / mRatioHeight, height) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/camera/view/CameraFragment.kt b/app/src/main/java/com/suaro/pidroidapp/camera/view/CameraFragment.kt new file mode 100644 index 0000000..46feacf --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/camera/view/CameraFragment.kt @@ -0,0 +1,127 @@ +package com.suaro.pidroidapp.camera.view + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.suaro.pidroidapp.R +import com.suaro.pidroidapp.camera.Camera + +class CameraFragment : Fragment() { + + private val CAMERA_PERMISSION: Int = 1001 + private lateinit var cameraView: Camera.View + private lateinit var camera2: CameraManager + private lateinit var v: View + + override fun onAttach(context: Context) { + super.onAttach(context) + cameraView = if (context is Camera.View) { + context + } else { + throw ClassCastException( + context.toString() + " must implement Camera.View" + ) + } + } + + override fun onCreate(savedInstance: Bundle?) { + super.onCreate(savedInstance) + + } + + override fun onCreateView( + layoutInflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + v = layoutInflater.inflate(R.layout.camera_fragment, container, false) + ?: return null + setRetainInstance(true) + + if (ContextCompat.checkSelfPermission( + activity!!, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) + + camera2 = CameraManager(activity!!, v.findViewById(R.id.camera_view), cameraView) + else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ActivityCompat.requestPermissions(activity!!, arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), CAMERA_PERMISSION) + } + else { + camera2 = CameraManager(activity!!, v.findViewById(R.id.camera_view), cameraView) + } + + } + + return v + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + + } + + override fun onResume() { + super.onResume() + if (::camera2.isInitialized) { + camera2.start() + } + + } + + override fun onPause() { + super.onPause() + if (::camera2.isInitialized) { + camera2.stop() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + CAMERA_PERMISSION -> { + // If request is cancelled, the result arrays are empty. + if ((grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + camera2 = CameraManager(activity!!, v.findViewById(R.id.camera_view), cameraView) + if (::camera2.isInitialized) { + camera2.start() + } + } else { + Toast.makeText(this.context, "You must accept camera permissions to continue", Toast.LENGTH_LONG).show() + } + return + } + + else -> { + // Ignore all other requests. + } + } + } + + companion object { + private val TAG = CameraFragment::class.java.simpleName + fun newInstance(): CameraFragment { + val f = CameraFragment() + //val args = Bundle() + //args.putParcelable("instructions_item", item) + //f.setArguments(args) + return f + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/camera/view/CameraManager.kt b/app/src/main/java/com/suaro/pidroidapp/camera/view/CameraManager.kt new file mode 100644 index 0000000..8b72527 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/camera/view/CameraManager.kt @@ -0,0 +1,493 @@ +package com.suaro.pidroidapp.camera.view + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.* +import android.graphics.Matrix.ScaleToFit +import android.graphics.Point +import android.hardware.camera2.* +import android.hardware.camera2.CameraManager +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import android.util.Size +import android.util.SparseIntArray +import android.view.Surface +import android.view.TextureView +import androidx.core.content.ContextCompat +import com.suaro.pidroidapp.camera.entity.Flash +import java.util.* +import java.util.Collections.singletonList +import kotlin.Comparator + + +class CameraManager(private val activity: Activity, private val textureView: AutoFitTextureView, private val cameraView: com.suaro.pidroidapp.camera.Camera.View) { + + private var first: Boolean = true + + private val cameraManager: CameraManager = + textureView.context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + private var cameraFacing = CameraCharacteristics.LENS_FACING_FRONT + private var previewSize: Size? = null + //Current Camera id + private var cameraId = "-1" + private var backgroundHandler: Handler? = null + private var backgroundThread: HandlerThread? = null + private var cameraDevice: CameraDevice? = null + // + private var cameraCaptureSession: CameraCaptureSession? = null + // capture request builder for camera. + private var captureRequestBuilder: CaptureRequest.Builder? = null + // capture request generated by above builder. + private var captureRequest: CaptureRequest? = null + private var flash = Flash.OFF + + private var surface: Surface? = null + private var isFlashSupported = true + private var mSensorOrientation = 0 + + + private companion object { + + private val ORIENTATIONS = SparseIntArray() + + init { + ORIENTATIONS.append(Surface.ROTATION_0, 90) + ORIENTATIONS.append(Surface.ROTATION_90, 0) + ORIENTATIONS.append(Surface.ROTATION_180, 270) + ORIENTATIONS.append(Surface.ROTATION_270, 180) + } + + + private const val MAX_PREVIEW_WIDTH = 640 + private const val MAX_PREVIEW_HEIGHT = 640 + + + /** + * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that + * is at least as large as the respective texture view size, and that is at most as large as the + * respective max size, and whose aspect ratio matches with the specified value. If such size + * doesn't exist, choose the largest one that is at most as large as the respective max size, + * and whose aspect ratio matches with the specified value. + * + * @param choices The list of sizes that the camera supports for the intended output + * class + * @param textureViewWidth The width of the texture view relative to sensor coordinate + * @param textureViewHeight The height of the texture view relative to sensor coordinate + * @param maxWidth The maximum width that can be chosen + * @param maxHeight The maximum height that can be chosen + * @param aspectRatio The aspect ratio + * @return The optimal {@code Size}, or an arbitrary one if none were big enough + */ + private fun chooseOptimalSize( + choices: Array, textureViewWidth: Int, + textureViewHeight: Int, maxWidth: Int, maxHeight: Int, aspectRatio: Size + ): Size { + + // Collect the supported resolutions that are at least as big as the preview Surface + val bigEnough = arrayListOf() + // Collect the supported resolutions that are smaller than the preview Surface + val notBigEnough = arrayListOf() + val w = aspectRatio.width + val h = aspectRatio.height + for (option in choices) { + if (option.width <= maxWidth && option.height <= maxHeight + ) { + if (option.width >= textureViewWidth && option.height >= textureViewHeight) { + bigEnough.add(option) + } else { + notBigEnough.add(option) + } + } + } + + // Pick the smallest of those big enough. If there is no one big enough, pick the + // largest of those not big enough. + + return when { + bigEnough.isNotEmpty() -> Collections.min(bigEnough, compareSizesByArea) + notBigEnough.isNotEmpty() -> Collections.max(notBigEnough, compareSizesByArea) + else -> { + Log.e("Camera", "Couldn't find any suitable preview size") + choices[0] + } + } + } + + private val compareSizesByArea = Comparator { lhs, rhs -> + // We cast here to ensure the multiplications won't overflow + java.lang.Long.signum(lhs.width.toLong() * lhs.height - rhs.width.toLong() * rhs.height) + } + + } + + private val surfaceTextureListener = object : TextureView.SurfaceTextureListener { + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { + configureTransform(width, height) + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { + + if(first) { + //First frame is deleted because is empty. + first = false + return; + } + + val frame = Bitmap.createBitmap( + textureView.width, + textureView.height, + Bitmap.Config.ARGB_8888 + ) + textureView.getBitmap(frame) + + cameraView.onPreviewFrame(frame) + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + return true + } + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + openCamera(width, height) + + } + } + + private val cameraStateCallback = object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) { + this@CameraManager.cameraDevice = camera + createPreviewSession() + } + + override fun onDisconnected(camera: CameraDevice) { + camera.close() + this@CameraManager.cameraDevice = null + } + + override fun onError(camera: CameraDevice, error: Int) { + } + } + + fun start() { + openBackgroundThread() + if (textureView.isAvailable) { + openCamera(textureView.width, textureView.height) + } else { + textureView.surfaceTextureListener = surfaceTextureListener + } + + } + + fun stop() { + closeCamera() + closeBackgroundThread() + + } + + + private fun closeCamera() { + if (cameraCaptureSession != null) { + cameraCaptureSession!!.close() + cameraCaptureSession = null + // cameraSessionClosed = true + } + + if (cameraDevice != null) { + cameraDevice!!.close() + cameraDevice = null + } + } + + private fun closeBackgroundThread() { + if (backgroundHandler != null) { + backgroundThread!!.quitSafely() + backgroundThread = null + backgroundHandler = null + } + } + + + private fun openCamera(width: Int, height: Int) { + if (ContextCompat.checkSelfPermission( + textureView.context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) { + setUpCameraOutputs(width, height) + configureTransform(width, height) + + cameraManager.openCamera(cameraId, cameraStateCallback, backgroundHandler) + cameraView.onCameraStart() + + } else Log.e("Camera2", "Requires Camera Permission") + } + + /** + * Sets up member variables related to camera. + * + * @param width The width of available size for camera preview + * @param height The height of available size for camera preview + */ + private fun setUpCameraOutputs(width: Int, height: Int) { + try { + for (cameraId in cameraManager.cameraIdList) { + val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) + val cameraFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + + if (cameraFacing == this.cameraFacing) { + val streamConfigurationMap = cameraCharacteristics.get( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP + ) +// For still image captures, we use the largest available size. + val largest = Collections.max( + streamConfigurationMap?.getOutputSizes(ImageFormat.JPEG)?.toList(), + compareSizesByArea + ) + +// Find out if we need to swap dimension to get the preview size relative to sensor +// coordinate. + val displayRotation = activity.windowManager.defaultDisplay.rotation + + //noinspection ConstantConditions + mSensorOrientation = cameraCharacteristics[CameraCharacteristics.SENSOR_ORIENTATION] ?: 0 + + var swappedDimensions = false + + when (displayRotation) { + Surface.ROTATION_0 -> { + } + Surface.ROTATION_90 -> { + } + Surface.ROTATION_180 -> { + swappedDimensions = + mSensorOrientation == 90 || mSensorOrientation == 270 + } + Surface.ROTATION_270 -> { + swappedDimensions = mSensorOrientation == 0 || mSensorOrientation == 180 + } + else -> Log.e("Camera2", "Display rotation is invalid: $displayRotation") + + } + + val displaySize = Point() + + activity.windowManager.defaultDisplay.getSize(displaySize) + + var rotatedPreviewWidth = width + var rotatedPreviewHeight = height + var maxPreviewWidth = displaySize.x + var maxPreviewHeight = displaySize.y + + if (swappedDimensions) { + rotatedPreviewWidth = height + rotatedPreviewHeight = width + maxPreviewWidth = displaySize.y + maxPreviewHeight = displaySize.x + } + + if (maxPreviewWidth > MAX_PREVIEW_WIDTH) { + maxPreviewWidth = MAX_PREVIEW_WIDTH + } + + if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) { + maxPreviewHeight = MAX_PREVIEW_HEIGHT + } + + + // Danger, W.R.! Attempting to use too large a preview size could exceed the camera + // bus' bandwidth limitation, resulting in gorgeous previews but the storage of + // garbage capture data. + previewSize = chooseOptimalSize( + streamConfigurationMap!!.getOutputSizes(SurfaceTexture::class.java), + rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth, + maxPreviewHeight, largest + ) + + // We fit the aspect ratio of TextureView to the size of preview we picked. + val orientation = activity.resources.configuration.orientation + + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + textureView.setAspectRatio( + previewSize!!.width, previewSize!!.height + ) + } else { + textureView.setAspectRatio( + previewSize!!.height, previewSize!!.width + ) + } + // check flash support + val flashSupported = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) + isFlashSupported = flashSupported == null ?: false + + this.cameraId = cameraId + + return + } + } + } catch (e: CameraAccessException) { + e.printStackTrace() + } + } + + /** + * Configures the necessary [android.graphics.Matrix] transformation to `mTextureView`. + * This method should be called after the camera preview size is determined in + * setUpCameraOutputs and also the size of `mTextureView` is fixed. + * + * @param viewWidth The width of `mTextureView` + * @param viewHeight The height of `mTextureView` + */ + private fun configureTransform(viewWidth: Int, viewHeight: Int) { + val rotation = activity.windowManager.defaultDisplay.rotation + val matrix = Matrix() + val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) + val bufferRect = RectF( + 0f, + 0f, + previewSize!!.height.toFloat(), + previewSize!!.width.toFloat() + ) + val centerX = viewRect.centerX() + val centerY = viewRect.centerY() + if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { + bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) + matrix.setRectToRect(viewRect, bufferRect, ScaleToFit.FILL) + val scale = Math.max( + viewHeight.toFloat() / previewSize!!.height, + viewWidth.toFloat() / previewSize!!.width + ) + matrix.postScale(scale, scale, centerX, centerY) + matrix.postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY) + } else if (Surface.ROTATION_180 == rotation) { + matrix.postRotate(180f, centerX, centerY) + } + textureView.setTransform(matrix) + } + + /** + * Retrieves the JPEG orientation from the specified screen rotation. + * + * @param rotation The screen rotation. + * @return The JPEG orientation (one of 0, 90, 270, and 360) + */ + private fun getOrientation(rotation: Int) = + + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) + // We have to take that into account and rotate JPEG properly. + // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. + // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. + (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360 + + + private fun openBackgroundThread() { + backgroundThread = HandlerThread("camera_background_thread") + backgroundThread!!.start() + backgroundHandler = Handler(backgroundThread!!.looper) + } + + // Creates a new camera preview session + private fun createPreviewSession() { + + try { + + val surfaceTexture = textureView.surfaceTexture +// We configure the size of default buffer to be the size of camera preview we want. + surfaceTexture?.setDefaultBufferSize(previewSize!!.width, previewSize!!.height) + +// This is the output Surface we need to start preview. + if (surface == null) + surface = Surface(surfaceTexture) + + val previewSurface = surface + + //val mImageSurface: Surface = imageReader!!.getSurface() + + captureRequestBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) + //captureRequestBuilder!!.addTarget(mImageSurface) + captureRequestBuilder!!.addTarget(previewSurface!!) + +// Here, we create a CameraCaptureSession for camera preview. + + cameraDevice!!.createCaptureSession( + singletonList(previewSurface), + object : CameraCaptureSession.StateCallback() { + + override fun onConfigured(cameraCaptureSession: CameraCaptureSession) { + if (cameraDevice == null) { + return + } + + try { +// When session is ready we start displaying preview. + this@CameraManager.cameraCaptureSession = cameraCaptureSession + // cameraSessionClosed = false + + this@CameraManager.configureCapture() + + } catch (e: CameraAccessException) { + e.printStackTrace() + } + + } + + override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) { + + } + }, backgroundHandler + ) + } catch (e: CameraAccessException) { + e.printStackTrace() + } + + } + + private fun configureCapture() { + if (flash == Flash.ON) { + captureRequestBuilder!!.set( + CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_TORCH + ) + } else { + captureRequestBuilder!!.set( + CaptureRequest.FLASH_MODE, + CaptureRequest.FLASH_MODE_OFF + ) + } + + captureRequestBuilder!!.set( + CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON + ) + + captureRequestBuilder!!.set( + CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE + ) + +// Finally, we start displaying the camera preview. + captureRequest = captureRequestBuilder!!.build() + + this@CameraManager.cameraCaptureSession!!.setRepeatingRequest( + captureRequest!!, + null, + backgroundHandler + ) + } + + fun setFlash(flash: Flash) { + + this.flash = flash + + if (textureView.context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH) && !cameraId.equals( + "-1" + )) { + configureCapture() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/Capturer.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/Capturer.kt new file mode 100644 index 0000000..224ab67 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/Capturer.kt @@ -0,0 +1,11 @@ +package com.suaro.pidroidapp.capturer + +interface Capturer { + interface View{ + fun onCapturerFinish() + } + + interface Presenter { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/ImageProcessor.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/ImageProcessor.kt new file mode 100644 index 0000000..444ea08 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/ImageProcessor.kt @@ -0,0 +1,26 @@ +package com.suaro.pidroidapp.capturer + +import android.graphics.Bitmap +import com.suaro.pidroid.core.FaceDetectionResult + + +interface ImageProcessor { + interface Interactor { + fun detectFace(intArray: IntArray, width: Int, height: Int) + fun detectFace(bitmap: Bitmap) + + interface FinishListener { + fun onFaceDetectorFinish(info: FaceDetectionResult) + } + } + + interface View { + fun onFaceDetectorFinish(info: FaceDetectionResult) + } + + interface Presenter { + fun detectFace(intArray: IntArray, width: Int, height: Int) + fun detectFace(bitmap: Bitmap) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/entity/Circle.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/entity/Circle.kt new file mode 100644 index 0000000..ef87789 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/entity/Circle.kt @@ -0,0 +1,15 @@ +package com.suaro.pidroidapp.capturer.entity + +import com.suaro.pidroid.core.Point + + +class Circle { + + constructor(center: Point, radius: Int) { + this.center = center + this.radius = radius + } + + var center: Point = Point(0,0) + var radius: Int = 0 +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/BaseFrameProcessor.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/BaseFrameProcessor.kt new file mode 100644 index 0000000..a1038b5 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/BaseFrameProcessor.kt @@ -0,0 +1,112 @@ +package com.suaro.pidroidapp.capturer.interactor + +import android.graphics.Bitmap +import android.os.SystemClock +import com.suaro.pidroidapp.capturer.ImageProcessor +import com.suaro.pidroidapp.core.Utils +import java.nio.ByteBuffer +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +abstract class BaseFrameProcessor(protected var listener: ImageProcessor.Interactor.FinishListener) : + Runnable { + + private val mStartTimeMillis = SystemClock.elapsedRealtime() + + // This lock guards all of the member variables below. + protected val mLock = ReentrantLock() + protected val condition = mLock.newCondition() + + protected var mActive = true + private val processingThread: Thread? = null + + // These pending variables hold the state associated with the new frame awaiting processing. + private var mPendingTimeMillis: Long = 0 + private var mPendingFrameId = 0 + protected var mPendingFrameData: IntArray? = null + + protected var previewWidth = 0 + protected var previewHeight = 0 + private var proccessingThread: Thread? = null + + + fun setProcessingThread(proccessingThread: Thread?) { + mLock.withLock { + this.proccessingThread = proccessingThread + condition.signalAll() + } + } + + /** + * Marks the runnable as active/not active. Signals any blocked threads to continue. + */ + fun setActive(active: Boolean) { + mLock.withLock { + mActive = active + condition.signalAll() + } + } + + /** + * Sets the frame data received from the camera. This adds the previous unused frame buffer + * (if present) back to the camera, and keeps a pending reference to the frame data for + * future use. + */ + fun setNextFrame( + data: IntArray?, + previewWidth: Int, + previewHeight: Int + ) { + mLock.withLock { + this.previewHeight = previewHeight + this.previewWidth = previewWidth + + if (mPendingFrameData != null) { + mPendingFrameData = null + } + + // Timestamp and frame ID are maintained here, which will give downstream code some + // idea of the timing of frames received and when frames were dropped along the way. + mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis + mPendingFrameId++ + mPendingFrameData = data + + // Notify the processor thread if it is waiting on the next frame (see below). + condition.signalAll() + } + } + + /** + * Sets the frame data received from the camera. This adds the previous unused frame buffer + * (if present) back to the camera, and keeps a pending reference to the frame data for + * future use. + */ + fun setNextFrame(data: Bitmap? + ) { + mLock.withLock { + this.previewHeight = data!!.height + this.previewWidth = data.width + + if (mPendingFrameData != null) { + mPendingFrameData = null + } + + // Timestamp and frame ID are maintained here, which will give downstream code some + // idea of the timing of frames received and when frames were dropped along the way. + mPendingTimeMillis = SystemClock.elapsedRealtime() - mStartTimeMillis + mPendingFrameId++ + mPendingFrameData = Utils.getBytes(data)!! + + // Notify the processor thread if it is waiting on the next frame (see below). + condition.signalAll() + } + } + + override fun run() { + + } + + init { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/FaceDetectorFrameProcessor.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/FaceDetectorFrameProcessor.kt new file mode 100644 index 0000000..d304e0a --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/FaceDetectorFrameProcessor.kt @@ -0,0 +1,51 @@ +package com.suaro.pidroidapp.capturer.interactor + +import android.util.Log +import com.suaro.pidroid.Pidroid +import com.suaro.pidroid.core.FaceDetectionResult +import com.suaro.pidroidapp.capturer.ImageProcessor +import kotlin.concurrent.withLock + +class FaceDetectorFrameProcessor(listener: ImageProcessor.Interactor.FinishListener) : + BaseFrameProcessor(listener) { + + companion object { + private val TAG = FaceDetectorFrameProcessor::class.java.simpleName + } + + fun detect(byteArray: IntArray, width: Int, height: Int): FaceDetectionResult { + var dInfo = FaceDetectionResult() + Pidroid.detectFace(byteArray, width, height, dInfo) + return dInfo + } + + override fun run() { + super.run() + var outputFrame: IntArray + + while (true) { + mLock.withLock { + while (mActive && mPendingFrameData == null) { + try { + condition.await() + } catch (e: InterruptedException) { + return + } + } + if (!mActive) { + return + } + outputFrame = mPendingFrameData!! + mPendingFrameData = null + } + try { + val detectionInfo: FaceDetectionResult = this.detect(outputFrame, previewWidth, previewHeight) + listener.onFaceDetectorFinish(detectionInfo) + } catch (t: Throwable) { + Log.e(FaceDetectorFrameProcessor.TAG, t.message.toString()) + } + } + + } +} + diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/ImageProcessorInteractor.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/ImageProcessorInteractor.kt new file mode 100644 index 0000000..4d75c2a --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/interactor/ImageProcessorInteractor.kt @@ -0,0 +1,30 @@ +package com.suaro.pidroidapp.capturer.interactor + +import android.content.Context +import android.graphics.Bitmap +import com.suaro.pidroidapp.capturer.ImageProcessor + +class ImageProcessorInteractor(private var context: Context, private var listener: ImageProcessor.Interactor.FinishListener) : ImageProcessor.Interactor { + + private var faceDetectorFrameProcessor: FaceDetectorFrameProcessor + + private var faceDetectorProcessingThread: Thread + + init { + faceDetectorFrameProcessor = FaceDetectorFrameProcessor(listener) + faceDetectorProcessingThread = Thread(faceDetectorFrameProcessor) + + faceDetectorFrameProcessor.setProcessingThread(faceDetectorProcessingThread) + faceDetectorFrameProcessor.setActive(true) + + faceDetectorProcessingThread.start() + } + + override fun detectFace(intArray: IntArray, width: Int, height: Int) { + faceDetectorFrameProcessor.setNextFrame(intArray, width, height) + } + + override fun detectFace(bitmap: Bitmap) { + faceDetectorFrameProcessor.setNextFrame(bitmap) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/presenter/ImageProcessorPresenter.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/presenter/ImageProcessorPresenter.kt new file mode 100644 index 0000000..1e24de3 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/presenter/ImageProcessorPresenter.kt @@ -0,0 +1,28 @@ +package com.suaro.pidroidapp.capturer.presenter + +import android.content.Context +import android.graphics.Bitmap +import com.suaro.pidroid.core.FaceDetectionResult +import com.suaro.pidroidapp.capturer.ImageProcessor +import com.suaro.pidroidapp.capturer.interactor.ImageProcessorInteractor + +class ImageProcessorPresenter(private var context: Context, private val view: ImageProcessor.View): ImageProcessor.Presenter, ImageProcessor.Interactor.FinishListener { + + private val interactor: ImageProcessorInteractor + + init { + interactor = ImageProcessorInteractor(context, this) + } + + override fun detectFace(intArray: IntArray, width: Int, height: Int) { + interactor.detectFace(intArray, width, height) + } + + override fun detectFace(bitmap: Bitmap) { + interactor.detectFace(bitmap) + } + + override fun onFaceDetectorFinish(info: FaceDetectionResult) { + view.onFaceDetectorFinish(info) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/view/CanvasView.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/view/CanvasView.kt new file mode 100644 index 0000000..50663bd --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/view/CanvasView.kt @@ -0,0 +1,153 @@ +package com.suaro.pidroidapp.capturer.view + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import com.suaro.pidroidapp.capturer.entity.Circle +import com.suaro.pidroid.core.Point + +class CanvasView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): View(context, attrs, defStyleAttr) { + + private val RECT_BORDER_RADIUS = 25f; + private val RECT_BORDER_WIDTH = 10f; + + var faces: ArrayList = ArrayList(); + var eyes: ArrayList = ArrayList(); + var landmarks: ArrayList = ArrayList(); + + var paint: Paint = Paint() + + private var mRatioWidth: Double = 0.0 + private var mRatioHeight: Double = 0.0 + + private var lastRatioWidth: Double = 0.0 + private var lastRatioHeight: Double = 0.0 + + private var currentRatio: Double = 0.0 + private var currentWidth: Double = 0.0 + private var currentHeight: Double = 0.0 + + private var widthScaleFactor: Double = 1.0; + private var heightScaleFactor: Double = 1.0; + + fun drawRect(rect: Rect, canvas: Canvas?, color: Int = Color.GREEN) {; + + paint.setStyle(Paint.Style.STROKE) + paint.setColor(color) + paint.setStrokeWidth(RECT_BORDER_WIDTH); + + canvas?.drawRoundRect(rect.left.toFloat(), rect.top.toFloat(), rect.right.toFloat(), rect.bottom.toFloat(), RECT_BORDER_RADIUS, RECT_BORDER_RADIUS, paint); + } + + fun drawCircle(circle: Circle, canvas: Canvas?, color: Int = Color.GREEN) {; + + paint.setStyle(Paint.Style.FILL) + paint.setColor(color) + + canvas?.drawCircle( + circle.center.x.toFloat(), + circle.center.y.toFloat(), circle.radius.toFloat(), paint) + } + + // Called when the view should render its content. + override fun onDraw(canvas: Canvas?) { + super.onDraw(canvas) + + faces.forEach { + drawRect(getScaleRect(it), canvas, Color.GREEN); + } + + eyes.forEach { + drawCircle(scaleCircle(it), canvas, Color.RED); + } + + landmarks.forEach { + drawCircle(scaleCircle(it), canvas, Color.BLUE); + } + } + + private fun scaleCircle(it: Circle): Circle { + val x = translateX(it.center.x.toInt()) + val y = translateY(it.center.y.toInt()) + val radius = scaleY(it.radius) + + return Circle(Point(x.toInt(),y.toInt()), radius.toInt()) + } + + private fun getScaleRect(rect: Rect): Rect { + val xRect = rect.left; + val yRect = rect.top; + val x: Float = translateX(xRect + rect.width() / 2) + val y: Float = translateY(yRect + rect.height() / 2) + + // Draws a bounding box around the face. + val xOffset: Float = scaleX((rect.width() / 2.0f).toInt()) + val left = x - xOffset + val right = x + xOffset + val width = right - left + val height = width + val yOffset = height / 2 + val top = y - yOffset + val bottom = y + yOffset + return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()) + } + + private fun translateX(x: Int): Float { + return scaleX(x) + } + + private fun scaleX(x: Int): Float { + return (x*widthScaleFactor).toFloat(); + } + + private fun translateY(y: Int): Float { + return scaleY(y) + } + + private fun scaleY(y: Int): Float { + return (y*heightScaleFactor).toFloat(); + } + + fun redraw() { + this.invalidate() + } + + fun setAspectRatio(width: Int, height: Int) { + if (width < 0 || height < 0) { + throw IllegalArgumentException("Size cannot be negative.") + } + + mRatioWidth = width.toDouble() + mRatioHeight = height.toDouble() + + if (this.measuredWidth < this.measuredHeight * mRatioWidth / mRatioHeight) { + currentWidth = this.measuredWidth.toDouble() + currentRatio = mRatioHeight / mRatioWidth + currentHeight = this.measuredWidth * currentRatio + } else { + currentHeight = this.measuredHeight.toDouble() + currentRatio = mRatioWidth / mRatioHeight + currentWidth = this.measuredHeight * currentRatio + } + + + requestLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + if (0.0 == this.currentWidth || 0.0 == this.currentHeight) { + currentWidth = width.toDouble() + currentHeight = height.toDouble() + } + + widthScaleFactor = currentWidth / mRatioWidth; + heightScaleFactor = currentHeight / mRatioHeight; + + setMeasuredDimension(currentWidth.toInt(), currentHeight.toInt()) + this.invalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/capturer/view/CapturerFragment.kt b/app/src/main/java/com/suaro/pidroidapp/capturer/view/CapturerFragment.kt new file mode 100644 index 0000000..b4c98c6 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/capturer/view/CapturerFragment.kt @@ -0,0 +1,127 @@ +package com.suaro.pidroidapp.capturer.view + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.suaro.pidroid.core.FaceDetectionResult +import com.suaro.pidroidapp.ImageActivity +import com.suaro.pidroidapp.MainActivity +import com.suaro.pidroidapp.R +import com.suaro.pidroidapp.capturer.Capturer +import com.suaro.pidroidapp.capturer.ImageProcessor +import com.suaro.pidroidapp.capturer.entity.Circle +import com.suaro.pidroidapp.capturer.presenter.ImageProcessorPresenter +import com.suaro.pidroidapp.core.CameraPreviewListener + + +class CapturerFragment : Fragment(), ImageProcessor.View, CameraPreviewListener { + private lateinit var capturerView: Capturer.View + private lateinit var presenter: ImageProcessorPresenter + private var canvasView: CanvasView? = null + private var first: Boolean = true + private var processing: Boolean = false + private var mLock: Any = Object() + + override fun onAttach(context: Context) { + super.onAttach(context) + capturerView = if (context is Capturer.View) { + context + } else { + throw ClassCastException( + context.toString() + .toString() + " must implement ObInstructions.View" + ) + } + //TODO: Fix this... + (activity as MainActivity).addCameraPreviewListener(this) + } + + override fun onCreate(savedInstance: Bundle?) { + super.onCreate(savedInstance) + } + + override fun onCreateView( + layoutInflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val v: View = layoutInflater.inflate(R.layout.capturer_fragment, container, false) + ?: return null + setRetainInstance(true) + presenter = ImageProcessorPresenter(activity!!, this) + canvasView = v.findViewById(R.id.canvas_view) + return v + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + + } + + override fun onResume() { + super.onResume() + } + + + companion object { + private val TAG = CapturerFragment::class.java.simpleName + fun newInstance(): CapturerFragment { + val f = CapturerFragment() + //val args = Bundle() + //args.putParcelable("instructions_item", item) + //f.setArguments(args) + return f + } + } + + override fun onFaceDetectorFinish(info: FaceDetectionResult) { + this.activity?.runOnUiThread { + synchronized(mLock) { + processing = false; + canvasView?.faces?.clear() + canvasView?.eyes?.clear() + canvasView?.landmarks?.clear() + + if (info.detected) { + //TODO: Check draw aspect ratio. + for (face in info.faces) { + var rect: Rect = Rect( + face.topLeft.x, + face.topLeft.y, + face.topLeft.x + face.width, + face.topLeft.y + face.height + ) + canvasView?.faces?.add(rect) + for (eye in face.eyes) { + canvasView?.eyes?.add(Circle(eye.center, eye.radius)) + } + + for (landmark in face.landmarks) { + canvasView?.landmarks?.add(Circle(landmark.center, landmark.radius)) + } + } + + } + + canvasView?.redraw() + } + } + } + + override fun onPreviewFrame(bitmap: Bitmap) { + if (first) { + canvasView?.setAspectRatio(bitmap.width, bitmap.height) + first = false + } + + //if (processing) + // return + + processing = true + this.presenter.detectFace(bitmap) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/core/CameraPreviewListener.kt b/app/src/main/java/com/suaro/pidroidapp/core/CameraPreviewListener.kt new file mode 100644 index 0000000..3c397fc --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/core/CameraPreviewListener.kt @@ -0,0 +1,7 @@ +package com.suaro.pidroidapp.core + +import android.graphics.Bitmap + +interface CameraPreviewListener { + fun onPreviewFrame(bitmap: Bitmap); +} \ No newline at end of file diff --git a/app/src/main/java/com/suaro/pidroidapp/core/Utils.kt b/app/src/main/java/com/suaro/pidroidapp/core/Utils.kt new file mode 100644 index 0000000..d7dde72 --- /dev/null +++ b/app/src/main/java/com/suaro/pidroidapp/core/Utils.kt @@ -0,0 +1,101 @@ +package com.suaro.pidroidapp.core + +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.graphics.YuvImage +import android.media.Image +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + + +class Utils { + + companion object { + + fun getBytes(image: Bitmap): IntArray? { + val pixels = IntArray(image.width * image.height * 4) + image.getPixels(pixels, 0, image.width, 0, 0, image.width, image.height) + return pixels + } + + fun bitmapToBgrBytes(image: Bitmap): ByteArray? { + + // calculate how many bytes our image consists of + val bytes = image.byteCount + val buffer = ByteBuffer.allocate(bytes) // Create a new buffer + image.copyPixelsToBuffer(buffer) // Move the byte data to the buffer + val temp = buffer.array() // Get the underlying array containing the data. + val pixels = ByteArray(temp.size / 4 * 3) // Allocate for 3 byte BGR + + // Copy pixels into place + for (i in 0 until temp.size / 4) { + pixels[i * 3] = temp[i * 4 + 3] // B + pixels[i * 3 + 1] = temp[i * 4 + 2] // G + pixels[i * 3 + 2] = temp[i * 4 + 1] // R + + // Alpha is discarded + } + return pixels + } + + fun bitmapToGrayBytes(image: Bitmap): ByteArray? { + // calculate how many bytes our image consists of + val bytes = image.byteCount + val buffer = ByteBuffer.allocate(bytes) // Create a new buffer + image.copyPixelsToBuffer(buffer) // Move the byte data to the buffer + val temp = buffer.array() // Get the underlying array containing the data. + val pixels = ByteArray(temp.size / 4) //RGBA (4 channel) size is 4x greater than Gray (One channel) size + + var b: Double = 0.0 + var g: Double = 0.0 + var r: Double = 0.0 + + var gray: Double = 0.0 + // Copy pixels into place + for (i in 0 until temp.size / 4) { + b = temp[i * 4 + 2].toUByte().toDouble() + g = temp[i * 4 + 1].toUByte().toDouble() + r = temp[i * 4 + 0].toUByte().toDouble() + + gray = 0.2 * r + 0.7*g + 0.1*b + + pixels[i] = gray.toInt().toByte() + } + return pixels + } + + fun bitmapToByteArray(bmp: Bitmap): ByteArray { + val stream = ByteArrayOutputStream() + bmp.compress(Bitmap.CompressFormat.PNG, 100, stream) + val byteArray: ByteArray = stream.toByteArray() + bmp.recycle() + return byteArray + } + + fun YUV_420_888toNV21(image: Image): ByteArray? { + val nv21: ByteArray + val yBuffer: ByteBuffer = image.getPlanes().get(0).getBuffer() + val uBuffer: ByteBuffer = image.getPlanes().get(1).getBuffer() + val vBuffer: ByteBuffer = image.getPlanes().get(2).getBuffer() + val ySize: Int = yBuffer.remaining() + val uSize: Int = uBuffer.remaining() + val vSize: Int = vBuffer.remaining() + nv21 = ByteArray(ySize + uSize + vSize) + + //U and V are swapped + yBuffer.get(nv21, 0, ySize) + vBuffer.get(nv21, ySize, vSize) + uBuffer.get(nv21, ySize + vSize, uSize) + return nv21 + } + + + fun NV21toJPEG(nv21: ByteArray, width: Int, height: Int): ByteArray? { + val out = ByteArrayOutputStream() + val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null) + yuv.compressToJpeg(Rect(0, 0, width, height), 100, out) + return out.toByteArray() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cuadrado.jpg b/app/src/main/res/drawable/cuadrado.jpg new file mode 100644 index 0000000..3eb1852 Binary files /dev/null and b/app/src/main/res/drawable/cuadrado.jpg differ diff --git a/app/src/main/res/drawable/frame.jpg b/app/src/main/res/drawable/frame.jpg new file mode 100755 index 0000000..488e70c Binary files /dev/null and b/app/src/main/res/drawable/frame.jpg differ diff --git a/app/src/main/res/drawable/friends.jpg b/app/src/main/res/drawable/friends.jpg new file mode 100644 index 0000000..e0da90b Binary files /dev/null and b/app/src/main/res/drawable/friends.jpg differ diff --git a/app/src/main/res/drawable/friends2.jpg b/app/src/main/res/drawable/friends2.jpg new file mode 100644 index 0000000..37eabc4 Binary files /dev/null and b/app/src/main/res/drawable/friends2.jpg differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/multi.jpg b/app/src/main/res/drawable/multi.jpg new file mode 100644 index 0000000..aa95ac7 Binary files /dev/null and b/app/src/main/res/drawable/multi.jpg differ diff --git a/app/src/main/res/drawable/nayla.jpg b/app/src/main/res/drawable/nayla.jpg new file mode 100644 index 0000000..a3dfc93 Binary files /dev/null and b/app/src/main/res/drawable/nayla.jpg differ diff --git a/app/src/main/res/drawable/portrait.jpg b/app/src/main/res/drawable/portrait.jpg new file mode 100644 index 0000000..4148f08 Binary files /dev/null and b/app/src/main/res/drawable/portrait.jpg differ diff --git a/app/src/main/res/drawable/ross.jpg b/app/src/main/res/drawable/ross.jpg new file mode 100644 index 0000000..fc5a5b1 Binary files /dev/null and b/app/src/main/res/drawable/ross.jpg differ diff --git a/app/src/main/res/drawable/rossout.jpg b/app/src/main/res/drawable/rossout.jpg new file mode 100755 index 0000000..c18f807 Binary files /dev/null and b/app/src/main/res/drawable/rossout.jpg differ diff --git a/app/src/main/res/drawable/sample.jpg b/app/src/main/res/drawable/sample.jpg new file mode 100644 index 0000000..d9f4c01 Binary files /dev/null and b/app/src/main/res/drawable/sample.jpg differ diff --git a/app/src/main/res/layout/activity_image.xml b/app/src/main/res/layout/activity_image.xml new file mode 100644 index 0000000..735e172 --- /dev/null +++ b/app/src/main/res/layout/activity_image.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..bd1c08b --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/camera_fragment.xml b/app/src/main/res/layout/camera_fragment.xml new file mode 100644 index 0000000..6895115 --- /dev/null +++ b/app/src/main/res/layout/camera_fragment.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/capturer_fragment.xml b/app/src/main/res/layout/capturer_fragment.xml new file mode 100644 index 0000000..efce3b4 --- /dev/null +++ b/app/src/main/res/layout/capturer_fragment.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..6e2ce11 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..20b5d16 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + PiDroid + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..95d5327 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/com/suaro/pidroid/ExampleUnitTest.kt b/app/src/test/java/com/suaro/pidroid/ExampleUnitTest.kt new file mode 100644 index 0000000..1f0c277 --- /dev/null +++ b/app/src/test/java/com/suaro/pidroid/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.suaro.pidroidapp + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e524978 --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = "1.4.10" + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.1.0" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..98bed16 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..960a14f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jan 21 20:28:18 CET 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pidroid/.gitignore b/pidroid/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/pidroid/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/pidroid/CMakeLists.txt b/pidroid/CMakeLists.txt new file mode 100644 index 0000000..9b8f5a2 --- /dev/null +++ b/pidroid/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.4.1) + +file(GLOB HEADER_FILES src/main/cpp/*.h src/main/cpp/*.hpp src/main/cpp/pidroid/*.hpp src/main/cpp/pidroid/commons.hpp) +file(GLOB SOURCE_FILES src/main/cpp/*.cpp src/main/cpp/pidroid/*.cpp src/main/cpp/pidroid/commons.cpp) + +add_library(pidroid SHARED ${SOURCE_FILES} ${HEADER_FILES}) +set(CMAKE_VERBOSE_MAKEFILE on) + +# include_directories(/home/adrian/Escritorio/AndroidStudioProjects/PiDroid/app/src/main/cpp/include) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log ) + +find_library( # Sets the name of the path variable. + android-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + android ) + +target_link_libraries( # Specifies the target library. + pidroid + ${log-lib} + ${android-lib}) + +target_compile_options(pidroid PRIVATE + "$<$:-O3>" + "$<$:-O3>" + ) \ No newline at end of file diff --git a/pidroid/build.gradle b/pidroid/build.gradle new file mode 100644 index 0000000..0d3a1d2 --- /dev/null +++ b/pidroid/build.gradle @@ -0,0 +1,67 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + externalNativeBuild { + + cmake { + cppFlags "-std=c++11 -frtti -fexceptions" + abiFilters 'x86', 'x86_64','armeabi-v7a', 'arm64-v8a' + } + + ndk { + abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" + } + } + } + + sourceSets { + main { + jniLibs.srcDirs = ['src/main/jniLibs/'] + } + } + buildTypes { + release { + minifyEnabled true + consumerProguardFiles 'release-proguard.cfg' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-build.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + externalNativeBuild { + cmake { + path file('CMakeLists.txt') + } + } + +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.3.0' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' +} \ No newline at end of file diff --git a/pidroid/consumer-rules.pro b/pidroid/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/pidroid/proguard-build.pro b/pidroid/proguard-build.pro new file mode 100644 index 0000000..72ccb6e --- /dev/null +++ b/pidroid/proguard-build.pro @@ -0,0 +1,30 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep class com.suaro.pidroid.Pidroid {*;} +-keep class com.suaro.pidroid.core.NativeMethods {*;} +-keep class com.suaro.pidroid.core.PidroidConfig {*;} +-keep class com.suaro.pidroid.core.FaceDetectionResult {*;} +-keep class com.suaro.pidroid.core.Face {*;} +-keep class com.suaro.pidroid.core.Eye {*;} +-keep class com.suaro.pidroid.core.Landmark {*;} +-keep class com.suaro.pidroid.core.Point {*;} \ No newline at end of file diff --git a/pidroid/release-proguard.cfg b/pidroid/release-proguard.cfg new file mode 100755 index 0000000..c5a235b --- /dev/null +++ b/pidroid/release-proguard.cfg @@ -0,0 +1,24 @@ +# ---- REQUIRED card.io CONFIG ---------------------------------------- +# card.io is a native lib, so anything crossing JNI must not be changed + +# Don't obfuscate DetectionInfo or public fields, since +# it is used by native methods +-keep class com.suaro.pidroid.Pidroid {*;} +-keep class com.suaro.pidroid.core.NativeMethods {*;} +-keep class com.suaro.pidroid.core.PidroidConfig {*;} +-keep class com.suaro.pidroid.core.FaceDetectionResult {*;} +-keep class com.suaro.pidroid.core.Face {*;} +-keep class com.suaro.pidroid.core.Eye {*;} +-keep class com.suaro.pidroid.core.Landmark {*;} +-keep class com.suaro.pidroid.core.Point {*;} + + +# Don't mess with classes with native methods + +-keepclasseswithmembers class * { + native ; +} + +-keepclasseswithmembernames class * { + native ; +} diff --git a/pidroid/src/androidTest/java/com/suaro/pidroid/ExampleInstrumentedTest.kt b/pidroid/src/androidTest/java/com/suaro/pidroid/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..cd1a9e7 --- /dev/null +++ b/pidroid/src/androidTest/java/com/suaro/pidroid/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.suaro.pidroid + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.suaro.pidroid.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/pidroid/src/main/AndroidManifest.xml b/pidroid/src/main/AndroidManifest.xml new file mode 100644 index 0000000..be06055 --- /dev/null +++ b/pidroid/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/pidroid/src/main/assets/facefinder b/pidroid/src/main/assets/facefinder new file mode 100644 index 0000000..340d0f5 Binary files /dev/null and b/pidroid/src/main/assets/facefinder differ diff --git a/pidroid/src/main/assets/lp312 b/pidroid/src/main/assets/lp312 new file mode 100644 index 0000000..af33fa7 Binary files /dev/null and b/pidroid/src/main/assets/lp312 differ diff --git a/pidroid/src/main/assets/lp38 b/pidroid/src/main/assets/lp38 new file mode 100644 index 0000000..ac843d3 Binary files /dev/null and b/pidroid/src/main/assets/lp38 differ diff --git a/pidroid/src/main/assets/lp42 b/pidroid/src/main/assets/lp42 new file mode 100644 index 0000000..25aef58 Binary files /dev/null and b/pidroid/src/main/assets/lp42 differ diff --git a/pidroid/src/main/assets/lp44 b/pidroid/src/main/assets/lp44 new file mode 100644 index 0000000..281e441 Binary files /dev/null and b/pidroid/src/main/assets/lp44 differ diff --git a/pidroid/src/main/assets/lp46 b/pidroid/src/main/assets/lp46 new file mode 100644 index 0000000..4d0ae7d Binary files /dev/null and b/pidroid/src/main/assets/lp46 differ diff --git a/pidroid/src/main/assets/lp81 b/pidroid/src/main/assets/lp81 new file mode 100644 index 0000000..f6acf1a Binary files /dev/null and b/pidroid/src/main/assets/lp81 differ diff --git a/pidroid/src/main/assets/lp82 b/pidroid/src/main/assets/lp82 new file mode 100644 index 0000000..5210cf7 Binary files /dev/null and b/pidroid/src/main/assets/lp82 differ diff --git a/pidroid/src/main/assets/lp84 b/pidroid/src/main/assets/lp84 new file mode 100644 index 0000000..b22eb3b Binary files /dev/null and b/pidroid/src/main/assets/lp84 differ diff --git a/pidroid/src/main/assets/lp93 b/pidroid/src/main/assets/lp93 new file mode 100644 index 0000000..b131380 Binary files /dev/null and b/pidroid/src/main/assets/lp93 differ diff --git a/pidroid/src/main/assets/puploc b/pidroid/src/main/assets/puploc new file mode 100644 index 0000000..0447dcf Binary files /dev/null and b/pidroid/src/main/assets/puploc differ diff --git a/pidroid/src/main/cpp/pidroid.cpp b/pidroid/src/main/cpp/pidroid.cpp new file mode 100644 index 0000000..7fa78af --- /dev/null +++ b/pidroid/src/main/cpp/pidroid.cpp @@ -0,0 +1,418 @@ + +/* + * See the file "LICENSE.md" for the full license governing this code. + */ + +#include +#include +#include +#include +#include +#include "pidroid/puploc.hpp" +#include "pidroid/flploc.hpp" +#include "pidroid/pico.hpp" +#include +#include +#include +#include +#include "pidroid.hpp" + +#define DEBUG_TAG "pidroid" + + + +pidroidlib::Pidroid pidroid; + + + +extern "C" +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + + JNIEnv *env; + jint status = vm->GetEnv((void **) &env, JNI_VERSION_1_6); + if (status != JNI_OK) + return -1; + + int result = pidroid.loadClasses(env); + + if(result != 0) { + return result; + } + + return JNI_VERSION_1_6; +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_suaro_pidroid_core_NativeMethods_00024Companion_setup(JNIEnv *env, jobject thiz, jobject pidroidConfig, jobject assetManager) { + + pidroid = pidroidlib::Pidroid(); + pidroid.setup(env, thiz, pidroidConfig, assetManager); + +} + + +extern "C" +JNIEXPORT void JNICALL +Java_com_suaro_pidroid_core_NativeMethods_00024Companion_detectFace(JNIEnv *env, jobject thiz, + jintArray rgbaBytes, jint width, + jint height, + jobject detection_info) { + + pidroid.detectFace(env, thiz, rgbaBytes, width, height, detection_info); + +} + +uint8_t* pidroidlib::Pidroid::RGBABytesToGrayBytes(JNIEnv *env, const uint8_t* rgbaBytes, int width, int height) { + const int size = height * width; + int i = -1; + float b = 0, g = 0, r = 0, gray = 0; + auto * out = new uint8_t[size]; + + for(i = 0; i < size; ++i ) { + b = rgbaBytes[i * 4 + 2]; + g = rgbaBytes[i * 4 + 1]; + r = rgbaBytes[i * 4 + 0]; + gray = 0.2f* r + 0.7f*g + 0.1f*b; + out[i] = int(gray); + } + return out; +} + +std::vector pidroidlib::Pidroid::arraylistToVectorJObject(JNIEnv *env, jobject arrayList) { + jint len = env->CallIntMethod(arrayList, jArrayList.size); + std::vector result; + result.reserve(len); + for (jint i=0; i(env->CallObjectMethod(arrayList, jArrayList.get, i)); + result.emplace_back(element); + env->DeleteLocalRef(element); + } + return result; +} + +jobject pidroidlib::Pidroid::vectorJObjectToArrayList(JNIEnv *env, std::vector vector) { + jobject result = env->NewObject(jArrayList.classRef, jArrayList.init, vector.size()); + for (jobject element: vector) { + env->CallBooleanMethod(result, jArrayList.add, element); + env->DeleteLocalRef(element); + } + return result; +} + +std::vector pidroidlib::Pidroid::readFileFromAssets(JNIEnv *pEnv, AAssetManager *mgr, const char *folder, const char *filename) { + AAssetDir* assetDir = AAssetManager_openDir(mgr, folder); + const char* currentFilename; + std::vector buffer; + while ((currentFilename = AAssetDir_getNextFileName(assetDir)) != NULL) + { + //search for desired file + if(!strcmp(currentFilename, filename)) + { + AAsset *asset = AAssetManager_open(mgr, currentFilename, 2); + + //holds size of searched file + off64_t length = AAsset_getLength64(asset); + //keeps track of remaining bytes to read + off64_t remaining = AAsset_getRemainingLength64(asset); + size_t Mb = 1000 *1024; // 1Mb is maximum chunk size for compressed assets + size_t currChunk; + buffer.reserve(length); + + //while we have still some data to read + while (remaining != 0) + { + //set proper size for our next chunk + if(remaining >= Mb) + { + currChunk = Mb; + } + else + { + currChunk = remaining; + } + char chunk[currChunk]; + + //read data chunk + if(AAsset_read(asset, chunk, currChunk) > 0) // returns less than 0 on error + { + //and append it to our vector + buffer.insert(buffer.end(),chunk, chunk + currChunk); + remaining = AAsset_getRemainingLength64(asset); + } + } + AAsset_close(asset); + } + + } + + return buffer; +} + + +int pidroidlib::Pidroid::loadClasses(JNIEnv *env) { + + jclass pointClass = env->FindClass("com/suaro/pidroid/core/Point"); + if (pointClass == NULL) { + return -1; + } + jPoint.classRef = (jclass) env->NewGlobalRef(pointClass); + jPoint.x = env->GetFieldID(pointClass, "x", "I"); + jPoint.y = env->GetFieldID(pointClass, "y", "I"); + + jclass dEyeClass = env->FindClass("com/suaro/pidroid/core/Eye"); + if (dEyeClass == NULL) { + return -1; + } + + jEye.classRef = (jclass) env->NewGlobalRef(dEyeClass); + jEye.init = env->GetMethodID(dEyeClass, "", "()V"); + jEye.center = env->GetFieldID(dEyeClass, "center", "Lcom/suaro/pidroid/core/Point;"); + jEye.radius = env->GetFieldID(dEyeClass, "radius", "I"); + + jclass dLandmarkClass = env->FindClass("com/suaro/pidroid/core/Landmark"); + if (dLandmarkClass == NULL) { + return -1; + } + + jLandmark.classRef = (jclass) env->NewGlobalRef(dLandmarkClass); + jLandmark.init = env->GetMethodID(dLandmarkClass, "", "()V"); + jLandmark.center = env->GetFieldID(dLandmarkClass, "center", "Lcom/suaro/pidroid/core/Point;"); + jLandmark.radius = env->GetFieldID(dLandmarkClass, "radius", "I"); + + jclass dFaceClass = env->FindClass("com/suaro/pidroid/core/Face"); + if (dFaceClass == NULL) { + return -1; + } + jFace.classRef = (jclass) env->NewGlobalRef(dFaceClass); + jFace.init = env->GetMethodID(dFaceClass, "", "()V"); + jFace.topLeft = env->GetFieldID(dFaceClass, "topLeft", + "Lcom/suaro/pidroid/core/Point;"); + jFace.width = env->GetFieldID(dFaceClass, "width", + "I"); + jFace.height = env->GetFieldID(dFaceClass, "height", + "I"); + jFace.eyes = env->GetFieldID(dFaceClass, "eyes", "Ljava/util/ArrayList;"); + + jFace.landmarks = env->GetFieldID(dFaceClass, "landmarks", "Ljava/util/ArrayList;"); + + jclass dArrayList = env->FindClass("java/util/ArrayList"); + if (dArrayList == NULL) { + return -1; + } + jArrayList.classRef = (jclass) env->NewGlobalRef(dArrayList); + jArrayList.init = env->GetMethodID(dArrayList, "", "(I)V"); + jArrayList.size = env->GetMethodID (dArrayList, "size", "()I"); + jArrayList.get = env->GetMethodID(dArrayList, "get", "(I)Ljava/lang/Object;"); + jArrayList.add = env->GetMethodID(dArrayList, "add", "(Ljava/lang/Object;)Z"); + + jclass dInfoClass = env->FindClass("com/suaro/pidroid/core/FaceDetectionResult"); + if (dInfoClass == NULL) { + return -1; + } + jFaceDetectionResult.classRef = (jclass) env->NewGlobalRef(dInfoClass); + jFaceDetectionResult.faces = env->GetFieldID(dInfoClass, "faces", "Ljava/util/ArrayList;"); + jFaceDetectionResult.detected = env->GetFieldID(dInfoClass, "detected", "Z"); + + jclass dCascadeClass = env->FindClass("com/suaro/pidroid/core/PidroidConfig"); + if (dCascadeClass == NULL) { + return -1; + } + jPidroidConfig.classRef = (jclass) env->NewGlobalRef(dCascadeClass); + jPidroidConfig.angle = env->GetFieldID(dCascadeClass, "angle", "F"); + jPidroidConfig.maxsize = env->GetFieldID(dCascadeClass, "maxsize", "I"); + jPidroidConfig.minsize = env->GetFieldID(dCascadeClass, "minsize", "I"); + jPidroidConfig.perturbs = env->GetFieldID(dCascadeClass, "perturbs", "I"); + jPidroidConfig.clustering = env->GetFieldID(dCascadeClass, "clustering", "Z"); + jPidroidConfig.qthreshold = env->GetFieldID(dCascadeClass, "qthreshold", "F"); + jPidroidConfig.scalefactor = env->GetFieldID(dCascadeClass, "scalefactor", "F"); + jPidroidConfig.stridefactor = env->GetFieldID(dCascadeClass, "stridefactor", "F"); + jPidroidConfig.pupilDetectionEnable = env->GetFieldID(dCascadeClass, "pupilDetectionEnable", "Z"); + jPidroidConfig.landmarkDetectionEnable = env->GetFieldID(dCascadeClass, "landmarkDetectionEnable", "Z"); + jPidroidConfig.prominentFaceOnly = env->GetFieldID(dCascadeClass, "prominentFaceOnly", "Z"); + + return 0; +} + +void pidroidlib::Pidroid::detectFace(JNIEnv *env, jobject thiz, jintArray rgbaBytes, jint width, + jint height, jobject detection_info) { + + uint8_t* pixels; + uint8_t* rgbaPixels; + int nrows, ncols, ldim; + + + std::vector faceArray = std::vector(); + std::vector eyeArray = std::vector(); + std::vector landmarkArray = std::vector(); + + nrows = height; + ncols = width; + ldim = width; + + jint* dataPtr = (*env).GetIntArrayElements(rgbaBytes, NULL); + + rgbaPixels = (uint8_t*)dataPtr; + pixels = RGBABytesToGrayBytes(env, rgbaPixels, ncols, nrows); + + pidroidlib::PicoResult res{}; + + auto start_time = std::chrono::high_resolution_clock::now(); + res = picol.RunCascade(nrows, ncols, pixels, ldim); + res = picol.updateMemory(res); + + if(clustering) { + res = pidroidlib::Pico::clusterDetections(res, 0.2); + } + + if(res.ndetections > 0 && prominentFaceOnly) { + res = pidroidlib::Pico::getProminentFace(res); + } + + pidroidlib::CascadeParams params = picol.getCascadeParams(); + + std::vector eyes = std::vector(); + pidroidlib::FlpDetection landmarks = {}; + + for(int i=0; i=params.minThreshold) { + int centerX = det.col; + int centerY = det.row; + int radius = det.scale / 2; + + if (radius > 50 && pupilDetectionEnable) { + pidroidlib::PuplocDetection left = { + .row = centerY - int(0.075*radius), + .col = centerX- int(0.175*radius), + .scale = int(det.scale * 0.35f), + .perturbs = perturbs + }; + left = puploc1.runDetector(left, nrows, ncols, pixels, ldim, params.angle, false); + + pidroidlib::PuplocDetection right = { + .row = centerY - int(0.075*radius), + .col = centerX + int(0.175*radius), + .scale = int(det.scale * 0.35f), + .perturbs = perturbs + }; + right = puploc1.runDetector(right, nrows, ncols, pixels, ldim, params.angle, false); + + + eyes.push_back(left); + right.scale = left.scale; + eyes.push_back(right); + + if(landmarkDetectionEnable) { + landmarks = flploc.getLandmarkPoints(left, right, nrows, ncols, pixels, ldim, perturbs); + } + } + + for(auto &&eye: eyes) { + jobject faceEye = env->NewObject(jEye.classRef, jEye.init); + jobject centerEyes = env->GetObjectField(faceEye, jEye.center); + + env->SetIntField(centerEyes, jPoint.x, eye.col); + env->SetIntField(centerEyes, jPoint.y, eye.row); + + env->SetObjectField(faceEye, jEye.center, centerEyes); + env->SetIntField(faceEye, jEye.radius, int(eye.scale * 0.2f)); + + eyeArray.push_back(faceEye); + } + + for(auto &&landmark: landmarks.detections) { + jobject faceLandmark = env->NewObject(jLandmark.classRef, jLandmark.init); + jobject centerLandmark = env->GetObjectField(faceLandmark, jLandmark.center); + + env->SetIntField(centerLandmark, jPoint.x, landmark.col); + env->SetIntField(centerLandmark, jPoint.y, landmark.row); + + env->SetObjectField(faceLandmark, jLandmark.center, centerLandmark); + env->SetIntField(faceLandmark, jLandmark.radius, int(landmark.scale * 0.15f)); + + landmarkArray.push_back(faceLandmark); + } + + + + int faceWidth = radius * 2; + int faceHeight = faceWidth; // 1.58 is Habitual Face Width-Height Ratio + int x = centerX - (faceWidth / 2); + int y = centerY - (faceHeight / 2); + + jobject face = env->NewObject(jFace.classRef, jFace.init); + jobject topLeft = env->GetObjectField(face, jFace.topLeft); + + env->SetIntField(topLeft, jPoint.x, x); + env->SetIntField(topLeft, jPoint.y, y); + env->SetObjectField(face, jFace.topLeft, topLeft); + + env->SetIntField(face, jFace.width, faceWidth); + env->SetIntField(face, jFace.height, faceHeight); + + env->SetObjectField(face, jFace.eyes, vectorJObjectToArrayList(env, eyeArray)); + env->SetObjectField(face, jFace.landmarks, vectorJObjectToArrayList(env, landmarkArray)); + + faceArray.push_back(face); + } + } + + env->SetObjectField(detection_info, jFaceDetectionResult.faces, vectorJObjectToArrayList(env, faceArray)); + env->SetBooleanField(detection_info, jFaceDetectionResult.detected, faceArray.size() > 0); + + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time-start_time).count(); + std::string logText = "Processing time: "+std::to_string(duration); + + __android_log_write(ANDROID_LOG_VERBOSE, "Detection", logText.c_str()); + + faceArray.clear(); + eyeArray.clear(); + landmarkArray.clear(); +} + +void +pidroidlib::Pidroid::setup(JNIEnv *env, jobject thiz, jobject pidroidConfig, jobject assetManager) { + pidroidlib::CascadeParams cascadeParams = { + .minSize = env->GetIntField(pidroidConfig, jPidroidConfig.minsize), + .maxSize = env->GetIntField(pidroidConfig, jPidroidConfig.maxsize), + .shiftFactor = env->GetFloatField(pidroidConfig, jPidroidConfig.stridefactor), + .scaleFactor = env->GetFloatField(pidroidConfig, jPidroidConfig.scalefactor), + .angle = env->GetFloatField(pidroidConfig, jPidroidConfig.angle), + .minThreshold = env->GetFloatField(pidroidConfig, jPidroidConfig.qthreshold) + }; + + perturbs = env->GetIntField(pidroidConfig, jPidroidConfig.perturbs); + clustering = env->GetBooleanField(pidroidConfig, jPidroidConfig.clustering); + pupilDetectionEnable = env->GetBooleanField(pidroidConfig, jPidroidConfig.pupilDetectionEnable); + landmarkDetectionEnable = env->GetBooleanField(pidroidConfig, jPidroidConfig.landmarkDetectionEnable); + prominentFaceOnly = env->GetBooleanField(pidroidConfig, jPidroidConfig.prominentFaceOnly); + + AAssetManager *mgr = AAssetManager_fromJava(env, assetManager); + + std::vector facefinder = readFileFromAssets(env, mgr, "", "facefinder"); + std::vector puploc = readFileFromAssets(env, mgr, "", "puploc"); + + picol = pidroidlib::Pico(cascadeParams); + picol.unpackPicoCascade(facefinder); + + puploc1 = pidroidlib::Puploc(); + puploc1.unpackCascade(puploc); + + //TODO: Improve flp reads. + std::vector flpCascades = {"lp38", "lp42", "lp44", + "lp46", "lp81", "lp82", + "lp84", "lp93", "lp312"}; + + for(const auto& cascadeName : flpCascades) { + flploc.addCascade(cascadeName, readFileFromAssets(env, mgr, "", cascadeName.c_str())); + } +} diff --git a/pidroid/src/main/cpp/pidroid.hpp b/pidroid/src/main/cpp/pidroid.hpp new file mode 100644 index 0000000..47f98a8 --- /dev/null +++ b/pidroid/src/main/cpp/pidroid.hpp @@ -0,0 +1,91 @@ +// +// Created by adrian on 23/02/21. +// +#include + +namespace pidroidlib { + static struct { + jclass classRef; + jfieldID minsize; + jfieldID maxsize; + jfieldID angle; + jfieldID scalefactor; + jfieldID stridefactor; + jfieldID qthreshold; + jfieldID perturbs; + jfieldID clustering; + jfieldID pupilDetectionEnable; + jfieldID landmarkDetectionEnable; + jfieldID prominentFaceOnly; + } jPidroidConfig; + + + static struct { + jclass classRef; + jmethodID init; + jfieldID topLeft; + jfieldID width; + jfieldID height; + jfieldID eyes; + jfieldID landmarks; + } jFace; + + + static struct { + jclass classRef; + jmethodID init; + jmethodID size; + jmethodID add; + jmethodID get; + } jArrayList; + + static struct { + jclass classRef; + jfieldID faces; + jfieldID detected; + } jFaceDetectionResult; + + static struct { + jclass classRef; + jfieldID x; + jfieldID y; + } jPoint; + + static struct { + jclass classRef; + jfieldID center; + jfieldID radius; + jmethodID init; + } jEye; + + static struct { + jclass classRef; + jfieldID center; + jfieldID radius; + jmethodID init; + } jLandmark; + + class Pidroid { + private: + Puploc puploc1; + Pico picol; + Flploc flploc; + + bool pupilDetectionEnable = false; + bool landmarkDetectionEnable = false; + bool prominentFaceOnly = false; + bool clustering = true; + int perturbs = 50; + + static std::vector arraylistToVectorJObject(JNIEnv *env, jobject arrayList); + static uint8_t* RGBABytesToGrayBytes(JNIEnv *env, const uint8_t* rgbaBytes, int width, int height); + static jobject vectorJObjectToArrayList(JNIEnv *env, std::vector vector); + static std::vector readFileFromAssets(JNIEnv *pEnv, AAssetManager *mgr,const char *folder, const char *filename); + public: + Pidroid() = default; + ~Pidroid() = default; + int loadClasses(JNIEnv *env); + void setup(JNIEnv *env, jobject thiz, jobject pidroidConfig, jobject assetManager); + void detectFace(JNIEnv *env, jobject thiz, jintArray rgbaBytes, jint width, jint height, jobject detection_info); + }; +} diff --git a/pidroid/src/main/cpp/pidroid/commons.cpp b/pidroid/src/main/cpp/pidroid/commons.cpp new file mode 100644 index 0000000..8cd37d7 --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/commons.cpp @@ -0,0 +1,40 @@ +// +// Created by adrian on 21/02/21. +// + +#include "commons.hpp" + +namespace pidroidlib { + + int Commons::buffToInteger(std::vector buffer) { + int a = static_cast(static_cast(buffer[0]) << 24 | + static_cast(buffer[1]) << 16 | + static_cast(buffer[2]) << 8 | + static_cast(buffer[3])); + return a; + } + + + float Commons::bytesToFloatLittleEndian(std::vector buffer) { + float output; + + *((unsigned char *) (&output) + 3) = buffer[0]; + *((unsigned char *) (&output) + 2) = buffer[1]; + *((unsigned char *) (&output) + 1) = buffer[2]; + *((unsigned char *) (&output) + 0) = buffer[3]; + + return output; + } + + + float Commons::bytesToFloatBigEndian(std::vector buffer) { + float output; + + *((unsigned char *) (&output) + 3) = buffer[3]; + *((unsigned char *) (&output) + 2) = buffer[2]; + *((unsigned char *) (&output) + 1) = buffer[1]; + *((unsigned char *) (&output) + 0) = buffer[0]; + + return output; + } +} \ No newline at end of file diff --git a/pidroid/src/main/cpp/pidroid/commons.hpp b/pidroid/src/main/cpp/pidroid/commons.hpp new file mode 100644 index 0000000..af446a8 --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/commons.hpp @@ -0,0 +1,19 @@ +// +// Created by adrian on 21/02/21. +// + +#pragma once +#include + +using namespace std; + +namespace pidroidlib { + class Commons { + public: + static int buffToInteger(std::vector buffer); + + static float bytesToFloatLittleEndian(std::vector buffer); + + static float bytesToFloatBigEndian(std::vector buffer); + }; +} \ No newline at end of file diff --git a/pidroid/src/main/cpp/pidroid/flploc.cpp b/pidroid/src/main/cpp/pidroid/flploc.cpp new file mode 100644 index 0000000..e8ea41f --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/flploc.cpp @@ -0,0 +1,81 @@ +// +// Created by adrian on 24/01/21. +// + +#include "flploc.hpp" + + +namespace pidroidlib { + + void Flploc::addCascade(std::string cascadeName, std::vector packet) { + Puploc puploc = Puploc(); + puploc.unpackCascade(std::move(packet)); + this->cascade.puplocCascade.insert(std::make_pair(cascadeName, puploc)); + } + + FlpDetection + Flploc::getLandmarkPoints(PuplocDetection leftEye, PuplocDetection rightEye, int nrows, + int ncols, const uint8_t *pixels, int dim, int perturb) { + + std::vector landmarks = std::vector(); + Puploc lpc; + + for (const auto &cascadeName : this->eyeCascades) { + lpc = this->cascade.puplocCascade.at(cascadeName); + landmarks.push_back( + pidroidlib::Flploc::getLandmarkPoint(lpc, leftEye, rightEye, nrows, ncols, pixels, dim, + perturb, false)); + landmarks.push_back( + pidroidlib::Flploc::getLandmarkPoint(lpc, leftEye, rightEye, nrows, ncols, pixels, dim, + perturb, true)); + } + + for (const auto &cascadeName : this->mouthCascades) { + lpc = this->cascade.puplocCascade.at(cascadeName); + landmarks.push_back( + pidroidlib::Flploc::getLandmarkPoint(lpc, leftEye, rightEye, nrows, ncols, pixels, dim, + perturb, false)); + } + + lpc = this->cascade.puplocCascade.at("lp84"); + landmarks.push_back( + pidroidlib::Flploc::getLandmarkPoint(lpc, leftEye, rightEye, nrows, ncols, pixels, dim, perturb, + true)); + + return { + .detections = landmarks + }; + } + + PuplocDetection Flploc::getLandmarkPoint(const Puploc& detector, PuplocDetection leftEye, + PuplocDetection rightEye, int nrows, + int ncols, const uint8_t *pixels, int dim, int perturb, + bool flipV) { + + + int dist1 = (leftEye.row - rightEye.row) * (leftEye.row - rightEye.row); + int dist2 = (leftEye.col - rightEye.col) * (leftEye.col - rightEye.col); + float dist = sqrt(float(dist1 + dist2)); + + auto row = float((leftEye.row + rightEye.row) / 2.0 + 0.25 * dist); + auto col = float((leftEye.col + rightEye.col) / 2.0 + 0.15 * dist); + auto scale = 3.0f * dist; + + PuplocDetection flploc = { + .row = int(row), + .col = int(col), + .scale = int(scale), + .perturbs = perturb + }; + + return detector.runDetector(flploc, nrows, ncols, pixels, dim, 0.0, flipV); + } + + Flploc::Flploc() { + this->eyeCascades = {"lp46", "lp44", "lp42", "lp38", "lp312"}; + this->mouthCascades = {"lp93", "lp84", "lp82", "lp81"}; + } + + Flploc::~Flploc() = default; +} + diff --git a/pidroid/src/main/cpp/pidroid/flploc.hpp b/pidroid/src/main/cpp/pidroid/flploc.hpp new file mode 100644 index 0000000..912ee2d --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/flploc.hpp @@ -0,0 +1,33 @@ +// +// Created by adrian on 24/01/21. +// +#pragma once +#include +#include +#include +#include +#include "puploc.hpp" +using namespace std; + +namespace pidroidlib { + struct FlpCascade { + std::map puplocCascade; + }; + + struct FlpDetection { + std::vector detections; + }; + + class Flploc { + private: + FlpCascade cascade; + std::vector eyeCascades; + std::vector mouthCascades; + public: + Flploc(); + ~Flploc(); + void addCascade(std::string cascadeName, std::vector packet); + static PuplocDetection getLandmarkPoint(const Puploc& detector, PuplocDetection leftEye, PuplocDetection rightEye, int nrows, int ncols, const uint8_t* pixels, int dim, int perturb, bool flipV); + FlpDetection getLandmarkPoints(PuplocDetection leftEye, PuplocDetection rightEye, int nrows, int ncols, const uint8_t* pixels, int dim, int perturb); + }; +} \ No newline at end of file diff --git a/pidroid/src/main/cpp/pidroid/pico.cpp b/pidroid/src/main/cpp/pidroid/pico.cpp new file mode 100644 index 0000000..2f46a65 --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/pico.cpp @@ -0,0 +1,333 @@ +// +// Created by adrian on 24/01/21. +// + +#include "pico.hpp" + +namespace pidroidlib { + + + Pico::Pico() { + this->cascadeParams = { + .minSize = 100, + .maxSize = 1024, + .shiftFactor = 0.1, + .scaleFactor = 1.1, + .angle = 0, + .minThreshold = 5 + }; + this->slot = 0; + } + + + Pico::Pico(CascadeParams cascadeParams) { + this->cascadeParams = cascadeParams; + this->slot = 0; + } + + Pico::~Pico() = default; + + void Pico::unpackPicoCascade(std::vector packet) { + int treeNum; + int treeDepth; + std::vector treeCodes; + std::vector treePreds; + std::vector treeThreshold; + //((int*)cascade)[2] + int pos = 8; + std::vector buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], + (packet)[pos + 0]}; + treeDepth = Commons::buffToInteger(buff); + pos += 4; + + buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], (packet)[pos + 0]}; + + treeNum = Commons::buffToInteger(buff); + pos += 4; + + for (int t = 0; t < treeNum; t++) { + std::vector empty = {0, 0, 0, 0}; + treeCodes.insert(treeCodes.end(), empty.data(), empty.data() + 4); + + std::vector v2 = std::vector(packet.begin() + pos, + packet.begin() + pos + int(4 * + pow(2, + float(treeDepth)) - + 4)); + treeCodes.insert(treeCodes.end(), v2.data(), + v2.data() + int(4 * pow(2, float(treeDepth)) - 4)); + pos += int(4 * pow(2, float(treeDepth)) - 4); + + for (int i = 0; i < int(pow(2, float(treeDepth))); i++) { + buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], (packet)[pos + 0]}; + treePreds.push_back(Commons::bytesToFloatLittleEndian(buff)); + pos += 4; + } + + buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], (packet)[pos + 0]}; + treeThreshold.push_back(Commons::bytesToFloatLittleEndian(buff)); + pos += 4; + } + + + this->cascade.treeCodes = new int[treeCodes.size()]; + this->cascade.treePreds = new float[treePreds.size()]; + this->cascade.treeThreshold = new float[treeThreshold.size()]; + + this->cascade.treeNum = treeNum; + this->cascade.treeDepth = treeDepth; + + std::copy(treeCodes.begin(), treeCodes.end(), this->cascade.treeCodes); + std::copy(treePreds.begin(), treePreds.end(), this->cascade.treePreds); + std::copy(treeThreshold.begin(), treeThreshold.end(), this->cascade.treeThreshold); + } + + + + float calculateIoU(PicoDetection det1, PicoDetection det2) { + auto r1 = float(det1.row), c1 = float(det1.col), s1 = float(det1.scale); + auto r2 = float(det2.row), c2 = float(det2.col), s2 = float(det2.scale); + + float overRow = max(0.0f, min(r1 + s1 / 2, r2 + s2 / 2) - max(r1 - s1 / 2, r2 - s2 / 2)); + float overCol = max(0.0f, min(c1 + s1 / 2, c2 + s2 / 2) - max(c1 - s1 / 2, c2 - s2 / 2)); + + float iOu = overRow * overCol / (s1 * s1 + s2 * s2 - overRow * overCol); + + return iOu; + } + + bool detcompare(PicoDetection det1, PicoDetection det2) { + return det1.threshold < det2.threshold; + } + + PicoResult Pico::clusterDetections(PicoResult pl, float iouThreshold) { + int r, c, s, n; + float q; + + + bool assignments[pl.ndetections]; + PicoResult out = {}; + + std::sort(pl.detections, pl.detections + pl.ndetections, detcompare); + + for (int i = 0; i < pl.ndetections; i++) { + if (!assignments[i]) { + r = 0, c = 0, s = 0, n = 0; + q = 0.0f; + for (int j = 0; j < pl.ndetections; j++) { + if (calculateIoU(pl.detections[i], pl.detections[j]) > iouThreshold) { + assignments[j] = true; + r += pl.detections[j].row; + c += pl.detections[j].col; + s += pl.detections[j].scale; + q += pl.detections[j].threshold; + n++; + } + } + if (n > 1) { + out.detections[out.ndetections] = { + .row = r / n, + .col = c / n, + .scale = s / n, + .threshold = q / float(n) + }; + out.ndetections++; + } + } + } + + return out; + } + + float Pico::classifyRegion(int r, int c, int s, int nrows, int ncols, const uint8_t *pixels, + int dim) const { + + int root = 0, i, j, idx, treeDepth = int(pow(2, float(cascade.treeDepth))); + float out = 0; + + r = r * 256; + c = c * 256; + + if ((r + 128 * s) / 256 >= nrows || (r - 128 * s) / 256 < 0 || + (c + 128 * s) / 256 >= ncols || (c - 128 * s) / 256 < 0) + return -1; + + // const reference improve speed + int tdepth = cascade.treeDepth; + int ntrees = cascade.treeNum; + const auto treeCodes = cascade.treeCodes; + const auto treePreds = cascade.treePreds; + const auto treeThreshold = cascade.treeThreshold; + + int offset = 4 * treeDepth; + + for (i = 0; i < ntrees; ++i) { + idx = 1; + + for (j = 0; j < tdepth; ++j) + idx = 2 * idx + (pixels[((r + (treeCodes[root + 4 * idx + 0]) * s) >> 8) * dim +((c + (treeCodes[root + 4 * idx + 1]) * s) >> 8)] <= pixels[((r + (treeCodes[root + 4 * idx + 2]) * s) >> 8) * dim + ((c + (treeCodes[root + 4 * idx + 3]) * s) >> 8)]); + + out += treePreds[treeDepth * i + idx - treeDepth]; + + if (out <= treeThreshold[i]) { + return -1; + } + root += offset; + } + + return out - treeThreshold[ntrees - 1]; + } + + float + Pico::classifyRotatedRegion(int r, int c, int s, float angle, int nrows, + int ncols, const uint8_t *pixels, int dim) const { + + float out = 0; + int c1 = 0, c2 = 0, root = 0, idx = 0, r1 = 0, r2 = 0, lutIdx = 0, row1 = 0, col1 = 0, row2 = 0, col2 = 0; + float dr = 0.0, dc = 0.0; + + int qCosTable[] = {256, 251, 236, 212, 181, 142, 97, 49, 0, -49, -97, -142, -181, -212, + -236, -251, -256, -251, -236, -212, -181, -142, -97, -49, 0, 49, 97, + 142, 181, 212, 236, 251, 256}; + int qSinTable[] = {0, 49, 97, 142, 181, 212, 236, 251, 256, 251, 236, 212, 181, 142, 97, + 49, 0, -49, -97, -142, -181, -212, -236, -251, -256, -251, -236, -212, + -181, -142, -97, -49, 0}; + + int qsin = s * qSinTable[int(32.0 * angle)]; //s*(256.0*math.Sin(2*math.Pi*a)) + int qcos = s * qCosTable[int(32.0 * angle)]; //s*(256.0*math.Cos(2*math.Pi*a)) + + int tdepth = cascade.treeDepth; + int ntrees = cascade.treeNum; + const auto treeCodes = cascade.treeCodes; + const auto treePreds = cascade.treePreds; + const auto treeThreshold = cascade.treeThreshold; + + int treeDepth = pow(2, tdepth); + + for (int i = 0; i < ntrees; i++) { + idx = 1; + for (int j = 0; j < tdepth; j++) { + r1 = abs(min(nrows - 1, max(0, int(65536 * r + qcos * treeCodes[root +4 *idx +0] -qsin * treeCodes[root + 4 * idx + 1]))>> 16)); + c1 = abs(min(nrows - 1, max(0, int(65536 * c + qsin * treeCodes[root +4 *idx +0] +qcos *treeCodes[root + 4 * idx + 1]))>> 16)); + + r2 = abs(min(nrows - 1, max(0, int(65536 * r + qcos * treeCodes[root +4 *idx +2] - qsin *treeCodes[root + 4 * idx + 3]))>> 16)); + c2 = abs(min(nrows - 1, max(0, int(65536 * c + qsin * treeCodes[root +4 *idx +2] +qcos * treeCodes[root + 4 * idx + 3]))>> 16)); + + + idx = 2 * idx + (pixels[r1 * dim + c1] > pixels[r2 * dim + c2]); + } + out += treePreds[treeDepth * i + idx - treeDepth]; + + if (out <= treeThreshold[i]) { + return -1; + } + root += 4 * treeDepth; + } + return out - treeThreshold[ntrees - 1]; + } + + PicoResult + Pico::RunCascade(int rows, int cols, const uint8_t *pixels, int dim) const { + + PicoResult result = {}; + + int step, row, col, offset; + float q; + + auto scale = cascadeParams.minSize; + const auto maxsizef = float(cascadeParams.maxSize); + const float shiftFactor = cascadeParams.shiftFactor; + const float angle = cascadeParams.angle > 1 ? 1 : cascadeParams.angle; + const float scaleFactor = cascadeParams.scaleFactor; + + while (scale <= maxsizef) { + step = max(int(shiftFactor * scale), 1); + offset = scale / 2 + 1; + + for (row = offset; row <= rows - offset; row += step) { + for (col = offset; col <= cols - offset; col += step) { + if (angle > 0) { + q = classifyRotatedRegion(row, col, scale, angle, rows, + cols, pixels, dim); + + } else { + q = classifyRegion(row, col, scale, rows, cols, pixels, dim); + } + + if (q > 0) { + result.detections[result.ndetections] = {.row = row, + .col = col, + .scale = scale, + .threshold = q}; + result.ndetections += 1; + } + } + } + scale *= scaleFactor; + } + + return result; + } + + const CascadeParams &Pico::getCascadeParams() const { + return cascadeParams; + } + + void Pico::setCascadeParams(const CascadeParams &newCascadeParams) { + Pico::cascadeParams = newCascadeParams; + } + + PicoResult Pico::updateMemory(PicoResult pl) { + int i, j; + PicoResult out = {}; + + // + counts[this->slot] = pl.ndetections; + + for (i = 0; i < counts[slot]; ++i) { + memory[slot * 4 * maxslotsize + 4 * i + 0] = float(pl.detections[i].row); + memory[slot * 4 * maxslotsize + 4 * i + 1] = float(pl.detections[i].col); + memory[slot * 4 * maxslotsize + 4 * i + 2] = float(pl.detections[i].scale); + memory[slot * 4 * maxslotsize + 4 * i + 3] = pl.detections[i].threshold; + } + + slot = (slot + 1) % nmemslots; + + + for (i = 0; i < nmemslots; ++i) + for (j = 0; j < counts[i]; ++j) { + if (out.ndetections >= maxslotsize) + return out; + + out.detections[out.ndetections] = { + .row = int(memory[i * 4 * maxslotsize + 4 * j + 0]), + .col = int(memory[i * 4 * maxslotsize + 4 * j + 1]), + .scale = int(memory[i * 4 * maxslotsize + 4 * j + 2]), + .threshold = memory[i * 4 * maxslotsize + 4 * j + 3] + }; + ++out.ndetections; + } + + return out; + } + + PicoResult Pico::getProminentFace(PicoResult result) { + int index = 0, i = 0; + int maxRadius = 0; + PicoResult out = {}; + + for(i = 0; i < result.ndetections; i++) { + if(result.detections[i].scale > maxRadius) { + index = i; + maxRadius = result.detections[i].scale; + } + } + + out.detections[0] = result.detections[index]; + out.ndetections = 1; + + return out; + } + +} + diff --git a/pidroid/src/main/cpp/pidroid/pico.hpp b/pidroid/src/main/cpp/pidroid/pico.hpp new file mode 100644 index 0000000..60008af --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/pico.hpp @@ -0,0 +1,71 @@ +// +// Created by adrian on 24/01/21. +// +#pragma once +#include +#include "commons.hpp" + +using namespace std; + +#define nmemslots 5 +#define maxslotsize 1024 + +namespace pidroidlib { + + struct PicoDetection { + int row; + int col; + int scale; + float threshold; + }; + + struct PicoResult { + int ndetections; + PicoDetection detections[2048]; + }; + + struct PicoCascade { + int treeNum; + int treeDepth; + int* treeCodes; + float* treePreds; + float* treeThreshold; + } ; + + struct CascadeParams { + int minSize; + int maxSize; + float shiftFactor; + float scaleFactor; + float angle; + float minThreshold; + } ; + + class Pico { + private: + CascadeParams cascadeParams{}; + PicoCascade cascade{}; + + int slot; + float memory[4*nmemslots*maxslotsize]{}; + int counts[nmemslots]{}; + public: + Pico(); + Pico(CascadeParams cascadeParams); + ~Pico(); + void unpackPicoCascade(std::vector packet); + float classifyRegion(int r, int c, int s, int nrows, int ncols, const uint8_t* pixels, int dim) const; + float classifyRotatedRegion(int r, int c, int s, float angle, int nrows, int ncols, const uint8_t* pixels, int dim) const; + PicoResult RunCascade(int rows, int cols, const uint8_t* pixels, int dim) const; + static PicoResult clusterDetections(PicoResult pl, float iouThreshold); + + const CascadeParams &getCascadeParams() const; + + void setCascadeParams(const CascadeParams &cascadeParams); + + PicoResult updateMemory(PicoResult pl); + + static PicoResult getProminentFace(PicoResult result); + }; +} + diff --git a/pidroid/src/main/cpp/pidroid/puploc.cpp b/pidroid/src/main/cpp/pidroid/puploc.cpp new file mode 100644 index 0000000..344c3b3 --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/puploc.cpp @@ -0,0 +1,244 @@ +// +// Created by adrian on 24/01/21. +// + +#include "puploc.hpp" + + +namespace pidroidlib { + + void Puploc::unpackCascade(std::vector packet) { + int stages; + float scales; + int trees; + int treeDepth; + std::vector treeCodes; + std::vector treePreds; + //((int*)cascade)[2] + int pos = 0; + std::vector buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], + (packet)[pos + 0]}; + stages = Commons::buffToInteger(buff); + + pos += 4; + buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], (packet)[pos + 0]}; + + scales = Commons::bytesToFloatLittleEndian(buff); + + pos += 4; + buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], (packet)[pos + 0]}; + + trees = Commons::buffToInteger(buff); + + pos += 4; + buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], (packet)[pos + 0]}; + + treeDepth = Commons::buffToInteger(buff); + + pos += 4; + int depth = pow(2, treeDepth); + + for (int s = 0; s < stages; s++) { + for (int t = 0; t < trees; t++) { + std::vector v2 = std::vector(packet.begin() + pos, + packet.begin() + pos + + 4 * depth - 4); + treeCodes.insert(treeCodes.end(), v2.data(), v2.data() + 4 * depth - 4); + pos += 4 * depth - 4; + + for (int i = 0; i < depth; i++) { + for (int l = 0; l < 2; l++) { + buff = {(packet)[pos + 3], (packet)[pos + 2], (packet)[pos + 1], + (packet)[pos + 0]}; + + treePreds.push_back(Commons::bytesToFloatLittleEndian(buff)); + pos += 4; + } + } + } + } + + + this->cascade = {.stages = stages, + .scales = scales, + .trees = trees, + .treeDepth = treeDepth, + .treeCodes = new int[treeCodes.size()], + .treePreds = new float[treePreds.size()] + }; + + std::copy(treeCodes.begin(), treeCodes.end(), this->cascade.treeCodes); + std::copy(treePreds.begin(), treePreds.end(), this->cascade.treePreds); + + } + + void + Puploc::classifyRegion(int r, int c, int s, int nrows, int ncols, const uint8_t *pixels, + int dim, bool flipV, int *res) const { + + int c1, c2, root = 0, idx, r1, r2, lutIdx, i, j, k; + float dr, dc; + int treeDepth = pow(2, cascade.treeDepth); + + auto flipF = flipV ? -1 : 1; + + auto minR = nrows - 1; + auto minC = ncols - 1; + + const auto stages = cascade.stages; + const auto trees = cascade.trees; + const auto scales = cascade.scales; + const auto tdepth = cascade.treeDepth; + const auto treeCodes = cascade.treeCodes; + const auto treePreds = cascade.treePreds; + + for (i = 0; i < stages; ++i) { + dr = 0.0, dc = 0.0; + for (j = 0; j < trees; ++j) { + idx = 0; + for (k = 0; k < tdepth; ++k) { + r1 = min(minR, + max(0, (256 * r + treeCodes[root + 4 * idx] * s) >> 8)); + c1 = min(minC, max(0, (256 * c + flipF * treeCodes[root + 4 * idx + 1] * s) >> 8)); + r2 = min(minR, + max(0, (256 * r + treeCodes[root + 4 * idx + 2] * s) >> 8)); + c2 = min(minC, max(0, (256 * c + flipF * treeCodes[root + 4 * idx + 3] * s) + >> 8)); + + idx = 2 * idx + 1 + (pixels[r1 * dim + c1] > pixels[r2 * dim + c2]); + } + lutIdx = + 2 * (trees * treeDepth * i + treeDepth * j + idx - (treeDepth - 1)); + + dr += treePreds[lutIdx + 0]; + dc += flipF * treePreds[lutIdx + 1]; + root += 4 * treeDepth - 4; + } + r += int(dr * s); + c += int(dc * s); + s *= scales; + } + + res[0] = r; + res[1] = c; + res[2] = s; + } + + + void Puploc::classifyRotatedRegion(int r, int c, int s, float angle, int nrows, + int ncols, const uint8_t *pixels, int dim, bool flipV, + int *res) const { + int c1, c2, root = 0, idx, r1, r2, lutIdx, row1, col1, row2, col2; + float dr, dc; + int treeDepth = pow(2, cascade.treeDepth); + + int qCosTable[] = {256, 251, 236, 212, 181, 142, 97, 49, 0, -49, -97, -142, -181, -212, + -236, -251, -256, -251, -236, -212, -181, -142, -97, -49, 0, 49, 97, + 142, 181, 212, 236, 251, 256}; + int qSinTable[] = {0, 49, 97, 142, 181, 212, 236, 251, 256, 251, 236, 212, 181, 142, 97, + 49, 0, -49, -97, -142, -181, -212, -236, -251, -256, -251, -236, -212, + -181, -142, -97, -49, 0}; + + int qsin = s * qSinTable[int(32.0 * angle)]; //s*(256.0*math.Sin(2*math.Pi*a)) + int qcos = s * qCosTable[int(32.0 * angle)]; //s*(256.0*math.Cos(2*math.Pi*a)) + + auto flipF = flipV ? -1 : 1; + + const auto stages = cascade.stages; + const auto trees = cascade.trees; + const auto scales = cascade.scales; + const auto tdepth = cascade.treeDepth; + const auto treeCodes = cascade.treeCodes; + const auto treePreds = cascade.treePreds; + + for (int i = 0; i < stages; i++) { + dr = 0.0, dc = 0.0; + for (int j = 0; j < trees; j++) { + idx = 0; + for (int k = 0; k < tdepth; k++) { + row1 = treeCodes[root + 4 * idx + 0]; + row2 = treeCodes[root + 4 * idx + 2]; + + col1 = flipF*treeCodes[root + 4 * idx + 1]; + col2 = flipF*treeCodes[root + 4 * idx + 3]; + + r1 = min(nrows - 1, + max(0, 65536 * int(r) + int(qcos) * row1 - int(qsin) * col1) >> 16); + c1 = min(ncols - 1, + max(0, 65536 * int(c) + int(qsin) * row1 + int(qcos) * col1) >> 16); + r2 = min(nrows - 1, + max(0, 65536 * int(r) + int(qcos) * row2 - int(qsin) * col2) >> 16); + c2 = min(ncols - 1, + max(0, 65536 * int(c) + int(qsin) * row2 + int(qcos) * col2) >> 16); + idx = 2 * idx + 1 + (pixels[r1 * dim + c1] > pixels[r2 * dim + c2]); + } + lutIdx = 2 * (trees * treeDepth * i + treeDepth * j + idx - + (treeDepth - 1)); + + dr += treePreds[lutIdx + 0]; + dc += flipF*treePreds[lutIdx + 1]; + + root += 4 * treeDepth - 4; + } + r += int(dr * s); + c += int(dc * s); + s *= scales; + } + + res[0] = r; + res[1] = c; + res[2] = s; + } + + PuplocDetection + Puploc::runDetector(PuplocDetection pl, int imRows, int imCols, const uint8_t *pixels, int dim, float angle, bool flipV) const { + int rows[pl.perturbs]; + int cols[pl.perturbs]; + int scale[pl.perturbs]; + int i = 0; + int res[3]; + int row = 0, col = 0, sc = 0; + + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_real_distribution dist(0.0, 1.0); + + for (i = 0; i < pl.perturbs; ++i) { + row = pl.row + float(pl.scale) * 0.15f * (0.5f - dist(mt)); + col = pl.col + float(pl.scale) * 0.15f * (0.5f - dist(mt)); + sc = pl.scale * (0.925f + 0.15f * dist(mt)); + + if (angle > 0.0) { + if (angle > 1.0) { + angle = 1.0; + } + classifyRotatedRegion(row, col, sc, angle, imRows, imCols, pixels, dim, flipV, + &res[0]); + } else { + classifyRegion(row, col, sc, imRows, imCols, pixels, dim, flipV, &res[0]); + } + + rows[i] = res[0]; + cols[i] = res[1]; + scale[i] = res[2]; + } + + std::sort(rows, rows + pl.perturbs, std::less()); + std::sort(cols, cols + pl.perturbs, std::less()); + std::sort(scale, scale + pl.perturbs, std::less()); + int pos = int(round(pl.perturbs / 2)); + + return { + .row = rows[pos], + .col = cols[pos], + .scale = scale[pos], + .perturbs = pl.perturbs + }; + } + + Puploc::Puploc() = default; + + Puploc::~Puploc() = default; + + +} \ No newline at end of file diff --git a/pidroid/src/main/cpp/pidroid/puploc.hpp b/pidroid/src/main/cpp/pidroid/puploc.hpp new file mode 100644 index 0000000..de57280 --- /dev/null +++ b/pidroid/src/main/cpp/pidroid/puploc.hpp @@ -0,0 +1,43 @@ +// +// Created by adrian on 24/01/21. +// +#pragma once +#include +#include +#include +#include "commons.hpp" + +using namespace std; + +namespace pidroidlib { + struct PuplocDetection { + int row; + int col; + int scale; + int perturbs; + } ; + + struct PuplocCascade { + int stages; + float scales; + int trees; + int treeDepth; + int* treeCodes; + float* treePreds; + } ; + + class Puploc { + private: + PuplocCascade cascade{}; + public: + Puploc(); + ~Puploc(); + void unpackCascade(std::vector packet); + void classifyRegion(int r, int c, int s, int nrows, int ncols, const uint8_t* pixels, int dim, bool flipV, int *res) const; + void classifyRotatedRegion( int r, int c, int s, float angle, int nrows, int ncols, const uint8_t* pixels, int dim, bool flipV, int *res) const; + PuplocDetection runDetector(PuplocDetection pl, int rows, int cols, const uint8_t* pixels, int dim, float angle, bool flipV) const; + }; + + + +} diff --git a/pidroid/src/main/java/com/suaro/pidroid/Pidroid.kt b/pidroid/src/main/java/com/suaro/pidroid/Pidroid.kt new file mode 100644 index 0000000..44531fb --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/Pidroid.kt @@ -0,0 +1,27 @@ +package com.suaro.pidroid + +import android.content.Context +import android.content.res.AssetManager +import com.suaro.pidroid.core.FaceDetectionResult +import com.suaro.pidroid.core.PidroidConfig +import com.suaro.pidroid.core.NativeMethods + +object Pidroid { + + private lateinit var mgr: AssetManager + private lateinit var cascadeConfig: PidroidConfig + + + fun setup(context: Context, config: PidroidConfig) { + this.mgr = context.resources.assets + this.cascadeConfig = config + + NativeMethods.setup(cascadeConfig, mgr) + } + + fun detectFace(byteArray: IntArray, width: Int, height: Int, dInfo: FaceDetectionResult) { + NativeMethods.detectFace(byteArray, width, height, dInfo); + } + + +} \ No newline at end of file diff --git a/pidroid/src/main/java/com/suaro/pidroid/core/Eye.kt b/pidroid/src/main/java/com/suaro/pidroid/core/Eye.kt new file mode 100644 index 0000000..5d04360 --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/core/Eye.kt @@ -0,0 +1,6 @@ +package com.suaro.pidroid.core + +class Eye { + var center: Point = Point(0,0) + var radius: Int = 0 +} \ No newline at end of file diff --git a/pidroid/src/main/java/com/suaro/pidroid/core/Face.kt b/pidroid/src/main/java/com/suaro/pidroid/core/Face.kt new file mode 100644 index 0000000..0066c82 --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/core/Face.kt @@ -0,0 +1,9 @@ +package com.suaro.pidroid.core + +class Face { + var topLeft: Point = Point(0,0) + var width: Int = 0 + var height: Int = 0 + var eyes: ArrayList = ArrayList() + var landmarks: ArrayList = ArrayList() +} \ No newline at end of file diff --git a/pidroid/src/main/java/com/suaro/pidroid/core/FaceDetectionResult.kt b/pidroid/src/main/java/com/suaro/pidroid/core/FaceDetectionResult.kt new file mode 100644 index 0000000..3e26e6c --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/core/FaceDetectionResult.kt @@ -0,0 +1,9 @@ +package com.suaro.pidroid.core + +class FaceDetectionResult { + + var faces: ArrayList = ArrayList() + var detected:Boolean = false; + + constructor() +} \ No newline at end of file diff --git a/pidroid/src/main/java/com/suaro/pidroid/core/Landmark.kt b/pidroid/src/main/java/com/suaro/pidroid/core/Landmark.kt new file mode 100644 index 0000000..150fed9 --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/core/Landmark.kt @@ -0,0 +1,6 @@ +package com.suaro.pidroid.core + +class Landmark { + var center: Point = Point(0,0) + var radius: Int = 0 +} \ No newline at end of file diff --git a/pidroid/src/main/java/com/suaro/pidroid/core/NativeMethods.kt b/pidroid/src/main/java/com/suaro/pidroid/core/NativeMethods.kt new file mode 100644 index 0000000..81a5c66 --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/core/NativeMethods.kt @@ -0,0 +1,16 @@ +package com.suaro.pidroid.core + +import android.content.res.AssetManager + + +class NativeMethods { + + companion object { + init { + System.loadLibrary("pidroid") + } + + external fun setup(config: PidroidConfig, mgr: AssetManager) + external fun detectFace(rgbaBytes: IntArray, width: Int, height: Int, detectionInfo: FaceDetectionResult) + } +} \ No newline at end of file diff --git a/pidroid/src/main/java/com/suaro/pidroid/core/PidroidConfig.kt b/pidroid/src/main/java/com/suaro/pidroid/core/PidroidConfig.kt new file mode 100644 index 0000000..6bad314 --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/core/PidroidConfig.kt @@ -0,0 +1,22 @@ +package com.suaro.pidroid.core + +class PidroidConfig { + var minsize: Int = 150 + var maxsize: Int = 1000 + + var angle: Float = 0f + + var scalefactor: Float = 1.1f + var stridefactor: Float = 0.1f + + var qthreshold: Float = 3.0f + + var perturbs: Int = 10; + + var clustering: Boolean = true + + var pupilDetectionEnable: Boolean = true + var landmarkDetectionEnable: Boolean = true + + var prominentFaceOnly: Boolean = true +} \ No newline at end of file diff --git a/pidroid/src/main/java/com/suaro/pidroid/core/Point.kt b/pidroid/src/main/java/com/suaro/pidroid/core/Point.kt new file mode 100644 index 0000000..557450d --- /dev/null +++ b/pidroid/src/main/java/com/suaro/pidroid/core/Point.kt @@ -0,0 +1,5 @@ +package com.suaro.pidroid.core + +class Point constructor(var x: Int, var y: Int) { + +} \ No newline at end of file diff --git a/pidroid/src/test/java/com/suaro/pidroid/ExampleUnitTest.kt b/pidroid/src/test/java/com/suaro/pidroid/ExampleUnitTest.kt new file mode 100644 index 0000000..2b70b42 --- /dev/null +++ b/pidroid/src/test/java/com/suaro/pidroid/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.suaro.pidroid + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..91bab70 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +include ':pidroid' +include ':app' +rootProject.name = "PiDroid" \ No newline at end of file