Skip to content
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ COPY . /app
RUN ./mvnw package
CMD ["java", "-jar", "target/spring-0.0.1-SNAPSHOT.jar"]
EXPOSE 8585
EXPOSE 8589
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ services:
build: .
ports:
- "8585:8585"
- "8589:8589"
volumes:
- ./volumes:/app/volumes
restart: unless-stopped
121 changes: 121 additions & 0 deletions scripts/deepface_match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
import sys
import os
import json
import base64
import tempfile

def decode_base64_image(base64_image):
if ',' in base64_image:
base64_image = base64_image.split(',')[1]
try:
return base64.b64decode(base64_image)
except Exception as e:
raise ValueError(f"Invalid base64 image data: {e}")


def image_to_embedding(base64_image):
try:
from deepface import DeepFace
except ImportError:
raise ImportError("deepface module is required. install with pip install deepface")

binary = decode_base64_image(base64_image)

with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
tmp.write(binary)
temp_path = tmp.name

try:
# Use VGG-Face for compatibility with existing pipeline
results = DeepFace.represent(img_path=temp_path, model_name='VGG-Face', enforce_detection=False)
if not results or len(results) == 0 or 'embedding' not in results[0]:
raise ValueError('Could not generate embedding from image')
return results[0]['embedding']
finally:
try:
os.remove(temp_path)
except Exception:
pass


def cosine_distance(a, b):
import numpy as np
a = np.asarray(a, dtype=np.float64)
b = np.asarray(b, dtype=np.float64)
if a.size == 0 or b.size == 0:
return 1.0
dot = np.dot(a, b)
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
if norm_a == 0 or norm_b == 0:
return 1.0
similarity = dot / (norm_a * norm_b)
return 1.0 - similarity


def main():
try:
payload = json.load(sys.stdin)

image = payload.get('image')
if not image:
print(json.dumps({'match': False, 'message': 'No image provided'}))
return

threshold = float(payload.get('threshold', 0.4))
candidates = payload.get('candidates', [])

if not candidates:
print(json.dumps({'match': False, 'message': 'No registered faces'}))
return

query_emb = image_to_embedding(image)

best_uid = None
best_name = None
best_dist = float('inf')

for candidate in candidates:
uid = candidate.get('uid')
face_data = candidate.get('faceData')
if not face_data or not uid:
continue

candidate_emb = None
# Check if face_data is a JSON array (legacy or pre-calculated embedding)
if isinstance(face_data, str) and face_data.strip().startswith('['):
try:
candidate_emb = json.loads(face_data)
except:
pass

if candidate_emb is None:
# Treat as base64 image
try:
candidate_emb = image_to_embedding(face_data)
except Exception as e:
continue

dist = cosine_distance(query_emb, candidate_emb)
if dist < best_dist:
best_dist = dist
best_uid = uid
best_name = candidate.get('name', uid)

if best_uid is not None and best_dist <= threshold:
print(json.dumps({'match': True, 'uid': best_uid, 'name': best_name, 'distance': float(best_dist)}))
else:
message = f"No match found (best dist: {best_dist:.4f})" if best_dist < 1.0 else "No face detected"
print(json.dumps({'match': False, 'message': message, 'distance': float(best_dist)}))

except json.JSONDecodeError:
print(json.dumps({'match': False, 'message': 'Invalid JSON payload'}))
except ImportError as e:
print(json.dumps({'match': False, 'message': f'Dependency error: {str(e)}'}))
except Exception as e:
print(json.dumps({'match': False, 'message': f'Internal error: {str(e)}'}))


if __name__ == '__main__':
main()
79 changes: 58 additions & 21 deletions src/main/java/com/open/spring/mvc/bathroom/BathroomQueue.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import jakarta.persistence.Column;
Expand All @@ -27,6 +28,9 @@ public class BathroomQueue {
private String peopleQueue;
private int away;

@Column(columnDefinition = "int default 1")
private int maxOccupancy = 1;

// Custom constructor

/**
Expand Down Expand Up @@ -54,24 +58,59 @@ public void addStudent(String studentName) {
}
}

/**
* Helper to check if a student is already in the queue
*/
public boolean containsStudent(String studentName) {
if (this.peopleQueue == null || this.peopleQueue.isEmpty())
return false;
return Arrays.asList(this.peopleQueue.split(",")).contains(studentName);
}

/**
* Helper to get the index of a student in the queue
*/
public int getStudentIndex(String studentName) {
if (this.peopleQueue == null || this.peopleQueue.isEmpty())
return -1;
List<String> students = Arrays.asList(this.peopleQueue.split(","));
return students.indexOf(studentName);
}

/**
* Function to remove the student from a queue
*
* @param studentName - the name you want to remove from the queue. In frontend,
* your own name is passed.
* @param studentName - the name you want to remove from the queue.
*/
public void removeStudent(String studentName) {
if (this.peopleQueue != null) {
this.peopleQueue = Arrays.stream(this.peopleQueue.split(","))
.filter(s -> !s.equals(studentName))
.collect(Collectors.joining(","));
if (this.peopleQueue != null && !this.peopleQueue.isEmpty()) {
String[] studentsBefore = this.peopleQueue.split(",");
int studentIndex = -1;
for (int i = 0; i < studentsBefore.length; i++) {
if (studentsBefore[i].equals(studentName)) {
studentIndex = i;
break;
}
}

if (studentIndex != -1) {
// Remove the student
this.peopleQueue = Arrays.stream(studentsBefore)
.filter(s -> !s.equals(studentName))
.collect(Collectors.joining(","));

// ONLY decrease away if the student was actually in the "away" portion
if (studentIndex < this.away) {
if (this.away > 0) {
this.away--;
}
}
}
}
}


/**
* @return - returns the student who is at the front of the line, removing the
* commas and sanitizing the data
* @return - returns the student who is at the front of the line
*/
public String getFrontStudent() {
if (this.peopleQueue != null && !this.peopleQueue.isEmpty()) {
Expand All @@ -86,19 +125,17 @@ public String getFrontStudent() {
* When they return, they are removed from the queue
*/
public void approveStudent() {
if (this.peopleQueue != null && !this.peopleQueue.isEmpty()) {
if (this.away == 0) {
// Student is approved to go away
this.away = 1;
} else {
// Student has returned; remove from queue
String[] students = this.peopleQueue.split(",");
if (students.length > 1) {
this.peopleQueue = String.join(",", Arrays.copyOfRange(students, 1, students.length));
} else {
this.peopleQueue = "";
if (this.peopleQueue != null && !this.peopleQueue.isEmpty()) {
if (this.away < this.maxOccupancy) {
// Determine how many people are actually in the queue
int totalInQueue = this.peopleQueue.split(",").length;
// We can't have more people 'away' than are in the queue
if (this.away < totalInQueue) {
this.away++;
}
this.away = 0;
} else {
// If already at max occupancy, we don't increment away.
// The frontend should handle showing they are in the waiting list.
}
} else {
throw new IllegalStateException("Queue is empty");
Expand Down
Loading