diff --git a/main.py b/main.py index f1f091bb35f0889e2d1e6c8c7e06497651074e5e..fcb6efd8d9eb77b9c9d626e0e2c4a4830240859a 100644 --- a/main.py +++ b/main.py @@ -16,60 +16,64 @@ from fastapi.responses import StreamingResponse import imutils - +# Ignorierung der Warnings warnings.filterwarnings("ignore") -# Initialisiere Text-to-Speech +# Initialisiere Text-to-Speech Engine engine = pyttsx3.init('sapi5') - +# Initialisierung einer Instanz von FastAPI für die Bereitstellung des Videostreams app = FastAPI() -# Definiere Kamera und starte Videoaufnahme +# Try and expect Block, um den Videostream des Video Capturing Services einzufangen. Hierbei wird versucht die +# Verbindung herzustellen und wenn keine hergestellt werden kann, wird das Programm abgebrochen! try: video_capture =VideoStream("http://localhost:80/video_feed").start() except Exception as e: print(f"We couldn't reach the camera") exit() -# Definiere, welche Cascade OpenCV verwenden soll + +# Definiere, welche Cascade OpenCV verwenden soll. Dabei können verschiedene ausgewählt werden, +# für unterschiedlichste Usecases face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml') +# Wartet, um sicherzugehen, dass die Verbindung mit dem Videostream established ist. time.sleep(2.0) +#Main Klasse in dem, die meiste Logik unseres Skripts abläuft class Main: - + #Initialisierung der benötigten Variablen für unser Skript def __init__(self): - self.outputFrame = None - self.lock = threading.Lock() - self.wait_in_thread = None - self.streamThread = None - self.analysis_results = None - self.analysis_thread = None - self.speaking_thread = None # Verfolgt den aktuellen Sprech-Thread - self.main_time = 0 - self.last_chat_time = 0 # Zeitpunkt des letzten Sprechens API kosten beachten - self.currentclient = None - self.unacked_publish = set() - self.flag = False - self.selection = 3 + self.outputFrame = None # Outputframe für den Stream. + self.lock = threading.Lock() #Instanz der Threading.lock Klasse, welches ein primitives Lock object darstellt. Nur ein Thread kann auf dieses immer zugreifen und sperrt es für die anderen + self.streamThread = None #Variable für den Streamthread + self.analysis_results = None # Analysergebnisse + self.analysis_thread = None #Variable für den Analysethread + self.speaking_thread = None # Variable für den aktuellen Speakingthread + self.main_time = 0 # Zeitpunkt der letzten Gesichtsanalyse + self.last_chat_time = 0 # Zeitpunkt des letzten Sprechens - API kosten beachten + self.currentclient = None # Variable für den Mqtt client + self.unacked_publish = set() # Nicht rausgesendete MQTT Nachrichten + self.flag = False # Eine normale Flag für den Stream + self.selection = 3 #Variablen für die Logik des animierten Maskottchen self.it = 0 - + # Funktion für den Stream - öffnet eine Fastapi auf vorgegebenen IP und Port def stream(self): - uvicorn.run(host="localhost", port=89, app=app) + uvicorn.run(host="192.168.1.1", port=89, app=app) + # Thread für die Streaming-Funktion def streamingThread(self): self.streamThread = threading.Thread(target=self.stream) - self.streamThread.daemon = True + self.streamThread.daemon = True # Als Daemon gesetzt, damit der Code sich nach Terminierung der Main beendet self.streamThread.start() - + # Funktion für die Generierung des Streams def generate(self): - # grab global references to the output frame and lock variables # loop over frames from the output stream while True: # wait until the lock is acquired - with self.lock: + with self.lock: #Dieser Block wartet das ein Lock aquired wird und released es mit erfolgreichen Ausführen. # check if the output frame is available, otherwise skip # the iteration of the loop if self.outputFrame is None: @@ -82,50 +86,54 @@ class Main: # yield the output frame in the byte format yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(encodedImage) + b'\r\n') + #Funktion wird mit der erfolgreichen Verbindung zum MQTT ausgeführt - Subscribed den Client zu Topic 1 def on_connect(self,client, userdata, flags, reason_code, properties): print(f"Connected with result code {reason_code}\n") client.subscribe("Topic1") + #Funktion, die bei einem Publish in ein Topic ausgeführt wird. def on_publish(self,client, userdata, mid, reason_code, properties): try: - userdata.remove(mid) + userdata.remove(mid) #Removed die gesendete Nachricht vom Speicher, um eine doppelte Sendung zu verhindern except KeyError: "Something went wrong. Please check if you have removed the mid from the userdata" + # Funktion zur Verbindung mit MQTT def connectwithmqtt(self, adress:str, targetport:int): - mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, userdata=None) + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, userdata=None) #Intitalisierung des Clients mqttc.on_connect = self.on_connect mqttc.on_publish = self.on_publish - mqttc.user_data_set(self.unacked_publish) - mqttc.username_pw_set(username="standardUser", password="GreatHHZ4Ever!") + mqttc.user_data_set(self.unacked_publish) # Den lokalen Speicher als User data setzen + mqttc.username_pw_set(username="standardUser", password="GreatHHZ4Ever!") # Username und Passwort - vorgegeben vom MQTT Server mqttc.connect(host=adress, port=targetport) while not mqttc.is_connected(): mqttc.loop() print("Waiting for connection...") return mqttc + #Funktion zum senden einer Nachricht def sendMessage(self, mqttc, topic, message): - mqttc.loop_start() + mqttc.loop_start() # Started einen gethreaded connection loop zum senden der Nachrichten. msg = mqttc.publish(topic, message, qos=1) - self.unacked_publish.add(msg.mid) + self.unacked_publish.add(msg.mid) # Mid sind die Daten der aktuellen Nachricht while len(self.unacked_publish): - time.sleep(0.1) + time.sleep(0.1) # Wartet bis alle gequeten Nachrichten gepublished werden msg.wait_for_publish() - + # Funktion für Text to speeech. def speak(self, text): engine.say(text) engine.runAndWait() - + #Funktion fürs Threading von Text to Speech def speak_in_thread(self, text): - if (self.speaking_thread is None or not self.speaking_thread.is_alive()): + if (self.speaking_thread is None or not self.speaking_thread.is_alive()): #Abfrage, ob ein Thread am leben oder defifniert ist, wenn ja keine Ausführung! self.speaking_thread = threading.Thread(target=self.speak, args=(text,)) self.speaking_thread.daemon = True self.speaking_thread.start() - + # Funktion fürs Threading von der Gesichtsanalyse def facial_analysis_thread(self, faceframe): - if (self.analysis_thread is None or not self.analysis_thread.is_alive()): + if (self.analysis_thread is None or not self.analysis_thread.is_alive()): #Abfrage, ob ein Thread am leben oder definiert ist, wenn ja keine Ausführung! self.analysis_thread = threading.Thread(target=self.facial_analysis, args=(faceframe,)) self.analysis_thread.daemon = True self.analysis_thread.start() - + # Funktion Gesichtsanalyse def facial_analysis(self, faceframe): self.analysis_results = DeepFace.analyze(faceframe, actions=["emotion", "age", "gender"], enforce_detection=False) @@ -134,19 +142,20 @@ class Main: def welcome_message(self): self.speak_in_thread('Welcome to the face recognition and prediction tool of the herman hollerith center') + #Threading des Senden des ChatGPT prompts def send_emotion_to_chatgpt_and_speak(self, emotion): - current_time = time.time() - if current_time - self.last_chat_time < 10: + current_time = time.time() #Setzen der aktuellen Zeit + if current_time - self.last_chat_time < 10: #Timer für 10 Sekunden return - self.last_chat_time = current_time + self.last_chat_time = current_time # Reset des Timers request_thread = threading.Thread(target=self.perform_request, args=(emotion,)) request_thread.daemon = True request_thread.start() - + # Methode des senden eines ChatGPT prompts def perform_request(self, emotion): headers = { "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMmY4YTA2ODgtODIzNC00N2RhLTgwNjAtMjJhYjdlOWExNzE0IiwidHlwZSI6InNhbmRib3hfYXBpX3Rva2VuIn0.RbkSarkirWu8MyOved0gmafiGa0XauxoSaf1flSg3s4" - } + } #Übermitteln des Autorisierungsschlüssels zur Identifikation des Clients payload = { "providers": "openai", "text": f"Die Person fühlt sich {emotion} an. Antworte auf Deutsch ohne Komma und in einem kurzen Satz. Motiviere die Person, erwähne die {emotion} Emotion. Du bist in einem Face detection Programm versuche immer Variation in neuen Sätzen zu bringen.", @@ -155,31 +164,31 @@ class Main: "temperature": 0.0, "max_tokens": 150, "fallback_providers": "" - } + } # Prompt, welcher an Eden AI gesendet wird try: - response = requests.post("https://api.edenai.run/v2/text/chat", json=payload, headers=headers) - result = response.json() - generated_text = result['openai']['generated_text'] - print(generated_text) - self.speak_in_thread(generated_text) + response = requests.post("https://api.edenai.run/v2/text/chat", json=payload, headers=headers) #Senden der JSON an EdenAI + result = response.json() #Annahme der AI response in einer variable + generated_text = result['openai']['generated_text'] #Entnahme der relevanten Teile aus der JSON + print(generated_text) # Debug Print + self.speak_in_thread(generated_text) #Aufruf Speaking Thread mit dem generierten Tet except Exception as e: print(f"Fehler beim Senden/Empfangen der Daten zu/von ChatGPT: {e}") - + #Hauptfunktion - Gesichtserkennung def face_recognition(self, mqttc): - + #Unendliche Schleife für den Dauerzustand, welches Videofeed benötigt while True: - frame = video_capture.read() - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=10) - current_time = time.time() + frame = video_capture.read() # Einlesen des Camerafeeds + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #Konvertierung von dem echten Colorspace zu dem idealen für Computervision.Damit sollen die Ergebnisse genauer werden + faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=5) #Erkennung der Gesichter anhand mehrerer Parameter + current_time = time.time() #Aktueller Zeitpunkt try: - if current_time - self.main_time > 2: - self.main_time = current_time - self.facial_analysis_thread(frame) - analyze = self.analysis_results[0] if self.analysis_results else None - analysis_results_dict = {"age": analyze.get("age", "N/A"), "gender": analyze.get("dominant_gender", "N/A"), "emotion": analyze.get("dominant_emotion", "N/A")} - self.sendMessage(mqttc=mqttc, topic="Topic1", message=json.dumps(analysis_results_dict)) + if current_time - self.main_time > 5: #Timer + self.main_time = current_time #Reset Timer + self.facial_analysis_thread(frame) # Ausführung der Gesichtsanalyse + analyze = self.analysis_results[0] if self.analysis_results else None #Schaut, ob Analyseergebnisse vorliegen + analysis_results_dict = {"age": analyze.get("age", "N/A"), "gender": analyze.get("dominant_gender", "N/A"), "emotion": analyze.get("dominant_emotion", "N/A")} #Analyseergebnisse in ein Dict für Konvertierung in JSON notwendig + self.sendMessage(mqttc=mqttc, topic="Topic1", message=json.dumps(analysis_results_dict)) # Sendung der Ergebnisse durch MQTT except Exception as e: print("Error in DeepFace Analysis:", e) @@ -192,13 +201,13 @@ class Main: if emotion in ["happy", "neutral", "sad", "fear", "suprise", "angry"]: self.send_emotion_to_chatgpt_and_speak(emotion) - maskottchen_images = { + maskottchen_images = { #Bilder Maskotchen "mas1": cv2.imread("emoji_folder/mas1.png", cv2.IMREAD_UNCHANGED), "mas2": cv2.imread("emoji_folder/mas2.png", cv2.IMREAD_UNCHANGED), "mas3": cv2.imread("emoji_folder/mas3.png", cv2.IMREAD_UNCHANGED), } - emoji_images = { + emoji_images = { #Bilder Emojis "happy": cv2.imread("emoji_folder/happy.png", cv2.IMREAD_UNCHANGED), "neutral": cv2.imread("emoji_folder/neutral.png", cv2.IMREAD_UNCHANGED), "sad": cv2.imread("emoji_folder/sad.png", cv2.IMREAD_UNCHANGED), @@ -207,76 +216,80 @@ class Main: "angry": cv2.imread("emoji_folder/angry.png", cv2.IMREAD_UNCHANGED) } - for (x, y, w, h) in faces: - cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 1) - if analyze: + for (x, y, w, h) in faces: #Schleife, welches gleichzeitig das Gesichtsframe in seine Bestandteile (Länge, Breite, Höhe, ...) herunterbricht + cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 1) #Setzt über das Gesicht ein Rectangle + if analyze: # Wenn Analyse Ergebnisse vorliegen, werden diese als Text über das Gesicht gelegt cv2.putText(frame, f'Approx. Age: {analyze.get("age", "N/A")}', (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) cv2.putText(frame, f'Approx. Gender: {analyze.get("dominant_gender", "N/A")}', (x, y - 40), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 255), 2) cv2.putText(frame, f'Current emotion: {analyze.get("dominant_emotion", "N/A")}', (x, y - 70), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 0), 2) emotion = analyze.get("dominant_emotion", "N/A") - if emotion in emoji_images: + if emotion in emoji_images: #Abfrage, ob Emotion durch Emojis realiserbar - frame_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + frame_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) # Änderungen des Farbraum für Verwendung von Pillows emoji_pil = Image.fromarray(cv2.cvtColor(emoji_images[emotion], cv2.COLOR_BGRA2RGBA)) - emoji_resized = emoji_pil.resize((w+20, h+20)) + emoji_resized = emoji_pil.resize((w+20, h+20)) #Emoji wird vergrößert - x_offset = x-10 + x_offset = x-10 # Positionsvariablen für die Setzung des Emojis y_offset = y-10 - frame_pil.paste(emoji_resized, (x_offset, y_offset), mask=emoji_resized) + frame_pil.paste(emoji_resized, (x_offset, y_offset), mask=emoji_resized) # Emoji wird ins Frame gesetzt - frame = cv2.cvtColor(np.array(frame_pil), cv2.COLOR_RGB2BGR) + frame = cv2.cvtColor(np.array(frame_pil), cv2.COLOR_RGB2BGR) # Colorspace des Frames wird für Pillow angepasst - if self.speaking_thread and self.speaking_thread.is_alive(): + if self.speaking_thread and self.speaking_thread.is_alive(): #Abfrage über Tezt to speech self.it = self.it + 1 + #Einfügen von Pillows frame_pillow = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))# Auslagern mas_pil = Image.fromarray(cv2.cvtColor(maskottchen_images["mas1"], cv2.COLOR_BGRA2RGBA))# Auslagern mas_pil2 = Image.fromarray(cv2.cvtColor(maskottchen_images["mas2"], cv2.COLOR_BGRA2RGBA))# Auslagern mas_pil3 = Image.fromarray(cv2.cvtColor(maskottchen_images["mas3"], cv2.COLOR_BGRA2RGBA))# Auslagern - + #Resize Pillows mas_resized = mas_pil.resize((200, 200)) mas_resized2 = mas_pil2.resize((200, 200)) mas_resized3 = mas_pil3.resize((200, 200)) - + #Position x_offset = 480 y_offset = 300 - + #Animationslogik mit Modulo if self.it % self.selection == 0: frame_pillow.paste(mas_resized, (x_offset, y_offset), mask=mas_resized) if self.it % self.selection == 1: frame_pillow.paste(mas_resized2, (x_offset, y_offset), mask=mas_resized) if self.it % self.selection == 2: frame_pillow.paste(mas_resized3, (x_offset, y_offset), mask=mas_resized) - + #Zurück ins normale Format frame = cv2.cvtColor(np.array(frame_pillow), cv2.COLOR_RGB2BGR) - + # Frame für Stream wird kopiert with self.lock: self.outputFrame = frame.copy() - cv2.imshow("video_capture", frame) - + #Anzeigen des Videostreams für Debuggen + #cv2.imshow("video_capture", frame) + # Wenn q gedrückt wird, shut down program if cv2.waitKey(1) & 0xFF == ord('q'): print("Goodbye!") mqttc.disconnect() mqttc.loop_stop() break - + #Endet Videocapturing und zerstört die Videos video_capture.stop() cv2.destroyAllWindows() +#Main if __name__ == '__main__': main = Main() main.welcome_message() main.streamingThread() + #Setzung des Endpoints für Videostream @app.get("/video_feed") def video_feed(): # return the response generated along with the specific media # type (mime type) return StreamingResponse(main.generate(), - media_type="multipart/x-mixed-replace; boundary=frame") + media_type="multipart/x-mixed-replace; boundary=frame") # HTTP-Response mit Videostream main.currentclient =main.connectwithmqtt(adress="localhost", targetport=1883) main.face_recognition(mqttc=main.currentclient)