Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve #289 Find similar students #316

Merged
merged 2 commits into from
Dec 31, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
android:name=".content.task.StarActivity"
android:screenOrientation="fullSensor" />
<activity android:name=".content.task.LessonCompleteActivity" />
<activity android:name=".authentication.StudentImageCollectionActivity"/>
<activity android:name=".authentication.StudentImageCollectionActivity" />
<activity
android:name=".content.letter.LettersActivity"
android:icon="@mipmap/ic_launcher_literacy"
Expand Down Expand Up @@ -95,7 +95,7 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="org.literacyapp.CategoryActivity" />
</activity>
<activity android:name=".authentication.datacollection.StudentImageCollectionActivity"/>
<activity android:name=".authentication.datacollection.StudentImageCollectionActivity" />

<service
android:name=".service.ScreenOnService"
Expand Down Expand Up @@ -127,7 +127,11 @@
android:name=".authentication.fallback.StudentSelectionActivity"
android:label="@string/title_activity_student_selection"
android:theme="@style/AppTheme" />
<activity android:name=".authentication.AuthenticationActivity"/>
<activity android:name=".authentication.AuthenticationActivity" />

<service
android:name=".service.synchronization.MergeSimilarStudentsJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>

</manifest>
1 change: 1 addition & 0 deletions app/src/main/java/org/literacyapp/LiteracyApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class LiteracyApplication extends Application {
public static final int FACE_RECOGNITION_TRAINING_JOB_ID = 0;
public static final int CONTENT_SYNCRHONIZATION_JOB_ID = 1;
public static final int AUTHENTICATION_JOB_ID = 2;
public static final int MERGE_SIMILAR_STUDENTS_JOB_ID = 3;

public static final String PREF_APP_VERSION_CODE = "pref_app_version_code";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,23 @@ public class RecognitionThread extends Thread {
private Mat img;
private Student student;
private Gson gson;
private boolean featuresAlreadyExtracted;

public RecognitionThread(TensorFlow tensorFlow, StudentImageCollectionEventDao studentImageCollectionEventDao) {
this.tensorFlow = tensorFlow;
this.studentImageCollectionEventDao = studentImageCollectionEventDao;
gson = new Gson();
featuresAlreadyExtracted = false;
}

@Override
public void run() {
Mat featureVectorToRecognize = getFeatureVector(img);
Mat featureVectorToRecognize;
if (!featuresAlreadyExtracted){
featureVectorToRecognize = getFeatureVector(img);
} else {
featureVectorToRecognize = img;
}
student = getMostSimilarStudentIfInThreshold(featureVectorToRecognize);
}

Expand All @@ -60,6 +67,10 @@ public Student getStudent() {
return student;
}

public void setFeaturesAlreadyExtracted(boolean featuresAlreadyExtracted) {
this.featuresAlreadyExtracted = featuresAlreadyExtracted;
}

/**
* Returns the recognized Student if the cosineSimilarity was above the threshold
* @param featureVectorToRecognize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,29 @@ private File createAvatarFileFromStudentImage(StudentImage studentImage, Student

/**
* Find similar students
* Case 1: Student was added during fallback but in the meantime the same person has an existing StudentImageCollectionEvent and a new Student entry
*
*
*/
public synchronized void findAndMergeSimilarStudents(){
Log.i(getClass().getName(), "findAndMergeSimilarStudents");
PreProcessorFactory ppF = new PreProcessorFactory(context);
TensorFlow tensorFlow = getInitializedTensorFlow();
findSimilarStudentsUsingAvatarImages(ppF, tensorFlow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does an avatar image also include those taken manually in the fall-back registration activity? If so, you currently get a mix between 180px and 224px images. Will this inconsistency in image size/format cause problems?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because our library uses the default value 224 for resizing the image to 224 x 224 if no sharedPreference with the key "key_faceSize" exists (this key doesn't exist in our app because we don't have the Settings from the library included)

findSimilarStudentsUsingMeanFeatureVector(ppF, tensorFlow);
}

/**
* Find similar students
* Case 1: Student was added during fallback but in the meantime the same person has an existing StudentImageCollectionEvent and a new Student entry
* ---> Use the avatar image as input for the recognition
* @param ppF
* @param tensorFlow
*/
private synchronized void findSimilarStudentsUsingAvatarImages(PreProcessorFactory ppF, TensorFlow tensorFlow){
// Iterate through all Students
List<Student> students = studentDao.loadAll();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add logging here so that we can see if the method was triggered

Log.i(getClass().getName(), "findSimilarStudentsUsingAvatarImages");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Iterate through all students
for (Student student : students){
// Take the avatar image of the student
// Take the avatar image of the Student
Mat avatarImage = Imgcodecs.imread(student.getAvatar());
// Search for faces in the avatar image
List<Mat> faceImages = ppF.getCroppedImage(avatarImage);
Expand All @@ -334,26 +348,60 @@ public synchronized void findAndMergeSimilarStudents(){
Rect[] faces = ppF.getFacesForRecognition();
if (faces != null && faces.length == 1) {
// Proceed if exactly one face rectangle exists
RecognitionThread recognitionThread = new RecognitionThread(getInitializedTensorFlow(), studentImageCollectionEventDao);
RecognitionThread recognitionThread = new RecognitionThread(tensorFlow, studentImageCollectionEventDao);
recognitionThread.setImg(faceImage);
Log.i(getClass().getName(), "findAndMergeSimilarStudents: recognitionThread will be started to recognize student: " + student.getUniqueId());
Log.i(getClass().getName(), "findSimilarStudentsUsingAvatarImages: recognitionThread will be started to recognize student: " + student.getUniqueId());
recognitionThread.start();
try {
recognitionThread.join();
Student recognizedStudent = recognitionThread.getStudent();
if (recognizedStudent != null){
Log.i(getClass().getName(), "findAndMergeSimilarStudents: The student " + student.getUniqueId() + " has been recognized as " + recognizedStudent.getUniqueId());
Log.i(getClass().getName(), "findSimilarStudentsUsingAvatarImages: The student " + student.getUniqueId() + " has been recognized as " + recognizedStudent.getUniqueId());
mergeSimilarStudents(student, recognizedStudent);
} else {
Log.i(getClass().getName(), "findAndMergeSimilarStudents: The student " + student.getUniqueId() + " was not recognized");
Log.i(getClass().getName(), "findSimilarStudentsUsingAvatarImages: The student " + student.getUniqueId() + " was not recognized");
}
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}
}
}
}

/**
* Find similar students
* Case 2: Student was added regularly but maybe on another tablet or due to some reason the authentication didn't recognize the student correctly in the numberOfTries
* ---> Use the meanFeatureVector as input for the cosineSimilarityScore calculation
* @param ppF
* @param tensorFlow
*/
private synchronized void findSimilarStudentsUsingMeanFeatureVector(PreProcessorFactory ppF, TensorFlow tensorFlow){
// Iterate through all StudentImageCollectionEvents
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add Log.i(getClass().getName(), "findSimilarStudentsUsingMeanFeatureVector");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

List<StudentImageCollectionEvent> studentImageCollectionEvents = studentImageCollectionEventDao.loadAll();
for (StudentImageCollectionEvent studentImageCollectionEvent : studentImageCollectionEvents){
// Take the meanFeatureVector of the StudentImageCollectionEvent
List<Float> meanFeatureVectorList = gson.fromJson(studentImageCollectionEvent.getMeanFeatureVector(), new TypeToken<List<Float>>(){}.getType());
Mat meanFeatureVector = Converters.vector_float_to_Mat(meanFeatureVectorList);
RecognitionThread recognitionThread = new RecognitionThread(tensorFlow, studentImageCollectionEventDao);
recognitionThread.setImg(meanFeatureVector);
// To indicate, that this Mat object contains the already extracted features and therefore this step can be skipped in the RecognitionThread
recognitionThread.setFeaturesAlreadyExtracted(true);
Student student = studentImageCollectionEvent.getStudent();
Log.i(getClass().getName(), "findSimilarStudentsUsingMeanFeatureVector: recognitionThread will be started to recognize student: " + student.getUniqueId());
recognitionThread.start();
try {
recognitionThread.join();
Student recognizedStudent = recognitionThread.getStudent();
if (recognizedStudent != null){
Log.i(getClass().getName(), "findSimilarStudentsUsingMeanFeatureVector: The student " + student.getUniqueId() + " has been recognized as " + recognizedStudent.getUniqueId());
mergeSimilarStudents(student, recognizedStudent);
} else {
Log.i(getClass().getName(), "findSimilarStudentsUsingMeanFeatureVector: The student " + student.getUniqueId() + " was not recognized");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Expand All @@ -365,5 +413,6 @@ public synchronized void findAndMergeSimilarStudents(){
*/
private synchronized void mergeSimilarStudents(Student student1, Student student2){
Log.i(getClass().getName(), "mergeSimilarStudents: student1: " + student1.getUniqueId() + " student2: " + student2.getUniqueId());
// TODO Implement merging of students (maybe in another class like StudentMergeHelper)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be implemented in #314

}
}
20 changes: 18 additions & 2 deletions app/src/main/java/org/literacyapp/receiver/BootReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import org.literacyapp.service.FaceRecognitionTrainingJobService;
import org.literacyapp.service.ScreenOnService;
import org.literacyapp.service.synchronization.AuthenticationJobService;
import org.literacyapp.service.synchronization.MergeSimilarStudentsJobService;

public class BootReceiver extends BroadcastReceiver {
public static final int MINUTES_BETWEEN_AUTHENTICATIONS = 30;
private static final int MINUTES_BETWEEN_FACE_RECOGNITION_TRAININGS = 15;

@Override
public void onReceive(Context context, Intent intent) {
Expand All @@ -37,9 +39,12 @@ public void onReceive(Context context, Intent intent) {
// Initiate background job for face recognition training
scheduleFaceRecognitionTranining(context);

// Initiate authentication job for face recognition authentication
// Initiate background job for face recognition authentication
scheduleAuthentication(context, MINUTES_BETWEEN_AUTHENTICATIONS);

// Initiate background job for merging similar students
scheduleMergeSimilarStudents(context);

// Start service for detecting when the screen is turned on
Intent screenOnServiceIntent = new Intent(context, ScreenOnService.class);
context.startService(screenOnServiceIntent);
Expand All @@ -50,7 +55,7 @@ public void onReceive(Context context, Intent intent) {
public static void scheduleFaceRecognitionTranining(Context context){
ComponentName componentNameFaceRecognitionTranining = new ComponentName(context, FaceRecognitionTrainingJobService.class);
JobInfo.Builder builderFaceRecognitionTranining = new JobInfo.Builder(LiteracyApplication.FACE_RECOGNITION_TRAINING_JOB_ID, componentNameFaceRecognitionTranining);
int faceRecognitionTrainingPeriodic = 15 * 60 * 1000;
int faceRecognitionTrainingPeriodic = MINUTES_BETWEEN_FACE_RECOGNITION_TRAININGS * 60 * 1000;
builderFaceRecognitionTranining.setPeriodic(faceRecognitionTrainingPeriodic); // Every 15 minutes
JobInfo faceRecognitionTrainingJobInfo = builderFaceRecognitionTranining.build();
JobScheduler jobSchedulerFaceRecognitionTranining = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
Expand All @@ -68,4 +73,15 @@ public static void scheduleAuthentication(Context context, int minutesBetweenAut
jobSchedulerAuthentication.schedule(authenticationJobInfo);
Log.i(context.getClass().getName(), "AUTHENTICATION_JOB with ID " + LiteracyApplication.AUTHENTICATION_JOB_ID + " has been scheduled with periodic time = " + authenticationPeriodic);
}

public static void scheduleMergeSimilarStudents(Context context){
ComponentName componentNameMergeSimilarStudents = new ComponentName(context, MergeSimilarStudentsJobService.class);
JobInfo.Builder builderMergeSimilarStudents = new JobInfo.Builder(LiteracyApplication.MERGE_SIMILAR_STUDENTS_JOB_ID, componentNameMergeSimilarStudents);
boolean requiresCharging = true;
builderMergeSimilarStudents.setRequiresCharging(requiresCharging);
JobInfo mergeSimilarStudentsJobInfo = builderMergeSimilarStudents.build();
JobScheduler jobSchedulerMergeSimilarStudents = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobSchedulerMergeSimilarStudents.schedule(mergeSimilarStudentsJobInfo);
Log.i(context.getClass().getName(), "MERGE_SIMILAR_STUDENTS_JOB with ID " + LiteracyApplication.MERGE_SIMILAR_STUDENTS_JOB_ID + " has been scheduled with requiresCharging = " + requiresCharging);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public boolean onStartJob(JobParameters jobParameters) {
TrainingHelper trainingHelper = new TrainingHelper(getApplicationContext());
trainingHelper.extractFeatures();
trainingHelper.trainClassifier();
isRunning = false;
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public boolean onStartJob(JobParameters jobParameters) {
Log.i(getClass().getName(), "onStartJob");
if (!isScreenTurnedOff()){
if (didTheMinimumTimePassSinceLastAuthentication()){
if (!rescheduleIfTrainingJobServiceIsRunning()){
if (!rescheduleIfCpuIntensiveServicesAreRunning()){
Intent authenticationIntent = new Intent(getApplicationContext(), AuthenticationActivity.class);
authenticationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(authenticationIntent);
Expand All @@ -53,14 +53,21 @@ public boolean onStopJob(JobParameters jobParameters) {
return false;
}

private boolean rescheduleIfTrainingJobServiceIsRunning(){
private boolean rescheduleIfCpuIntensiveServicesAreRunning(){
boolean isFaceRecognitionTrainingJobServiceRunning = FaceRecognitionTrainingJobService.isRunning;
Log.i(getClass().getName(), "isFaceRecognitionTrainingJobServiceRunning: " + isFaceRecognitionTrainingJobServiceRunning);
if (isFaceRecognitionTrainingJobServiceRunning){

boolean isMergeSimilarStudentsJobServiceRunning = MergeSimilarStudentsJobService.isRunning;
Log.i(getClass().getName(), "isMergeSimilarStudentsJobServiceRunning: " + isMergeSimilarStudentsJobServiceRunning);

boolean isCpuIntensiveServiceRunning = (isFaceRecognitionTrainingJobServiceRunning || isMergeSimilarStudentsJobServiceRunning);

if (isCpuIntensiveServiceRunning){
BootReceiver.scheduleAuthentication(getApplicationContext(), BootReceiver.MINUTES_BETWEEN_AUTHENTICATIONS);
Log.i(getClass().getName(), "The AuthenticationJobService has been rescheduled, because the CPU hungry FaceRecognitionTrainingJobService is running at the moment.");
Log.i(getClass().getName(), "The AuthenticationJobService has been rescheduled, because a Cpu intensive service is running at the moment.");
}
return isFaceRecognitionTrainingJobServiceRunning;

return isCpuIntensiveServiceRunning;
}

private boolean isScreenTurnedOff(){
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.literacyapp.service.synchronization;

import android.app.Service;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import org.literacyapp.authentication.TrainingHelper;

public class MergeSimilarStudentsJobService extends JobService {
public static boolean isRunning = false;

@Override
public boolean onStartJob(JobParameters jobParameters) {
Log.i(getClass().getName(), "onStartJob");
isRunning = true;
TrainingHelper trainingHelper = new TrainingHelper(getApplicationContext());
trainingHelper.findAndMergeSimilarStudents();
isRunning = false;
return false;
}

@Override
public boolean onStopJob(JobParameters jobParameters) {
Log.i(getClass().getName(), "onStopJob");
return false;
}
}