From a84e1e3ae09c1c590670a0abc335e37737673b18 Mon Sep 17 00:00:00 2001 From: Joss STUART Date: Wed, 28 Oct 2020 14:51:15 +0100 Subject: [PATCH] Add English and French dialog to the sample application --- .../facemaskdetection/FaceMaskDetection.kt | 2 +- app/build.gradle | 14 +- .../peppermaskdetection/MainActivity.kt | 231 ++++++++++++++++-- app/src/main/res/drawable/sbrlogo.jpg | Bin 0 -> 6140 bytes app/src/main/res/layout/activity_main.xml | 49 +++- app/src/main/res/layout/face_layout_small.xml | 20 +- app/src/main/res/raw-fr/chat.top | 108 ++++++++ app/src/main/res/raw/chat.top | 79 ++++++ app/src/main/res/values/strings.xml | 2 +- build.gradle | 2 +- 10 files changed, 454 insertions(+), 53 deletions(-) create mode 100644 app/src/main/res/drawable/sbrlogo.jpg create mode 100644 app/src/main/res/raw-fr/chat.top create mode 100644 app/src/main/res/raw/chat.top diff --git a/FaceMaskDetection/src/main/java/com/softbankrobotics/facemaskdetection/FaceMaskDetection.kt b/FaceMaskDetection/src/main/java/com/softbankrobotics/facemaskdetection/FaceMaskDetection.kt index 290de2f..a7e417e 100644 --- a/FaceMaskDetection/src/main/java/com/softbankrobotics/facemaskdetection/FaceMaskDetection.kt +++ b/FaceMaskDetection/src/main/java/com/softbankrobotics/facemaskdetection/FaceMaskDetection.kt @@ -28,7 +28,7 @@ class FaceMaskDetection(private val detector: FaceMaskDetector, private val came private val detectionScope = CoroutineScope( ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, - ArrayBlockingQueue(1), ThreadPoolExecutor.DiscardOldestPolicy()).asCoroutineDispatcher()) + ArrayBlockingQueue(1), ThreadPoolExecutor.DiscardOldestPolicy()).asCoroutineDispatcher()) sealed class Message { class FaceMaskDetect( diff --git a/app/build.gradle b/app/build.gradle index db20e18..ddac0c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,11 +7,11 @@ android { buildToolsVersion "29.0.3" defaultConfig { - applicationId "com.softbankrobotics.peppermaskdetection" + applicationId "com.softbankrobotics.dx.peppermaskdetection" minSdkVersion 23 targetSdkVersion 29 - versionCode 6 - versionName "1.3.2" + versionCode 9 + versionName "2.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -61,4 +61,12 @@ dependencies { // OpenCV implementation project(":OpenCV") implementation project(":FaceMaskDetection") + + implementation 'com.aldebaran:qisdk-conversationalcontent-greetings:0.19.1-experimental-05' + implementation 'com.aldebaran:qisdk-conversationalcontent-robotabilities:0.19.1-experimental-05' + implementation 'com.aldebaran:qisdk-conversationalcontent-volumecontrol:0.19.1-experimental-05' + implementation 'com.aldebaran:qisdk-conversationalcontent-farewell:0.19.1-experimental-05' + implementation 'com.aldebaran:qisdk-conversationalcontent-datetime:0.19.1-experimental-05' + implementation 'com.aldebaran:qisdk-conversationalcontent-askrobotname:0.19.1-experimental-05' + implementation 'com.aldebaran:qisdk-conversationalcontent-conversationbasics:0.19.1-experimental-05' } diff --git a/app/src/main/java/com/softbankrobotics/peppermaskdetection/MainActivity.kt b/app/src/main/java/com/softbankrobotics/peppermaskdetection/MainActivity.kt index 54ab031..82ef9f2 100644 --- a/app/src/main/java/com/softbankrobotics/peppermaskdetection/MainActivity.kt +++ b/app/src/main/java/com/softbankrobotics/peppermaskdetection/MainActivity.kt @@ -14,12 +14,22 @@ import com.aldebaran.qi.Future import com.aldebaran.qi.sdk.QiContext import com.aldebaran.qi.sdk.QiSDK import com.aldebaran.qi.sdk.RobotLifecycleCallbacks +import com.aldebaran.qi.sdk.`object`.conversation.* +import com.aldebaran.qi.sdk.builder.QiChatbotBuilder +import com.aldebaran.qi.sdk.builder.TopicBuilder +import com.aldebaran.qi.sdk.conversationalcontentlibrary.askrobotname.AskRobotNameConversationalContent +import com.aldebaran.qi.sdk.conversationalcontentlibrary.base.AbstractConversationalContent +import com.aldebaran.qi.sdk.conversationalcontentlibrary.base.ConversationalContentChatBuilder +import com.aldebaran.qi.sdk.conversationalcontentlibrary.datetime.DateTimeConversationalContent +import com.aldebaran.qi.sdk.conversationalcontentlibrary.farewell.FarewellConversationalContent +import com.aldebaran.qi.sdk.conversationalcontentlibrary.greetings.GreetingsConversationalContent +import com.aldebaran.qi.sdk.conversationalcontentlibrary.robotabilities.RobotAbilitiesConversationalContent +import com.aldebaran.qi.sdk.conversationalcontentlibrary.volumecontrol.VolumeControlConversationalContent import com.aldebaran.qi.sdk.design.activity.RobotActivity -import com.aldebaran.qi.sdk.design.activity.conversationstatus.SpeechBarDisplayStrategy import com.softbankrobotics.facemaskdetection.FaceMaskDetection import com.softbankrobotics.facemaskdetection.capturer.BottomCameraCapturer -import com.softbankrobotics.facemaskdetection.detector.AizooFaceMaskDetector import com.softbankrobotics.facemaskdetection.capturer.TopCameraCapturer +import com.softbankrobotics.facemaskdetection.detector.AizooFaceMaskDetector import com.softbankrobotics.facemaskdetection.detector.FaceMaskDetector import com.softbankrobotics.facemaskdetection.utils.OpenCVUtils import com.softbankrobotics.facemaskdetection.utils.TAG @@ -27,11 +37,14 @@ import kotlinx.android.synthetic.main.activity_main.* class MainActivity : RobotActivity(), RobotLifecycleCallbacks { - private val useTopCamera = false + private var qiChatbot: QiChatbot? = null + private var chatFuture: Future? = null + private val useTopCamera = true + private var shouldBeRecognizing = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setSpeechBarDisplayStrategy(SpeechBarDisplayStrategy.IMMERSIVE) + //setSpeechBarDisplayStrategy(SpeechBarDisplayStrategy.IMMERSIVE) setContentView(R.layout.activity_main) clearFaces() if (useTopCamera || cameraPermissionAlreadyGranted()) { @@ -45,6 +58,8 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { public override fun onPause() { super.onPause() + Log.i(TAG, "onPause") + shouldBeRecognizing = false detectionFuture?.requestCancellation() detectionFuture = null } @@ -53,6 +68,7 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { super.onResume() OpenCVUtils.loadOpenCV(this) clearFaces() + shouldBeRecognizing = true startDetecting() } @@ -73,14 +89,17 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { private fun requestPermissionForCamera() { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { - Toast.makeText(this, + Toast.makeText( + this, R.string.permissions_needed, - Toast.LENGTH_LONG).show() + Toast.LENGTH_LONG + ).show() } else { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.CAMERA), - CAMERA_PERMISSION_REQUEST_CODE) + CAMERA_PERMISSION_REQUEST_CODE + ) } } @@ -89,9 +108,11 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { return resultCamera == PackageManager.PERMISSION_GRANTED } - override fun onRequestPermissionsResult(requestCode: Int, - permissions: Array, - grantResults: IntArray) { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { var cameraPermissionGranted = true @@ -102,9 +123,11 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { if (cameraPermissionGranted) { QiSDK.register(this, this) } else { - Toast.makeText(this, + Toast.makeText( + this, R.string.permissions_needed, - Toast.LENGTH_LONG).show() + Toast.LENGTH_LONG + ).show() } } } @@ -116,17 +139,18 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { inner class FacesForDisplay(rawFaces: List) { // Choose the "main" focused faced, which is either the biggest or, when there are a lot of // people, the one in the middle. - val mainFace : FaceMaskDetector.DetectedFace? = when { + val mainFace: FaceMaskDetector.DetectedFace? = when { rawFaces.size >= 5 -> rawFaces[2] rawFaces.size == 4 -> rawFaces.subList(1, 3).maxBy { it.size() } else -> rawFaces.maxBy { it.size() } } private val mainFaceIndex = rawFaces.indexOf(mainFace) + // Set the other faces relatively - val leftFarFace : FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex - 2) - val leftNearFace : FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex - 1) - val rightNearFace : FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex + 1) - val rightFarFace : FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex + 2) + val leftFarFace: FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex - 2) + val leftNearFace: FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex - 1) + val rightNearFace: FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex + 1) + val rightFarFace: FaceMaskDetector.DetectedFace? = rawFaces.getOrNull(mainFaceIndex + 2) } private fun setFaces(faces: List) { @@ -150,7 +174,11 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { } } - private fun setFace(card: View, face: FaceMaskDetector.DetectedFace?, hideIfEmpty : Boolean = true) { + private fun setFace( + card: View, + face: FaceMaskDetector.DetectedFace?, + hideIfEmpty: Boolean = true + ) { if (hideIfEmpty && face == null) { card.visibility = View.INVISIBLE } else { @@ -167,7 +195,8 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { resources.getColor(R.color.colorNoMaskDetected, null) } circle.setColorFilter(color) - label.text = resources.getString(if(face.hasMask) R.string.mask else R.string.no_mask) + label.text = + resources.getString(if (face.hasMask) R.string.mask else R.string.no_mask) } else { photo.visibility = View.INVISIBLE circle.setColorFilter(resources.getColor(R.color.colorNobody, null)) @@ -176,27 +205,178 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { } } + + /********************** + * Greetings + **********************/ + + var engaged = false + var sawNobodyCount = 0 + + private fun jumpToBookmark(bookmark: Bookmark?) { + bookmark?.let { + qiChatbot?.async()?.goToBookmark( + it, + AutonomousReactionImportance.HIGH, + AutonomousReactionValidity.DELAYABLE + ) + } + } + + var lastSawWithoutMask = false + var annoyance = 0 + + var lastMentionedPeopleNum = 0 + var worthMentioningPeopleCounter = 0 + + private fun updateState(seesWithMask: Boolean, seesWithoutMask: Boolean, numPeople: Int) { + val seesSomebody = seesWithMask || seesWithoutMask + Log.w( + TAG, + "DBG updating state wMask=${seesWithMask} noMask=${seesWithoutMask} engaged=${engaged}" + ) + + if (engaged) { + // Update status when already engaged + if (seesSomebody) { + sawNobodyCount = 0 // Stay engaged + // See if they put on a mask + var saidSomething = false + + // See if it's worth mentioning people putting on masks or taking them off + if (seesWithoutMask == lastSawWithoutMask) { + annoyance = 0 + } else { + // Something changed ! + annoyance += 1 + if (annoyance >= 2) { + annoyance = 0 + lastSawWithoutMask = seesWithoutMask + saidSomething = true + if (seesWithoutMask) { + if (seesWithMask) { + jumpToBookmark(newWithoutMaskBookmark) + } else { + jumpToBookmark(tookOffMaskBookmark) + } + } else { + jumpToBookmark(putOnMaskBookmark) + } + lastMentionedPeopleNum = numPeople + worthMentioningPeopleCounter = 0 + } + } + + // See if it's worth mentioning a lot of people + if (numPeople > lastMentionedPeopleNum) { + if (worthMentioningPeopleCounter > 2) { + lastMentionedPeopleNum = numPeople + worthMentioningPeopleCounter = 0 + jumpToBookmark(manyPeopleBookmark) + } else { + worthMentioningPeopleCounter += 1 + } + } else { + lastMentionedPeopleNum = numPeople + worthMentioningPeopleCounter = 0 + } + + } else { + sawNobodyCount += 1 + if (sawNobodyCount > 2) { + engaged = false + //chat?.removeAllOnStartedListeners() + //chatFuture?.cancel(false) + chatFuture = null + lastSawWithoutMask = false + annoyance = 0 + } + } + } else if (seesSomebody) { + engaged = true + chat?.let { chat -> + if (seesWithoutMask) { + jumpToBookmark(noMaskBookmark) + } else { + jumpToBookmark(maskBookmark) + } + lastSawWithoutMask = seesWithoutMask + annoyance = 0 + } + } + } + + /********************** * Robot Lifecycle **********************/ + private fun startDetecting() { detectionFuture = detection?.start { faces -> - // Filter and sort the faces so that they're left to right, with no uncertain or - // non-unique results + // Filter and sort the faces so that they're left to right and certain enough val sortedFaces = faces - .filter { (it.confidence > 0.5)} + .filter { (it.confidence > 0.5) } .sortedBy { -it.bb.left } - Log.v(TAG, "Filtered faces ${faces.size}, -> ${sortedFaces.size}") + Log.i(TAG, "Filtered faces ${faces.size}, -> ${sortedFaces.size}") setFaces(sortedFaces) + // Now update the logic + val seesWithMask = sortedFaces.any { it.hasMask } + val seesWithoutMask = sortedFaces.any { !it.hasMask } + val numPeople = sortedFaces.size + updateState(seesWithMask, seesWithoutMask, numPeople) } detectionFuture?.thenConsume { - Log.i(TAG, "Detection future has finished: success=${it.isSuccess}, cancelled=${it.isCancelled}") + Log.i( + TAG, + "Detection future has finished: success=${it.isSuccess}, cancelled=${it.isCancelled}" + ) + if (shouldBeRecognizing) { + Log.w(TAG, "Stopped, but it shouldn't have - starting it again") + startDetecting() + } } } + var chat: Chat? = null + var maskBookmark: Bookmark? = null + var noMaskBookmark: Bookmark? = null + var tookOffMaskBookmark: Bookmark? = null + var putOnMaskBookmark: Bookmark? = null + var newWithoutMaskBookmark: Bookmark? = null + var manyPeopleBookmark: Bookmark? = null + override fun onRobotFocusGained(qiContext: QiContext) { Log.i(TAG, "onRobotFocusGained") + if (chat == null) { + val topic = TopicBuilder.with(qiContext).withResource(R.raw.chat).build() + val qiChatbot = QiChatbotBuilder.with(qiContext).withTopic(topic).build() + this.qiChatbot = qiChatbot + maskBookmark = topic.bookmarks["GREETING_MASK"] + noMaskBookmark = topic.bookmarks["GREETING_NO_MASK"] + tookOffMaskBookmark = topic.bookmarks["TOOK_OFF_MASK"] + putOnMaskBookmark = topic.bookmarks["PUT_ON_MASK"] + newWithoutMaskBookmark = topic.bookmarks["NEW_WITHOUT_MASK"] + manyPeopleBookmark = topic.bookmarks["MANY_PEOPLE"] + + val conversationalContents: List = listOf( + GreetingsConversationalContent(), + FarewellConversationalContent(), + AskRobotNameConversationalContent(), + DateTimeConversationalContent(), + RobotAbilitiesConversationalContent(), + VolumeControlConversationalContent() + ) + + chat = ConversationalContentChatBuilder.with(qiContext) + .withChatbot(qiChatbot) + .withConversationalContents(conversationalContents) + .build() + chat?.listeningBodyLanguage = BodyLanguageOption.DISABLED + } + + Log.i(TAG, "Initialised chat") + val capturer = if (useTopCamera) { TopCameraCapturer(qiContext) } else { @@ -204,7 +384,10 @@ class MainActivity : RobotActivity(), RobotLifecycleCallbacks { } val detector = AizooFaceMaskDetector(this) detection = FaceMaskDetection(detector, capturer) + shouldBeRecognizing = true startDetecting() + Log.i(TAG, "Starting chat") + chatFuture = chat?.async()?.run() } override fun onRobotFocusLost() { diff --git a/app/src/main/res/drawable/sbrlogo.jpg b/app/src/main/res/drawable/sbrlogo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af4dbe438cb6b6044849bb40d2fad40418365f78 GIT binary patch literal 6140 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 tbR1532=2Yx-g - - - - + + + + + + + + + +