diff --git a/Exercise_1/IntroML_Exercise1.pdf b/Exercise_1/IntroML_Exercise1.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..87b08b72f25046f1797da6096e16a5683817bec6
Binary files /dev/null and b/Exercise_1/IntroML_Exercise1.pdf differ
diff --git a/Exercise_1/data/contrast.jpg b/Exercise_1/data/contrast.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..c00cdc6c52715aebc1901fb18a9790cad2258645
Binary files /dev/null and b/Exercise_1/data/contrast.jpg differ
diff --git a/Exercise_1/data/hello.png b/Exercise_1/data/hello.png
new file mode 100644
index 0000000000000000000000000000000000000000..4dfa24f503bec3fe97a4f28f559b7d19d9253035
Binary files /dev/null and b/Exercise_1/data/hello.png differ
diff --git a/Exercise_1/data/hello_gaussian.png b/Exercise_1/data/hello_gaussian.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec5794618cd00c12597027bfeb23b8954f79914e
Binary files /dev/null and b/Exercise_1/data/hello_gaussian.png differ
diff --git a/Exercise_1/data/hello_poisson.png b/Exercise_1/data/hello_poisson.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce4251dc6526a392f7f98589b7a62c2ba72e2316
Binary files /dev/null and b/Exercise_1/data/hello_poisson.png differ
diff --git a/Exercise_1/data/hello_salt_pepper.png b/Exercise_1/data/hello_salt_pepper.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a8203d626643cb8832400deced2596085ab2f81
Binary files /dev/null and b/Exercise_1/data/hello_salt_pepper.png differ
diff --git a/Exercise_1/data/hello_uniform.png b/Exercise_1/data/hello_uniform.png
new file mode 100644
index 0000000000000000000000000000000000000000..08161eeecb246ed9418c17dfb51dfc05aee3ebfe
Binary files /dev/null and b/Exercise_1/data/hello_uniform.png differ
diff --git a/Exercise_1/data/kitty.png b/Exercise_1/data/kitty.png
new file mode 100644
index 0000000000000000000000000000000000000000..402070f273cc975e3c469a7c9ac1b3b970e7c6d3
Binary files /dev/null and b/Exercise_1/data/kitty.png differ
diff --git a/Exercise_1/data/runes.png b/Exercise_1/data/runes.png
new file mode 100644
index 0000000000000000000000000000000000000000..d9ffb907cf922182596e1acca244fe0ec9eff73e
Binary files /dev/null and b/Exercise_1/data/runes.png differ
diff --git a/Exercise_1/histogram_equalization.py b/Exercise_1/histogram_equalization.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5c759130b35d848e4299d168f4ab0b7c2281c3c
--- /dev/null
+++ b/Exercise_1/histogram_equalization.py
@@ -0,0 +1,76 @@
+import numpy as np
+import cv2
+import matplotlib.pyplot as plt
+
+
+def load_image(path: str) -> np.ndarray:
+    # Load the image using CV2 and return it.
+    loaded_image = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
+    if loaded_image is None:
+        raise FileNotFoundError(f"Cannot load image at {path}")
+    return loaded_image
+
+
+def compute_histogram(image: np.ndarray) -> np.ndarray:
+    # ToDo: Create a histogram for the given image (256 values).
+    # ToDo: Don't use functions like np.histogram.
+    # ToDo: It is easier if you flatten your image first.
+    histogram = np.zeros(0)
+    return histogram
+
+
+def compute_cdf(histogram: np.ndarray) -> np.ndarray:
+    # ToDo: Compute the CDF.
+    # ToDo: Don't forget to normalize it (turn it into a distribution).
+    cdf = np.zeros(0)
+    return cdf
+
+
+def equalize_image(image: np.ndarray, cdf: np.ndarray) -> np.ndarray:
+    # ToDo: Apply histogram equalization to the given image.
+    # ToDo: Hint: Flatten the image first and reshape it again in the end.
+    equalized_image = np.zeros(0)
+    return equalized_image
+
+
+def save_image(image: np.ndarray, path: str) -> None:
+    # Save the image to the given folder.
+    cv2.imwrite(path, image)
+
+
+def show_images(original_image: np.ndarray, equalized_image: np.ndarray) -> None:
+    # ToDo: Display the original and the equalized images next to each other.
+    plt.figure(figsize=(10, 5))
+    plt.subplot(1, 2, 1)
+    plt.imshow(original_image, cmap='gray')
+    plt.title('Original Image')
+    plt.axis('off')
+
+    plt.subplot(1, 2, 2)
+    plt.imshow(equalized_image, cmap='gray')
+    plt.title('Equalized Image')
+    plt.axis('off')
+
+    plt.tight_layout()
+    plt.show()
+
+
+def histogram_equalization(input_path: str, output_path: str) -> None:
+    # ToDo: Combine the different functions into one.
+    loaded_image = load_image(input_path)
+    histogram = compute_histogram(loaded_image)
+    cdf = compute_cdf(histogram)
+    equalized_image = equalize_image(loaded_image, cdf)
+    save_image(equalized_image, output_path)
+
+
+if __name__ == '__main__':
+    # Load the images and perform histogram equalization.
+    input_image_path = 'data/hello.png'
+    output_image_path = 'data/kitty.png'
+    histogram_equalization(input_image_path, output_image_path)
+
+    # Show the images next to each other.
+    original = load_image(input_image_path)
+    equalized = load_image(output_image_path)
+    show_images(original, equalized)
diff --git a/Exercise_1/noise.py b/Exercise_1/noise.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4fcd3509536007ae5194ca354a2be9a685188f8
--- /dev/null
+++ b/Exercise_1/noise.py
@@ -0,0 +1,94 @@
+import numpy as np
+import cv2
+import matplotlib.pyplot as plt
+
+
+def load_image(file_path: str) -> np.ndarray:
+    # Load the image (either gray or colour).
+    loaded_image = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
+    if loaded_image is None:
+        raise FileNotFoundError(f"Cannot load image at {file_path}")
+    return loaded_image
+
+
+def save_image(image: np.ndarray, file_path: str) -> None:
+    # Save the image.
+    cv2.imwrite(file_path, image)
+
+
+def add_gaussian_noise(image: np.ndarray, mean: float = 0.0, sigma: float = 10.0) -> np.ndarray:
+    # ToDo: Generate gaussian noise and add it to the image.
+    # ToDo: Hint: Look at the options among np.random to generate the noise.
+    # ToDo: Hint: Don't forget to clip the values.
+    return image
+
+
+def add_salt_and_pepper_noise(image: np.ndarray, salt_prob: float = 0.01, pepper_prob: float = 0.01) -> np.ndarray:
+    # ToDo: Generate random salt and pepper noise based on the provided probabilities.
+    # ToDo: Hint: Look at the options among np.random to generate the noise.
+    return image
+
+
+def add_poisson_noise(image: np.ndarray) -> np.ndarray:
+    # ToDo: Add poisson noise to the image.
+    # ToDo: Hint: Look at the options among np.random to generate the noise.
+    return image
+
+
+def add_uniform_noise(image: np.ndarray, low: float = -20.0, high: float = 20.0) -> np.ndarray:
+    # ToDo: Add uniform noise to the image, which is sampled uniformly from the available values.
+    # ToDo: Hint: Look at the options among np.random to generate the noise.
+    return image
+
+
+def display_images(original: np.ndarray, processed: np.ndarray, title: str) -> None:
+    # Transform the colour image (BGR) into an RGB image.
+    def to_rgb(image):
+        return cv2.cvtColor(image, cv2.COLOR_BGR2RGB) if image.ndim == 3 else image
+
+    adapted_original_image = to_rgb(original)
+    adapted_noise_image = to_rgb(processed)
+
+    plt.figure(figsize=(10, 5))
+    plt.subplot(1, 2, 1)
+    plt.imshow(adapted_original_image, cmap=None if adapted_original_image.ndim == 3 else 'gray')
+    plt.title('Original')
+    plt.axis('off')
+
+    plt.subplot(1, 2, 2)
+    plt.imshow(adapted_noise_image, cmap=None if adapted_noise_image.ndim == 3 else 'gray')
+    plt.title(title)
+    plt.axis('off')
+
+    plt.tight_layout()
+    plt.show()
+
+
+if __name__ == '__main__':
+    # Example usage
+    input_file = 'data/hello.png'
+    gaussian_file = 'data/hello_gaussian.png'
+    salt_pepper_file = 'data/hello_salt_pepper.png'
+    poisson_file = 'data/hello_poisson.png'
+    uniform_file = 'data/hello_uniform.png'
+
+    original_image = load_image(input_file)
+
+    # Apply noise to the images.
+    gaussian = add_gaussian_noise(original_image)
+    save_image(gaussian, gaussian_file)
+
+    salt_pepper = add_salt_and_pepper_noise(original_image)
+    save_image(salt_pepper, salt_pepper_file)
+
+    poisson = add_poisson_noise(original_image)
+    save_image(poisson, poisson_file)
+
+    uniform = add_uniform_noise(original_image)
+    save_image(uniform, uniform_file)
+
+    # Display the images side by side.
+    display_images(original_image, gaussian, 'Gaussian Noise')
+    display_images(original_image, salt_pepper, 'Salt & Pepper Noise')
+    display_images(original_image, poisson, 'Poisson Noise')
+    display_images(original_image, uniform, 'Uniform Noise')
diff --git a/Exercise_1/otsu.py b/Exercise_1/otsu.py
new file mode 100644
index 0000000000000000000000000000000000000000..86e3ce7b50f5f7b4e1be4a2da94462b686591034
--- /dev/null
+++ b/Exercise_1/otsu.py
@@ -0,0 +1,76 @@
+import numpy as np
+import cv2
+import matplotlib.pyplot as plt
+
+
+def compute_histogram(image: np.ndarray) -> np.ndarray:
+    # ToDo: Compute a grayscale histogram with 256 bins.
+    histogram = np.zeros(0)
+    return histogram
+
+
+def p_helper(prob: np.ndarray, theta: int) -> tuple[float, float]:
+    # ToDo: Compute the class probabilities p0 and p1 for the current threshold theta.
+    p0 = 0.0
+    p1 = 0.0
+    return p0, p1
+
+
+def mu_helper(prob: np.ndarray, theta: int, p0: float, p1: float) -> tuple[float, float]:
+    # ToDo: Compute the class means mu0 and mu1 for the current threshold theta.
+    mu0 = 0.0
+    mu1 = 0.0
+    return mu0, mu1
+
+
+def otsu_threshold(histogram: np.ndarray) -> int:
+    # ToDo: Compute Otsu's threshold from a histogram using p_helper and mu_helper.
+    # ToDo: Normalize the histogram to its probabilities (PDF).
+    prob = histogram.astype(np.float64)
+
+    # ToDo: Iterate over all possible thresholds, select the best one.
+    # ToDo: Hint: Skip invalid splits (p0 == 0 or p1 == 0).
+    max_variance = 0.0
+    best_threshold = 0
+
+    return best_threshold
+
+
+def otsu_binarize(image: np.ndarray) -> tuple[np.ndarray, int]:
+    # ToDo: Binarize the threshold image.
+    # ToDo: Simply combine the existing functions.
+    theta = 0
+    new_image = np.zeros(0)
+    return new_image, theta
+
+
+def custom_binarization(image: np.ndarray, theta: int) -> tuple[np.ndarray, int]:
+    # ToDo: Binarize the image with a custom value.
+    new_image = np.where(image > theta, 255, 0).astype(np.uint8)
+    return new_image, theta
+
+
+if __name__ == '__main__':
+    # Load grayscale image.
+    loaded_image = cv2.imread('data/runes.png', cv2.IMREAD_GRAYSCALE)
+    if loaded_image is None:
+        raise FileNotFoundError("Cannot load the image.")
+
+    # Compute Otsu's binarization or perform a custom binarization. Comment out one of the options.
+    # binarized_image, threshold = otsu_binarize(loaded_image)
+    binarized_image, threshold = custom_binarization(loaded_image, 180)
+
+    # Display the original and the binarized image next to each other.
+    plt.figure(figsize=(8, 4))
+    plt.subplot(1, 2, 1)
+    plt.imshow(loaded_image, cmap='gray')
+    plt.title('Original')
+    plt.axis('off')
+
+    plt.subplot(1, 2, 2)
+    plt.imshow(binarized_image, cmap='gray')
+    plt.title(f"Otsu Binarization (t={threshold})")
+    plt.axis('off')
+
+    plt.tight_layout()
+    plt.show()