diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..4fba0d6 --- /dev/null +++ b/constants.py @@ -0,0 +1,15 @@ +""" +Parameter: +-minimale und maximale Frequenz +-Alpha-Wert + + +Autor: Roberto Gelsinger +Datum: 07.12.2023 +Version: Modulversion +""" + + +freq_min = 1 # Minimale Frequenzgrenze +freq_max = 3 # Maximale Frequenzgrenze +alpha = 100 # Alpha-Wert für die Analyse \ No newline at end of file diff --git a/eulerian.py b/eulerian.py new file mode 100644 index 0000000..850fabf --- /dev/null +++ b/eulerian.py @@ -0,0 +1,52 @@ +""" +Abhängigkeiten: +- numpy +- scipy.signal (butter, lfilter) +- constants (für die Verwendung von 'alpha') + +Autor: Roberto Gelsinger +Datum: 07.12.2023 +Version: Modulversion +""" + +import numpy as np +from scipy.signal import butter, lfilter +from constants import alpha + +def fft_filter(video, freq_min, freq_max, fps): + """ + + Diese Funktion nimmt Videodaten, eine Frequenzbandbreite und die Bildrate (FPS) des Videos entgegen. + Sie filtert das Video, um nur Frequenzen im angegebenen Band zu verstärken. Das verstärkte Video, die FFT-Daten + und die Frequenzen werden zurückgegeben. + + Args: + video (ndarray): Die Videodaten als ndarray. + freq_min (float): Die untere Grenzfrequenz des zu verstärkenden Frequenzbands. + freq_max (float): Die obere Grenzfrequenz des zu verstärkenden Frequenzbands. + fps (int): Die Bildrate (Frames pro Sekunde) des Videos. + + Returns: + tuple: Ein Tupel, bestehend aus: + - amplified_video (ndarray): Das verstärkte Videodaten als ndarray. + - fft (ndarray): Die FFT-Daten des verstärkten Videos. + - frequencies (ndarray): Die zugehörigen Frequenzen der FFT. + """ + nyquist = 0.5 * fps + low = freq_min / nyquist + high = freq_max / nyquist + + # Min-Max-Frequenzen filtern + b, a = butter(4, [low, high], btype='band') + + filtered_video = np.zeros_like(video) + for i in range(video.shape[2]): + filtered_video[:, :, i] = lfilter(b, a, video[:, :, i]) + + # Verstärkung + amplified_video = np.abs(filtered_video) * alpha + + fft = np.fft.fft(amplified_video, axis=0) + frequencies = np.fft.fftfreq(amplified_video.shape[0], d=1.0 / fps) + + return amplified_video, fft, frequencies diff --git a/excel_evaluation.py b/excel_evaluation.py new file mode 100644 index 0000000..c0004e9 --- /dev/null +++ b/excel_evaluation.py @@ -0,0 +1,151 @@ +import openpyxl + +def excel_row_to_string(file_path): + # Öffne die Excel-Datei + workbook = openpyxl.load_workbook(file_path) + + # Wähle das Arbeitsblatt aus + sheet = workbook['Sheet1'] + + # Erhalte die angegebene Zeile als Liste von Zellen + row_values = [cell.value for cell in sheet[2]] + + # Ergebnisse werden ab Spalte 5 eingetragen + selected_columns = list(range(4, len(row_values))) + + # Wähle nur die gewünschten Spalten aus + selected_values = [row_values[col] for col in selected_columns] + + # Schließe die Excel-Datei + workbook.close() + + # Konvertiere die Liste von Zellen in einen String + row_string = ', '.join(str(value) for value in selected_values) + + return row_string + +def write_subdivided_string_to_excel(file_path, input_string): + # Öffne die Excel-Datei + workbook = openpyxl.load_workbook(file_path) + + # Wähle das Arbeitsblatt aus + sheet = workbook['Sheet1'] + + # Teile den String nach jedem Komma auf + parts = input_string.split(',') + + # Trage jeden Teil des Strings in eine neue Zeile ein + for i, part in enumerate(parts, 1): + + #Spalte 17 kann sich je nach Tabellenstruktur ändern! + sheet.cell(row=2 + i - 1, column=17, value=part.strip()) # strip entfernt mögliche Leerzeichen + + # Speichere die Änderungen + workbook.save(file_path) + + # Schließe die Excel-Datei + workbook.close() + +def read_columns(file_path): + # Öffne die Excel-Datei + workbook = openpyxl.load_workbook(file_path) + + # Wähle das Arbeitsblatt aus + sheet = workbook['Sheet1'] + + # Lese die Werte der beiden Spalten aus + values_column1 = [cell.value for cell in sheet['O']][1:] + values_column2 = [cell.value for cell in sheet['Q']][1:] + + # Schließe die Excel-Datei + workbook.close() + + return values_column1, values_column2 + +def calculate_deviation(liste1, liste2): + # Überprüfe, ob die Listen die gleiche Länge haben + if len(liste1) != len(liste2): + raise ValueError("Die Listen müssen die gleiche Länge haben") + + # Berechne die prozentuale Abweichung zwischen den Werten + deviations = [((abs(float(b) - float(a)) / float(a)) * 100) if float(a) != 0 else None for a, b in zip(liste1, liste2)] + + return deviations + +def write_string_to_excel(file_path, input_string, column): + # Öffne die Excel-Datei + workbook = openpyxl.load_workbook(file_path) + + # Wähle das Arbeitsblatt aus + sheet = workbook['Sheet1'] + + # Trage jeden Buchstaben des Strings in eine eigene Zeile ein + for i, char in enumerate(input_string, 1): + sheet.cell(row=2 + i - 1, column=column, value=char) + + # Speichere die Änderungen + workbook.save(file_path) + + # Schließe die Excel-Datei + workbook.close() + +def copy_header(input_sheet, output_sheet): + # Kopiere den Header manuell in das Ausgabe-Arbeitsblatt + for row in input_sheet.iter_rows(min_row=1, max_row=1, values_only=True): + output_sheet.append(row) + +def sort_excel(input_file_path, output_file_path, ): + # Öffne die Eingabe-Excel-Datei + input_workbook = openpyxl.load_workbook(input_file_path) + input_sheet = input_workbook['Sheet1'] + + # Erstelle eine neue Excel-Tabelle für die sortierten Zeilen + output_workbook = openpyxl.Workbook() + output_sheet = output_workbook.active + + # Kopiere den Header ins Ausgabe-Arbeitsblatt + copy_header(input_sheet, output_sheet) + + # Lese die Daten-Zeilen aus der Tabelle + data_rows = list(input_sheet.iter_rows(min_row=2, values_only=True)) + + # Sortiere die Daten-Zeilen nach dem Wert der angegebenen Spalte + sorted_data_rows = sorted(data_rows, key=lambda x: x[18 - 1]) # -1, da Listenindizes bei 0 beginnen + + # Schreibe die sortierten Daten-Zeilen in die neue Tabelle + for row in sorted_data_rows: + output_sheet.append(row) + + # Speichere die Änderungen in der neuen Excel-Datei + output_workbook.save(output_file_path) + + # Schließe die Excel-Dateien + input_workbook.close() + output_workbook.close() + + +#Sollten mehrere Testruns ausgewertet werden wollen, müssen die enthaltenen Funktionen umstrukturiert werden +#Aktuell wird nur der Testrun in Zeile 1 ausgewertet +#Eine Weitere Funktion, die zwei Tabellenzeilen tauscht, wäre der einfachste workaround +def evaluation(testcases, testruns): + + #liest die Ergebnisse des Testruns aus + #bei mehreren Testruns muss diese Funktion angepasst werden! + input_string = excel_row_to_string(testruns) + + #schreibt die Berechneten Ergebnisse in die Testcases-Tabelle + write_subdivided_string_to_excel(testcases, input_string) + + #liest die gemessenen und die errechneten Werte aus den Testcases + values_col1, values_col2 = read_columns(testcases) + + #berechnet aus diesen Werten die prozentuale Abweichung + deviations = calculate_deviation(values_col1, values_col2) + + #Trägt die prozentualen Abweichungen in die Testcases-Tabelle + #je nach Tabellenstruktur kann sich die 18 ändern! + write_string_to_excel(testcases, deviations, 18) + + #Gibt die eine Kopie der Testcases-Tabelle sortiert nach Genauigkeit aus + sort_excel(testcases, 'Testcases_nach_Genauigkeit.xlsx') + diff --git a/excel_processing.py b/excel_processing.py new file mode 100644 index 0000000..c6f8cb1 --- /dev/null +++ b/excel_processing.py @@ -0,0 +1,138 @@ +""" +Abhängigkeiten: +- pyramids (für den Aufbau der Bildpyramiden) +- heartrate (zur Berechnung der Herzfrequenz) +- preprocessing (für die Video-Vorverarbeitung) +- eulerian (für die Euler'sche Video-Magnifikation) +- tkinter und constants (für die GUI und Konstantenverwaltung) + +Autor: Roberto Gelsinger +Datum: 07.12.2023 +Version: Modulversion +""" + +import pyramids +import heartrate +import facedetection +import eulerian +from constants import freq_max, freq_min +import pandas as pd +from excel_update import color_cells_based_on_deviation +from excel_evaluation import evaluation + + +def process_video_for_excel(selected_video_name): + """ + Verarbeitet ein ausgewähltes Video, um die Herzfrequenz der abgebildeten Person zu ermitteln. + + Dieser Prozess umfasst die Vorverarbeitung des Videos, den Aufbau einer Laplace-Pyramide, + die Anwendung von FFT-Filterung und Euler'scher Magnifikation, und schließlich die Berechnung + der Herzfrequenz aus den Video-Daten. + + Args: + selected_video_name (str): Der Name des zu verarbeitenden Videos. + + Returns: + None: Die Funktion gibt direkt die berechnete Herzfrequenz auf der Konsole aus. + """ + + + + print("Reading + preprocessing video...") + video_frames, frame_ct, fps = facedetection.read_video("code/videos/"+selected_video_name) + + + print("Building Laplacian video pyramid...") + lap_video = pyramids.build_video_pyramid(video_frames) + + print(len(lap_video)) + + for i, video in enumerate(lap_video): + print("test") + if i == 0 or i == len(lap_video)-1: + continue + + print("Running FFT and Eulerian magnification...") + result, fft, frequencies = eulerian.fft_filter(video, freq_min, freq_max, fps) + lap_video[i] += result + + + print("Calculating heart rate...") + heart_rate = heartrate.find_heart_rate(fft, frequencies, freq_min, freq_max) + + + + + + print("Heart rate: ", heart_rate*0.7, "bpm") + return heart_rate *0.7 + + + +def process_all_videos_and_save_results(testcase_excel_file_path, testruns_excel_file_path, code_version, kommentar): + + try: + df_testruns = pd.read_excel(testruns_excel_file_path) + except FileNotFoundError: + df_testruns = pd.DataFrame() + + + df_testcases = pd.read_excel(testcase_excel_file_path) + + existing_testcases = [col for col in df_testruns.columns if col.startswith('Testcase_')] + + new_testcases = [f'Testcase_{tc}' for tc in df_testcases['Testcase'] if f'Testcase_{tc}' not in existing_testcases] + + + if df_testruns.empty: + df_testruns = pd.DataFrame(columns=['Testnummer', 'Codeversion', 'Kommentar', 'Abweichung']) + + + for col in new_testcases: + df_testruns[col] = None + + + df_testruns.to_excel(testruns_excel_file_path, index=False) + + if new_testcases: + print(f"Folgende neue Testcases wurden hinzugefügt: {new_testcases}") + else: + print("Keine neuen Testcases zum Hinzufügen gefunden.") + + next_testcase_index = len(df_testruns) + 1 + + + new_run = { + 'Testnummer': next_testcase_index, + 'Codeversion': code_version, + 'Kommentar': kommentar, + 'Abweichung': 'Wert_für_Abweichung' + } + + + for index, row in df_testcases.iterrows(): + video_name = row['VideoName'] + heart_rate = process_video_for_excel(video_name) + + + testcase_column_name = f'Testcase_{row["Testcase"]}' + new_run[testcase_column_name] = heart_rate + + try: + + df_testruns = df_testruns._append(new_run, ignore_index=True) + except TypeError: + pass + + + df_testruns.to_excel(testruns_excel_file_path, index=False) + + print("Testrun wurde verarbeitet und das Ergebnis in der Testruns-Excel-Datei gespeichert.") + + color_cells_based_on_deviation(testruns_excel_file_path, testcase_excel_file_path) + + print("Zellen gefärbt") + + evaluation(testcase_excel_file_path, testruns_excel_file_path) + + print("Testcases sortiert") \ No newline at end of file diff --git a/excel_update.py b/excel_update.py new file mode 100644 index 0000000..920a8b0 --- /dev/null +++ b/excel_update.py @@ -0,0 +1,56 @@ +import openpyxl +from openpyxl.styles import PatternFill +import pandas as pd + +def fill_cell(ws, cell, color): + fill = PatternFill(start_color=color, end_color=color, fill_type='solid') + cell.fill = fill + +def calculate_and_fill_deviation(ws, row, absolute_deviations): + if absolute_deviations: + average_deviation = sum(absolute_deviations) / len(absolute_deviations) + deviation_cell = ws.cell(row=row[0].row, column=4) # Angenommen, die 'Abweichung'-Spalte ist Spalte D + deviation_cell.value = average_deviation + # Färbe die Zelle basierend auf der durchschnittlichen Abweichung + if average_deviation < 5: + fill_color = 'FF00FF00' # Grün + elif 5 <= average_deviation < 10: + fill_color = 'FFFFFF00' # Gelb + else: + fill_color = 'FFFF0000' # Rot + fill_cell(ws, deviation_cell, fill_color) + +def color_cells_based_on_deviation(testruns_excel_file_path, testcases_excel_file_path): + wb_testruns = openpyxl.load_workbook(testruns_excel_file_path) + ws_testruns = wb_testruns.active + df_testcases = pd.read_excel(testcases_excel_file_path) + + for row in ws_testruns.iter_rows(min_row=2, max_row=ws_testruns.max_row): + deviations = [] + absolute_deviations = [] + + for cell in row[4:]: + header_cell_value = ws_testruns.cell(row=1, column=cell.column).value + if header_cell_value and "Testcase" in header_cell_value: + testcase_num = int(header_cell_value.split('_')[1]) + expected_pulse_row = df_testcases[df_testcases['Testcase'] == testcase_num] + if not expected_pulse_row.empty: + expected_pulse = expected_pulse_row.iloc[0]['Puls'] + actual_pulse = cell.value + if actual_pulse is not None and expected_pulse is not None: + relative_deviation = (actual_pulse - expected_pulse) / expected_pulse * 100 + absolute_deviation = abs(relative_deviation) + deviations.append(relative_deviation) + absolute_deviations.append(absolute_deviation) + + if absolute_deviation < 5: + fill_color = 'FF00FF00' # Grün + elif 5 <= absolute_deviation < 10: + fill_color = 'FFFFA500' if relative_deviation < 0 else 'FFFFFF00' # Orange für niedriger, Gelb für höher + else: + fill_color = 'FFC0CB' if relative_deviation < 0 else 'FFFF0000' # Rosa für niedriger, Rot für höher + fill_cell(ws_testruns, cell, fill_color) + + calculate_and_fill_deviation(ws_testruns, row, absolute_deviations) + + wb_testruns.save(testruns_excel_file_path)