@@ -0,0 +1,150 @@ | |||
import numpy as np | |||
from scipy.signal import butter, lfilter | |||
import pyramid | |||
import video | |||
import numpy as np | |||
from scipy.fft import fft, fftfreq | |||
import numpy as np | |||
from scipy import fftpack, signal | |||
from numpy.fft import fft, fftfreq | |||
# Temporal bandpass filter with Fast-Fourier Transform | |||
def fft_filter(video, freq_min, freq_max, fps): | |||
fft = fftpack.fft(video, axis=0) | |||
frequencies = fftpack.fftfreq(video.shape[0], d=1.0 / fps) | |||
bound_low = (np.abs(frequencies - freq_min)).argmin() | |||
bound_high = (np.abs(frequencies - freq_max)).argmin() | |||
# Zero out frequencies outside the desired range | |||
fft[:bound_low] = 0 | |||
fft[bound_high:] = 0 | |||
# Apply inverse FFT to get the filtered video | |||
filtered_video = np.abs(fftpack.ifft(fft, axis=0)) | |||
return filtered_video, fft, frequencies | |||
def find_breath_rate(fft, freqs, freq_min, freq_max): | |||
fft_maximums = [] | |||
# Iterate through the frequencies and accumulate FFT amplitude | |||
for i in range(fft.shape[0]): | |||
if freq_min <= freqs[i] <= freq_max: | |||
fft_maximums.append(np.abs(fft[i]).max()) | |||
else: | |||
fft_maximums.append(0) | |||
peaks, _ = signal.find_peaks(fft_maximums) | |||
if len(peaks) == 0: | |||
return 0 # No peaks found | |||
max_peak = peaks[np.argmax([fft_maximums[peak] for peak in peaks])] | |||
return freqs[max_peak] * 60 # Convert frequency (Hz) to breaths per minute (bpm) | |||
def butter_bandpass(lowcut, highcut, fs, order=1): | |||
""" | |||
Calculates the Butterworth bandpass filter coefficients. | |||
:param lowcut: Low frequency cutoff (e.g., 0.1 Hz for breath detection). | |||
:param highcut: High frequency cutoff (e.g., 0.5 Hz for breath detection). | |||
:param fs: Video frame rate (sampling frequency). | |||
:param order: Filter order (default is 1). | |||
:return: Numerator (b) and denominator (a) polynomials of the IIR filter. | |||
""" | |||
low = lowcut / (0.5 * fs) # Normalize the frequencies by Nyquist frequency | |||
high = highcut / (0.5 * fs) | |||
b, a = butter(order, [low, high], btype='band') | |||
return b, a | |||
def apply_butter(laplace_video_list, levels, alpha, cutoff=20, low=0.1, high=0.5, fps=30, width=512, height=512, linearAttenuation=True): | |||
""" | |||
Applies the Butterworth filter to the video sequence, magnifies the filtered video sequence, | |||
and attenuates spatial frequencies for breath detection. | |||
:param laplace_video_list: Laplace video pyramid. | |||
:param levels: Pyramid levels. | |||
:param alpha: Magnification factor. | |||
:param cutoff: Spatial frequencies cutoff factor. | |||
:param low: Temporal low frequency cutoff (related to the breath rate). | |||
:param high: Temporal high frequency cutoff. | |||
:param fps: Video frame rate. | |||
:param width: Video frame width. | |||
:param height: Video frame height. | |||
:param linearAttenuation: Whether to apply linear attenuation. | |||
:return: List of filtered video frames. | |||
""" | |||
print('Applying Butterworth filter...') | |||
filtered_video_list = [] | |||
b, a = butter_bandpass(low, high, fps, order=1) | |||
# Spatial wavelength (lambda) | |||
lambda1 = (width ** 2 + height ** 2) ** 0.5 | |||
delta = cutoff / 8 / (1 + alpha) | |||
for i in range(levels): # Iterate through pyramid levels | |||
current_alpha = lambda1 / (8 * delta) - 1 # Alpha calculation | |||
current_alpha /= 2 | |||
# Apply the Butterworth filter to the temporal image sequence | |||
filtered = lfilter(b, a, laplace_video_list[i], axis=0) | |||
# Ignore the lowest and highest pyramid levels | |||
if i == levels - 1 or i == 0: | |||
filtered *= 0 | |||
# Spatial frequencies attenuation | |||
if current_alpha > alpha: | |||
filtered *= alpha | |||
else: | |||
filtered *= current_alpha if linearAttenuation else 0 | |||
filtered_video_list.append(filtered) | |||
lambda1 /= 2 # Decrease lambda for the next level | |||
return filtered_video_list | |||
def start(video_frames, alpha=50, cutoff=16, low=0.1, high=0.5, linearAttenuation=True, chromAttenuation=0.4, fps=30, width=512, height=512, time_window = 5): | |||
""" | |||
Performs motion magnification on the video frames by applying Butterworth bandpass filter and saves the output video. | |||
This can be used for detecting breathing patterns. | |||
:param video_frames: Numpy array containing video frames (shape: [num_frames, height, width, channels]) | |||
:param alpha: Magnification factor (e.g., 50 for subtle motion like breathing). | |||
:param cutoff: Spatial frequencies cutoff factor. | |||
:param low: Temporal low frequency cutoff (e.g., 0.1 Hz for breaths). | |||
:param high: Temporal high frequency cutoff (e.g., 0.5 Hz for breaths). | |||
:param fps: Frames per second of the video. | |||
:param width: Width of the video frames. | |||
:param height: Height of the video frames. | |||
:return: Filtered video frames and estimated breathing rate. | |||
""" | |||
# Convert RGB to YIQ for luminance-based processing | |||
yiq_video = video.rgb2yiq(video_frames) | |||
# Determine pyramid levels for the video | |||
levels = video.calculate_pyramid_levels(width, height) | |||
# Build Laplacian pyramid for each video frame | |||
lap_video_list = pyramid.laplacian_video_pyramid(yiq_video, levels) | |||
# Apply Butterworth filter for breath frequencies (low=0.1 Hz, high=0.5 Hz) | |||
filtered_video_list = apply_butter(lap_video_list, levels, alpha, cutoff, low, high, fps, width, height, linearAttenuation) | |||
# Reconstruct the magnified video from the filtered pyramid | |||
final_video = pyramid.reconstruct(filtered_video_list, levels) | |||
filtered_video, fft_data, frequencies = fft_filter(final_video.mean(axis=(1, 2, 3)), low, high, fps) | |||
bpm = find_breath_rate(fft_data, frequencies, low, high) | |||
final_video += yiq_video | |||
final_rgb = video.yiq2rgb(final_video) | |||
final_rgb = np.clip(final_rgb, 0, 255).astype(np.uint8) | |||
return final_rgb, bpm | |||
@@ -0,0 +1,12 @@ | |||
[Parameters] | |||
alpha = 20 | |||
low = 60 | |||
high = 120 | |||
chromattenuation = 0.1 | |||
mode = some_mode | |||
[Video] | |||
width = 1280 | |||
height = 720 | |||
fps = 30.0 | |||
@@ -0,0 +1,187 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<ui version="4.0"> | |||
<class>Dialog</class> | |||
<widget class="QDialog" name="Dialog"> | |||
<property name="geometry"> | |||
<rect> | |||
<x>0</x> | |||
<y>0</y> | |||
<width>1151</width> | |||
<height>676</height> | |||
</rect> | |||
</property> | |||
<property name="windowTitle"> | |||
<string>Dialog</string> | |||
</property> | |||
<widget class="QLabel" name="video"> | |||
<property name="geometry"> | |||
<rect> | |||
<x>370</x> | |||
<y>30</y> | |||
<width>691</width> | |||
<height>521</height> | |||
</rect> | |||
</property> | |||
<property name="text"> | |||
<string/> | |||
</property> | |||
</widget> | |||
<widget class="QPushButton" name="playButton"> | |||
<property name="geometry"> | |||
<rect> | |||
<x>340</x> | |||
<y>610</y> | |||
<width>85</width> | |||
<height>27</height> | |||
</rect> | |||
</property> | |||
<property name="text"> | |||
<string>Play Video</string> | |||
</property> | |||
</widget> | |||
<widget class="QWidget" name=""> | |||
<property name="geometry"> | |||
<rect> | |||
<x>20</x> | |||
<y>30</y> | |||
<width>326</width> | |||
<height>521</height> | |||
</rect> | |||
</property> | |||
<layout class="QVBoxLayout" name="verticalLayout"> | |||
<item> | |||
<layout class="QHBoxLayout" name="horizontalLayout"> | |||
<item> | |||
<widget class="QPushButton" name="lButton"> | |||
<property name="text"> | |||
<string>Load Video</string> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QLabel" name="nameLabel"> | |||
<property name="text"> | |||
<string/> | |||
</property> | |||
</widget> | |||
</item> | |||
</layout> | |||
</item> | |||
<item> | |||
<widget class="QComboBox" name="comboBox"> | |||
<item> | |||
<property name="text"> | |||
<string>Motion Magnification</string> | |||
</property> | |||
</item> | |||
<item> | |||
<property name="text"> | |||
<string>Color Magnification </string> | |||
</property> | |||
</item> | |||
</widget> | |||
</item> | |||
<item> | |||
<layout class="QHBoxLayout" name="horizontalLayout_3"> | |||
<item> | |||
<widget class="QLabel" name="label"> | |||
<property name="text"> | |||
<string>Maginfication Factor </string> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QLineEdit" name="alpha"/> | |||
</item> | |||
</layout> | |||
</item> | |||
<item> | |||
<layout class="QHBoxLayout" name="horizontalLayout_4"> | |||
<item> | |||
<widget class="QLabel" name="label_2"> | |||
<property name="text"> | |||
<string>Spatial Frequency Cutoff</string> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QLineEdit" name="cutoff"/> | |||
</item> | |||
</layout> | |||
</item> | |||
<item> | |||
<layout class="QHBoxLayout" name="horizontalLayout_2"> | |||
<item> | |||
<widget class="QLabel" name="label_3"> | |||
<property name="text"> | |||
<string>Temporal Frequency Passband</string> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QLineEdit" name="low"> | |||
<property name="text"> | |||
<string/> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QLineEdit" name="high"> | |||
<property name="text"> | |||
<string/> | |||
</property> | |||
</widget> | |||
</item> | |||
</layout> | |||
</item> | |||
<item> | |||
<layout class="QHBoxLayout" name="horizontalLayout_5"> | |||
<item> | |||
<widget class="QLabel" name="label_4"> | |||
<property name="text"> | |||
<string>Chromatic Attenuation</string> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QDoubleSpinBox" name="chromAtt"> | |||
<property name="maximum"> | |||
<double>1.000000000000000</double> | |||
</property> | |||
<property name="singleStep"> | |||
<double>0.100000000000000</double> | |||
</property> | |||
</widget> | |||
</item> | |||
</layout> | |||
</item> | |||
<item> | |||
<widget class="QCheckBox" name="linearAtt"> | |||
<property name="text"> | |||
<string>Linear Attenuation</string> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QPushButton" name="startButton"> | |||
<property name="text"> | |||
<string>Start</string> | |||
</property> | |||
</widget> | |||
</item> | |||
<item> | |||
<widget class="QLabel" name="finished"> | |||
<property name="text"> | |||
<string/> | |||
</property> | |||
<property name="alignment"> | |||
<set>Qt::AlignCenter</set> | |||
</property> | |||
</widget> | |||
</item> | |||
</layout> | |||
</widget> | |||
</widget> | |||
<resources/> | |||
<connections/> | |||
</ui> |
@@ -0,0 +1,108 @@ | |||
import scipy.fftpack as fftpack | |||
import numpy as np | |||
import cv2 | |||
from scipy.signal import find_peaks | |||
import pyramid | |||
import video | |||
def start(vidFile, alpha, low, high, chromAttenuation, fps, width, height): | |||
''' | |||
Performs color magnification on the video by applying an ideal bandpass filter, | |||
i.e. applies a discrete fourier transform on the gaussian downsapled video and | |||
cuts off the frequencies outside the bandpass filter, magnifies the result and | |||
saves the output video. Additionally, it detects heartbeat and returns BPM. | |||
:param vidFile: Video file | |||
:param alpha: Magnification factor | |||
:param low: Low frequency cut-off | |||
:param high: High frequency cut-off | |||
:param chromAttenuation: Chrominance attenuation factor | |||
:param mode: Processing mode (unused in this example, but kept for compatibility) | |||
:param fps: Frames per second of the video | |||
:param width: Width of the video frame | |||
:param height: Height of the video frame | |||
:return: final processed video, heart rate in BPM | |||
''' | |||
# Convert from RGB to YIQ for better processing of chrominance information | |||
t = video.rgb2yiq(vidFile) | |||
levels = 4 | |||
# Build Gaussian pyramid and use the highest level | |||
gauss_video_list = pyramid.gaussian_video(t, levels) | |||
print('Apply Ideal filter') | |||
# Apply discrete Fourier transformation (real) | |||
fft = fftpack.rfft(gauss_video_list, axis=0) | |||
frequencies = fftpack.rfftfreq(fft.shape[0], d=1.0 / fps) # Sample frequencies | |||
mask = np.logical_and(frequencies > low, frequencies < high) # Logical array if values between low and high frequencies | |||
fft[~mask] = 0 # Cutoff values outside the bandpass | |||
filtered = fftpack.irfft(fft, axis=0) # Inverse Fourier transformation | |||
filtered *= alpha # Magnification | |||
# Chromatic attenuation | |||
filtered[:, :, :, 1] *= chromAttenuation | |||
filtered[:, :, :, 2] *= chromAttenuation | |||
print(chromAttenuation) | |||
# Resize last Gaussian level to the frame size | |||
filtered_video_list = np.zeros(t.shape) | |||
for i in range(t.shape[0]): | |||
f = filtered[i] | |||
filtered_video_list[i] = cv2.resize(f, (t.shape[2], t.shape[1])) | |||
final = filtered_video_list | |||
# Add to original | |||
final += t | |||
# Convert back from YIQ to RGB | |||
final = video.yiq2rgb(final) | |||
# Cutoff invalid values | |||
final[final < 0] = 0 | |||
final[final > 255] = 255 | |||
# Detect heartbeat and return BPM | |||
bpm = detect_heartbeat(filtered_video_list, fps) | |||
return final, bpm | |||
def detect_heartbeat(video_frames, fps): | |||
''' | |||
Detects heartbeat by analyzing pixel intensity variations in the filtered video over time. | |||
:param video_frames: Processed video frames (filtered and magnified) | |||
:param fps: Frames per second of the video | |||
:return: Detected heart rate in BPM (beats per minute) | |||
''' | |||
# Focus on the green channel for heart rate detection (more sensitive to blood flow changes) | |||
green_channel = video_frames[:, :, :, 1] # Extract green channel | |||
# Calculate the average intensity of the green channel for each frame | |||
avg_intensity = np.mean(green_channel, axis=(1, 2)) # Shape: (num_frames,) | |||
# Normalize intensity values | |||
avg_intensity -= np.mean(avg_intensity) | |||
avg_intensity /= np.std(avg_intensity) | |||
# Detect peaks in the intensity signal (peaks correspond to heartbeats) | |||
peaks, _ = find_peaks(avg_intensity, distance=fps // 2) # Ensure at least half a second between peaks | |||
# Calculate the time differences between peaks to compute the heart rate | |||
peak_intervals = np.diff(peaks) / fps # Convert frame intervals to seconds | |||
if len(peak_intervals) > 0: | |||
avg_heartbeat_interval = np.mean(peak_intervals) | |||
bpm = 60 / avg_heartbeat_interval # Convert to beats per minute | |||
else: | |||
bpm = 0 # No peaks detected | |||
return bpm |
@@ -0,0 +1,113 @@ | |||
#.................................. | |||
#........Visualisierung 2.......... | |||
#.................................. | |||
#...Eulerian Video Magnification... | |||
#.................................. | |||
#.. Author: Galya Pavlova.......... | |||
#.................................. | |||
import os | |||
import sys | |||
import cv2 | |||
from PyQt5.QtCore import QTimer | |||
from PyQt5.QtGui import QPixmap, QImage | |||
from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog | |||
from PyQt5.uic import loadUi | |||
import butterworth_filter | |||
import ideal_filter | |||
class App(QDialog): | |||
def __init__(self): | |||
''' | |||
Initializes and loads the GUI PyQt file | |||
''' | |||
super(App, self).__init__() | |||
self.vid = None | |||
self.name = None | |||
self.capture = None | |||
self.len = None | |||
self.l = 0 | |||
loadUi('gui.ui', self) | |||
self.startButton.clicked.connect(self.on_start_clicked) | |||
self.lButton.clicked.connect(self.open_file) | |||
self.playButton.clicked.connect(self.play_video) | |||
def play_video(self): | |||
''' | |||
A function to play a given video | |||
''' | |||
self.capture = cv2.VideoCapture(self.videoOut) | |||
frame_rate = self.capture.get(cv2.CAP_PROP_FPS) | |||
self.len = int(self.capture.get(cv2.CAP_PROP_FRAME_COUNT)) | |||
self.timer = QTimer(self) | |||
self.timer.timeout.connect(self.dispayImage) | |||
self.timer.start(frame_rate) | |||
def dispayImage(self): | |||
''' | |||
Each video frame is read and loaded | |||
''' | |||
self.l += 1 | |||
if self.l >= self.len: | |||
self.timer.stop() | |||
self.timer.deleteLater() | |||
self.l = 0 | |||
ret, img = self.capture.read() | |||
qformat = QImage.Format_RGB888 | |||
outImage = QImage(img, img.shape[1], img.shape[0], qformat) | |||
outImage = outImage.rgbSwapped() | |||
self.video.setPixmap(QPixmap.fromImage(outImage)) | |||
def open_file(self): | |||
''' | |||
Opens Files | |||
''' | |||
filename, _ = QFileDialog.getOpenFileName(self, 'Open Video File', '../', 'All Files(*)') | |||
if filename: | |||
self.vid = filename | |||
base = os.path.basename(filename) | |||
self.name = os.path.splitext(base)[0] | |||
self.nameLabel.setText(base) | |||
def on_start_clicked(self): | |||
''' | |||
Reads the input from the GUI and uses the parameters to start the program | |||
''' | |||
self.finished.clear() | |||
QApplication.instance().processEvents() | |||
alpha = float(self.alpha.text()) | |||
cutoff = float(self.cutoff.text()) | |||
low = float(self.low.text()) | |||
high = float(self.high.text()) | |||
chromAttenuation = float(self.chromAtt.text()) | |||
linearAttenuation = self.linearAtt.isChecked() | |||
mode = self.comboBox.currentIndex() | |||
if mode == 0: | |||
butterworth_filter.start(self.vid, alpha, cutoff, low, high, linearAttenuation, chromAttenuation, self.name) | |||
else: | |||
if mode == 1: | |||
ideal_filter.start(self.vid, alpha, low, high, chromAttenuation, self.name) | |||
self.finished.setText('Done!') | |||
self.videoOut = self.name+"Out.avi" | |||
if __name__ == "__main__": | |||
app = QApplication(sys.argv) | |||
window = App() | |||
window.setWindowTitle('Eulerian Video Magnification') | |||
window.show() | |||
sys.exit(app.exec_()) | |||
@@ -0,0 +1,398 @@ | |||
import sys | |||
import cv2 | |||
import numpy as np | |||
from PyQt5.QtWidgets import ( | |||
QApplication, QWidget, QFormLayout, QPushButton, QLabel, QHBoxLayout, QVBoxLayout, QComboBox | |||
) | |||
from PyQt5.QtGui import QImage, QPixmap | |||
from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal, QElapsedTimer | |||
import ideal_filter # Ensure this is properly implemented | |||
import butterworth_filter # Ensure this is properly implemented | |||
class CameraThread(QThread): | |||
frame_ready = pyqtSignal(np.ndarray) | |||
def __init__(self): | |||
super().__init__() | |||
self.is_running = True | |||
self.cap = cv2.VideoCapture(0) | |||
self.fps = self.cap.get(cv2.CAP_PROP_FPS) or 30 | |||
def run(self): | |||
while self.is_running: | |||
ret, frame = self.cap.read() | |||
if ret: | |||
self.frame_ready.emit(frame) | |||
else: | |||
print("Error: Could not read frame from camera.") | |||
self.msleep(int(1000 / self.fps)) | |||
def stop(self): | |||
self.is_running = False | |||
self.cap.release() | |||
class FilterWorker(QThread): | |||
result_ready = pyqtSignal(np.ndarray, float) | |||
def __init__(self, buffer, alpha, chromAttenuation, fps, filter_type="Ideal", width=512, height=512, time_window=5): | |||
super().__init__() | |||
self.buffer = buffer | |||
self.alpha = alpha | |||
self.chromAttenuation = chromAttenuation | |||
self.fps = fps | |||
self.width = width | |||
self.height = height | |||
self.time_window = time_window | |||
self.is_running = True | |||
self.filter_type = filter_type | |||
self.low, self.high = (1, 2.5) if filter_type == "Ideal" else (0.1, 0.5) | |||
def run(self): | |||
if self.filter_type == "Ideal": | |||
final_video, bpm = ideal_filter.start( | |||
vidFile=self.buffer, | |||
alpha=self.alpha, | |||
low=self.low, | |||
high=self.high, | |||
chromAttenuation=self.chromAttenuation, | |||
fps=self.fps, | |||
width=self.width, height=self.height | |||
) | |||
elif self.filter_type == "Butterworth": | |||
final_video, bpm = butterworth_filter.start( | |||
video_frames=self.buffer, | |||
alpha=self.alpha, | |||
low=self.low, | |||
high=self.high, | |||
chromAttenuation=self.chromAttenuation, | |||
fps=self.fps, | |||
width=self.width, | |||
height=self.height, | |||
time_window=self.time_window | |||
) | |||
if self.is_running: | |||
self.result_ready.emit(final_video, bpm) | |||
def stop(self): | |||
self.is_running = False | |||
class ParameterGUI(QWidget): | |||
def __init__(self): | |||
super().__init__() | |||
self.setWindowTitle('Video Filtering Display') | |||
self.setFixedSize(1400, 800) | |||
self.setup_ui() | |||
modelFile = "res10_300x300_ssd_iter_140000_fp16.caffemodel" | |||
configFile = "deploy.prototxt" | |||
self.face_net = cv2.dnn.readNetFromCaffe(configFile, modelFile) | |||
self.face_buffer = [] | |||
self.video_buffer = [] | |||
self.buffer_length = 0 | |||
self.elapsed_timer = QElapsedTimer() | |||
self.is_processing = False | |||
self.worker = None | |||
self.camera_thread = None | |||
def setup_ui(self): | |||
layout = QVBoxLayout() | |||
# ComboBoxes for user parameters | |||
self.alphaMenu = QComboBox(self) | |||
alpha_values = [5, 10, 15, 20, 30, 40, 50, 60] | |||
self.alphaMenu.addItems([str(value) for value in alpha_values]) | |||
self.chromAtt = QComboBox(self) | |||
chrom_values = [0.0001, 0.001,0.01,0.1,0.5] | |||
self.chromAtt.addItems([str(value) for value in chrom_values]) | |||
self.timeWindowMenu = QComboBox(self) | |||
self.timeWindowMenu.addItems(["5", "10", "15", "20"]) | |||
self.filterMenu = QComboBox(self) | |||
self.filterMenu.addItems(["Ideal", "Butterworth"]) | |||
# Form layout for parameters | |||
form_layout = QFormLayout() | |||
form_layout.addRow("Alpha:", self.alphaMenu) | |||
form_layout.addRow("ChromAttenuation:", self.chromAtt) | |||
form_layout.addRow("Filter:", self.filterMenu) | |||
form_layout.addRow("Time Window (seconds):", self.timeWindowMenu) | |||
self.submitButton = QPushButton('Start Camera') | |||
self.submitButton.clicked.connect(self.start_camera) | |||
form_layout.addRow(self.submitButton) | |||
layout.addLayout(form_layout) | |||
# Layout for displaying video | |||
video_layout = QHBoxLayout() | |||
self.liveVideoLabel = QLabel(self) | |||
self.liveVideoLabel.setFixedSize(640, 480) | |||
self.processedVideoLabel = QLabel(self) | |||
self.processedVideoLabel.setFixedSize(640, 480) | |||
video_layout.addWidget(self.liveVideoLabel, alignment=Qt.AlignCenter) | |||
video_layout.addWidget(self.processedVideoLabel, alignment=Qt.AlignCenter) | |||
layout.addLayout(video_layout) | |||
# BPM and status labels | |||
self.bpmLabel = QLabel('BPM: ', self) | |||
layout.addWidget(self.bpmLabel) | |||
self.bufferStatusLabel = QLabel('Buffer status: Waiting...', self) | |||
layout.addWidget(self.bufferStatusLabel) | |||
self.filterStatusLabel = QLabel('Filter status: Not running', self) | |||
layout.addWidget(self.filterStatusLabel) | |||
self.ParameterStatusLabel = QLabel('No parameters set', self) | |||
layout.addWidget(self.ParameterStatusLabel) | |||
self.setLayout(layout) | |||
def start_camera(self): | |||
# Stop existing camera thread if it's running | |||
if self.camera_thread is not None: | |||
self.camera_thread.stop() | |||
self.camera_thread.wait() | |||
# Stop existing worker thread if it's running | |||
if self.worker is not None: | |||
self.worker.stop() | |||
self.worker.wait() | |||
# Stop any existing timer for video display | |||
if not hasattr(self, 'timer'): | |||
self.timer = QTimer(self) | |||
if self.timer.isActive(): | |||
self.timer.stop() # Stop any running timer before starting new camera session | |||
# Reset buffers and status labels | |||
self.face_buffer.clear() | |||
self.video_buffer.clear() | |||
self.is_processing = False | |||
self.bufferStatusLabel.setText('Buffer status: Waiting...') | |||
self.filterStatusLabel.setText('Filter status: Not running') | |||
self.bpmLabel.setText('BPM: ') | |||
# Fetch parameters from UI | |||
self.alpha = int(self.alphaMenu.currentText()) | |||
self.chromAttenuation = float(self.chromAtt.currentText()) | |||
self.filter = str(self.filterMenu.currentText()) | |||
self.timeWindow = int(self.timeWindowMenu.currentText()) | |||
# Update the parameter status label | |||
self.ParameterStatusLabel.setText(f'Alpha: {self.alpha} ChromAttenuation: {self.chromAttenuation} TimeWindow: {self.timeWindow}') | |||
# Start the camera thread | |||
self.camera_thread = CameraThread() # Initialize the new camera thread | |||
self.camera_thread.frame_ready.connect(self.update_frame) | |||
self.camera_thread.start() | |||
# Set FPS and buffer length based on the camera's FPS | |||
self.fps = self.camera_thread.fps | |||
self.buffer_length = int(self.camera_thread.fps * self.timeWindow) | |||
# Start the elapsed timer to measure buffering time | |||
self.elapsed_timer.start() | |||
def update_frame(self, frame): | |||
if not self.is_processing: | |||
self.bufferStatusLabel.setText('Buffer status: Filling up') | |||
if self.filter == "Butterworth": | |||
upper_body_region, coords = self.get_upper_body(frame) | |||
if upper_body_region is not None: | |||
upper_body_resized = cv2.resize(upper_body_region, (512, 512)) | |||
self.video_buffer.append(upper_body_resized) | |||
startX, startY, endX, endY = coords | |||
cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 255, 0), 2) | |||
if self.filter == "Ideal": | |||
face_region = self.get_face(frame) | |||
if face_region is not None: | |||
face_region_resized = cv2.resize(face_region, (512, 512)) | |||
#Weißabgleich | |||
face_region_resized = cv2.GaussianBlur(face_region_resized,(25,25),0) | |||
face_region_resized = cv2.medianBlur(face_region_resized,25) | |||
self.face_buffer.append(face_region_resized) | |||
if self.elapsed_timer.elapsed() >= self.timeWindow * 1000: | |||
self.process_buffers() | |||
self.elapsed_timer.restart() | |||
# Display the live frame | |||
frame_display = self.resize_frame(frame, self.liveVideoLabel) | |||
frame_display = cv2.cvtColor(frame_display, cv2.COLOR_BGR2RGB) | |||
height, width, channel = frame_display.shape | |||
bytes_per_line = channel * width | |||
q_img = QImage(frame_display.data, width, height, bytes_per_line, QImage.Format_RGB888) | |||
self.liveVideoLabel.setPixmap(QPixmap.fromImage(q_img)) | |||
def get_face(self, frame): | |||
(h, w) = frame.shape[:2] | |||
blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0)) | |||
self.face_net.setInput(blob) | |||
detections = self.face_net.forward() | |||
for i in range(0, detections.shape[2]): | |||
confidence = detections[0, 0, i, 2] | |||
if confidence > 0.5: | |||
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) | |||
(startX, startY, endX, endY) = box.astype("int") | |||
face_region = frame[startY:endY, startX:endX] | |||
cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 255, 0), 2) | |||
return face_region | |||
return None | |||
def get_upper_body(self, frame): | |||
(h, w) = frame.shape[:2] | |||
startY = int(h / 2) | |||
endY = h | |||
startX = int(w / 4) | |||
endX = int(w * 3 / 4) | |||
cropped_frame = frame[startY:endY, startX:endX] | |||
return cropped_frame, (startX, startY, endX, endY) | |||
def process_buffers(self): | |||
if self.is_processing: | |||
return | |||
self.is_processing = True | |||
self.bufferStatusLabel.setText('Buffer status: Completed') | |||
self.filterStatusLabel.setText('Filter status: Running') | |||
time_window = int(self.timeWindowMenu.currentText()) | |||
if self.filter == "Ideal" and self.face_buffer: | |||
self.worker = FilterWorker( | |||
self.face_buffer.copy(), # Copy buffer before clearing | |||
self.alpha, | |||
self.chromAttenuation, | |||
self.camera_thread.fps, | |||
filter_type="Ideal", | |||
time_window=time_window | |||
) | |||
self.worker.result_ready.connect(self.display_filtered_video) | |||
self.worker.start() | |||
self.face_buffer.clear() | |||
elif self.filter == "Butterworth" and self.video_buffer: | |||
self.worker = FilterWorker( | |||
self.video_buffer.copy(), # Copy buffer before clearing | |||
self.alpha, | |||
self.chromAttenuation, | |||
self.camera_thread.fps, | |||
filter_type="Butterworth", | |||
time_window=time_window | |||
) | |||
self.worker.result_ready.connect(self.display_filtered_video) | |||
self.worker.start() | |||
# Clear the buffer after starting the filter worker | |||
self.video_buffer.clear() | |||
def display_filtered_video(self, final_video, bpm): | |||
self.bpmLabel.setText(f'BPM: {bpm:.2f}') | |||
self.filterStatusLabel.setText('Filter status: Displaying video') | |||
self.frame_index = 0 | |||
self.final_video = final_video | |||
# Stop the existing timer (if any) and set up a new timer for frame display | |||
if hasattr(self, 'frame_timer'): | |||
self.frame_timer.stop() | |||
self.frame_timer = QTimer(self) | |||
self.frame_timer.timeout.connect(lambda: self.show_filtered_frame(self.final_video)) | |||
self.frame_timer.start(int(1000 / self.fps)) # Display frames based on FPS | |||
print(self.fps) | |||
def show_filtered_frame(self, final_video): | |||
"""Displays each frame from the filtered video using a QTimer.""" | |||
if self.frame_index < len(final_video): | |||
frame = final_video[self.frame_index] | |||
if frame.dtype == np.float64: | |||
frame = cv2.normalize(frame, None, 0, 255, cv2.NORM_MINMAX) | |||
frame = frame.astype(np.uint8) | |||
# Resize and display the filtered frame | |||
frame_resized = self.resize_frame(frame, self.processedVideoLabel) | |||
frame_resized = cv2.cvtColor(frame_resized, cv2.COLOR_BGR2RGB) | |||
height, width, channel = frame_resized.shape | |||
bytes_per_line = channel * width | |||
q_img = QImage(frame_resized.data, width, height, bytes_per_line, QImage.Format_RGB888) | |||
self.processedVideoLabel.setPixmap(QPixmap.fromImage(q_img)) | |||
QApplication.processEvents() | |||
self.frame_index += 1 | |||
else: | |||
# Stop the filtered video display timer | |||
self.frame_timer.stop() | |||
# Restart the live video feed | |||
if hasattr(self, 'timer'): | |||
self.timer.start(int(1000 / self.fps)) # Restart the live feed timer | |||
else: | |||
print("Error: Timer for live video is not initialized.") | |||
self.filterStatusLabel.setText('Filter status: Completed') | |||
self.is_processing = False | |||
self.bufferStatusLabel.setText('Buffer status: Blocked') | |||
def resize_frame(self, frame, label): | |||
size = label.size() | |||
return cv2.resize(frame, (size.width(), size.height())) | |||
def closeEvent(self, event): | |||
if self.camera_thread: | |||
self.camera_thread.stop() | |||
if self.worker: | |||
self.worker.stop() | |||
if self.frame_timer: | |||
self.frame_timer.stop() | |||
event.accept() | |||
if __name__ == '__main__': | |||
app = QApplication(sys.argv) | |||
window = ParameterGUI() | |||
window.show() | |||
sys.exit(app.exec_()) | |||
for i in range(frame_count): | |||
ret, frame = cap.read() | |||
if not ret: | |||
print(f"Frame {i+1} konnte nicht gelesen werden.") | |||
break | |||
frames[i] = frame | |||
# Optional: Vorschau anzeigen | |||
cv2.imshow("Aufnahme", frame) | |||
if cv2.waitKey(1) & 0xFF == ord('q'): # Beenden durch Drücken von 'q' | |||
break | |||
# Kamera und Fenster freigeben | |||
cap.release() | |||
cv2.destroyAllWindows() |
@@ -0,0 +1,76 @@ | |||
import configparser | |||
# Erstelle eine Parser-Klasse für INI-Dateien | |||
class ParameterParser: | |||
def __init__(self, config_file='config.ini'): | |||
self.config_file = config_file | |||
self.config = configparser.ConfigParser() | |||
self.config.read(self.config_file) | |||
# Ensure 'Parameters' and 'Video' sections exist | |||
if 'Parameters' not in self.config: | |||
self.config['Parameters'] = {} | |||
if 'Video' not in self.config: | |||
self.config['Video'] = {} | |||
def get_parameters(self): | |||
# Lese die Parameter aus der Konfigurationsdatei | |||
try: | |||
alpha = self.config.getfloat('Parameters', 'alpha') | |||
cutoff = self.config.getfloat('Parameters', 'cutoff') | |||
low = self.config.getfloat('Parameters', 'low') | |||
high = self.config.getfloat('Parameters', 'high') | |||
chromAttenuation = self.config.getfloat('Parameters', 'chromAttenuation') | |||
mode = self.config.getint('Parameters', 'mode') | |||
# Read video parameters | |||
width = self.config.getint('Video', 'width', fallback=1280) | |||
height = self.config.getint('Video', 'height', fallback=720) | |||
fps = self.config.getint('Video', 'fps', fallback=30) | |||
return { | |||
"alpha": alpha, | |||
"cutoff": cutoff, | |||
"low": low, | |||
"high": high, | |||
"chromAttenuation": chromAttenuation, | |||
"mode": mode, | |||
"width": width, | |||
"height": height, | |||
"fps": fps | |||
} | |||
except Exception as e: | |||
print(f"Error reading config: {e}") | |||
return None | |||
def set_parameters(self, alpha, cutoff, low, high, chromAttenuation, mode, width, height, fps): | |||
# Schreibe die Parameter in die Konfigurationsdatei | |||
self.config['Parameters'] = { | |||
'alpha': str(alpha), | |||
'cutoff': str(cutoff), | |||
'low': str(low), | |||
'high': str(high), | |||
'chromAttenuation': str(chromAttenuation), | |||
'mode': str(mode) | |||
} | |||
# Save video parameters | |||
self.config['Video'] = { | |||
'width': str(width), | |||
'height': str(height), | |||
'fps': str(fps) | |||
} | |||
with open(self.config_file, 'w') as configfile: | |||
self.config.write(configfile) | |||
# Beispiel: Erstellen und Speichern von Parametern | |||
if __name__ == "__main__": | |||
parser = ParameterParser() | |||
# Beispielwerte speichern | |||
parser.set_parameters(0.5, 1.0, 0.2, 1.5, 0.8, 1, 1280, 720, 30) | |||
# Parameter auslesen | |||
parameters = parser.get_parameters() | |||
print(parameters) |
@@ -0,0 +1,108 @@ | |||
#.................................. | |||
#........Visualisierung 2.......... | |||
#.................................. | |||
#...Eulerian Video Magnification... | |||
#.................................. | |||
#.. Author: Galya Pavlova.......... | |||
#.................................. | |||
import cv2 | |||
import numpy as np | |||
def create_gaussian_pyramid(image, levels): | |||
''' | |||
Creates a Gaussian pyramid for each image. | |||
:param image: An image, i.e video frame | |||
:param levels: The Gaussian pyramid level | |||
:return: Returns a pyramid of nr. levels images | |||
''' | |||
gauss = image.copy() | |||
gauss_pyr = [gauss] | |||
for level in range(1, levels): | |||
gauss = cv2.pyrDown(gauss) | |||
gauss_pyr.append(gauss) | |||
return gauss_pyr | |||
def gaussian_video(video_tensor, levels): | |||
''' | |||
For a given video sequence the function creates a video with | |||
the highest (specified by levels) Gaussian pyramid level | |||
:param video_tensor: Video sequence | |||
:param levels: Specifies the Gaussian pyramid levels | |||
:return: a video sequence where each frame is the downsampled of the original frame | |||
''' | |||
for i in range(0, video_tensor.shape[0]): | |||
frame = video_tensor[i] | |||
pyr = create_gaussian_pyramid(frame, levels) | |||
gaussian_frame = pyr[-1] # use only highest gaussian level | |||
if i == 0: # initialize one time | |||
vid_data = np.zeros((video_tensor.shape[0], gaussian_frame.shape[0], gaussian_frame.shape[1], 3)) | |||
vid_data[i] = gaussian_frame | |||
return vid_data | |||
def create_laplacian_pyramid(image, levels): | |||
''' | |||
Builds a Laplace pyramid for an image, i.e. video frame | |||
:param image: Image, i.e. single video frame | |||
:param levels: Specifies the Laplace pyramid levels | |||
:return: Returns a pyramid of nr. levels images | |||
''' | |||
gauss_pyramid = create_gaussian_pyramid(image, levels) | |||
laplace_pyramid = [] | |||
for i in range(levels-1): | |||
size = (gauss_pyramid[i].shape[1], gauss_pyramid[i].shape[0]) # reshape | |||
laplace_pyramid.append(gauss_pyramid[i]-cv2.pyrUp(gauss_pyramid[i+1], dstsize=size)) | |||
laplace_pyramid.append(gauss_pyramid[-1]) # add last gauss pyramid level | |||
return laplace_pyramid | |||
def laplacian_video_pyramid(video_stack, levels): | |||
''' | |||
Creates a Laplacian pyramid for the whole video sequence | |||
:param video_stack: Video sequence | |||
:param levels: Specifies the Laplace pyramid levels | |||
:return: A two-dimensional array where the first index is used for the pyramid levels | |||
and the second for each video frame | |||
''' | |||
print('Build laplace pyramid') | |||
# "2 dimensional" array - first index for pyramid level, second for frames | |||
laplace_video_pyramid = [[0 for x in range(video_stack.shape[0])] for x in range(levels)] | |||
for i in range(video_stack.shape[0]): | |||
frame = video_stack[i] | |||
pyr = create_laplacian_pyramid(frame, levels) | |||
for n in range(levels): | |||
laplace_video_pyramid[n][i] = pyr[n] | |||
return laplace_video_pyramid | |||
def reconstruct(filtered_video, levels): | |||
''' | |||
Reconstructs a video sequence from the filtered Laplace video pyramid | |||
:param filtered_video: 2 dimensional video sequence - 1st. index pyramid levels, 2nd. - video frames | |||
:param levels: pyramid levels | |||
:return: video sequence | |||
''' | |||
print('Reconstruct video') | |||
final = np.empty(filtered_video[0].shape) | |||
for i in range(filtered_video[0].shape[0]): # iterate through frames | |||
up = filtered_video[-1][i] # highest level | |||
for k in range(levels-1, 0, -1): # going down to lowest level | |||
size = (filtered_video[k-1][i].shape[1], filtered_video[k-1][i].shape[0]) # reshape | |||
up = cv2.pyrUp(up, dstsize=size) + filtered_video[k-1][i] | |||
final[i] = up | |||
return final |
@@ -0,0 +1,43 @@ | |||
import numpy as np | |||
import cv2 | |||
import platform | |||
def calculate_pyramid_levels(vidWidth, vidHeight): | |||
''' | |||
Calculates the maximal pyramid levels for the Laplacian pyramid | |||
:param vidWidth: video frames' width | |||
:param vidHeight: video frames' height | |||
''' | |||
if vidWidth < vidHeight: | |||
levels = int(np.log2(vidWidth)) | |||
else: | |||
levels = int(np.log2(vidHeight)) | |||
return levels | |||
def rgb2yiq(video): | |||
''' | |||
Converts the video color from RGB to YIQ (NTSC) | |||
:param video: RGB video sequence | |||
:return: YIQ-color video sequence | |||
''' | |||
yiq_from_rgb = np.array([[0.299, 0.587, 0.114], | |||
[0.596, -0.274, -0.322], | |||
[0.211, -0.523, 0.312]]) | |||
t = np.dot(video, yiq_from_rgb.T) | |||
return t | |||
def yiq2rgb(video): | |||
''' | |||
Converts the video color from YIQ (NTSC) to RGB | |||
:param video: YIQ-color video sequence | |||
:return: RGB video sequence | |||
''' | |||
rgb_from_yiq = np.array([[1, 0.956, 0.621], | |||
[1, -0.272, -0.647], | |||
[1, -1.106, 1.703]]) | |||
t = np.dot(video, rgb_from_yiq.T) | |||
return t |