diff --git a/conditions/sem02/lesson03/tasks.md b/conditions/sem02/lesson03/tasks.md index b40c1713..31818efb 100644 --- a/conditions/sem02/lesson03/tasks.md +++ b/conditions/sem02/lesson03/tasks.md @@ -21,7 +21,7 @@ def sum_arrays_naive( ] ``` -Допишите код векторизованной функции в файле [task1](../solutions/sem02/lesson03/task1.py). +Допишите код векторизованной функции в файле [task1](../../../solutions/sem02/lesson03/task1.py). **Входные данные:** - `lhs` - одномерный массив чисел с плавающей точкой; @@ -42,7 +42,7 @@ def sum_arrays_naive( def compute_poly_naive(abscissa: list[float]) -> list[float]: return [3 * (x ** 2) + 2 * x + 1 for x in abscissa] ``` -Допишите код векторизованной функции в файле [task1](../solutions/sem02/lesson03/task1.py). +Допишите код векторизованной функции в файле [task1](../../../solutions/sem02/lesson03/task1.py). **Входные данные:** - `abscissa` - одномерный массив чисел с плавающей точкой - область определения для вычисления полинома; @@ -54,7 +54,7 @@ def compute_poly_naive(abscissa: list[float]) -> list[float]: Векторизуйте код функции `get_mutual_l2_distances_naive`. -Допишите код векторизованной функции в файле [task1](../solutions/sem02/lesson03/task1.py). +Допишите код векторизованной функции в файле [task1](../../../solutions/sem02/lesson03/task1.py). **Python функция**: ```python @@ -92,7 +92,7 @@ def get_mutual_l2_distances_naive( ### Перевод из декартовых координат в сферические -Допишите код функции `convert_from_sphere` в файле [task2](../solutions/sem02/lesson03/task2.py). +Допишите код функции `convert_from_sphere` в файле [task2](../../../solutions/sem02/lesson03/task2.py). **Входные данные**: - `abscissa` - np.ndarray, абсциссы точек; @@ -112,7 +112,7 @@ def get_mutual_l2_distances_naive( ### Перевод из сферических координат в декартовы -Допишите код функции `convert_to_sphere` в файле [task2](../solutions/sem02/lesson03/task2.py). +Допишите код функции `convert_to_sphere` в файле [task2](../../../solutions/sem02/lesson03/task2.py). **Входные данные**: - `distances` - np.ndarray, массив расстояний; @@ -129,7 +129,7 @@ def get_mutual_l2_distances_naive( На вход подается одномерный массив чисел с плавающей точкой - значения некоторой функции на определенном отрезке. Ваша задача - вычислить индексы элементов, соответствующие точкам экстремума данной функции. -Допишите код в файле [task3](../solutions/sem02/lesson03/task3.py). +Допишите код в файле [task3](../../../solutions/sem02/lesson03/task3.py). **Входные данные**: - `ordinates` - np.ndarray числе с плавающей точкой, значения некоторой функции на определенном отрезке; diff --git a/conditions/sem02/lesson04/images/compare.png b/conditions/sem02/lesson04/images/compare.png new file mode 100644 index 00000000..9fed9b26 Binary files /dev/null and b/conditions/sem02/lesson04/images/compare.png differ diff --git a/conditions/sem02/lesson04/images/conv.png b/conditions/sem02/lesson04/images/conv.png new file mode 100644 index 00000000..0b62b034 Binary files /dev/null and b/conditions/sem02/lesson04/images/conv.png differ diff --git a/conditions/sem02/lesson04/images/padding.jpg b/conditions/sem02/lesson04/images/padding.jpg new file mode 100644 index 00000000..65a0f4ec Binary files /dev/null and b/conditions/sem02/lesson04/images/padding.jpg differ diff --git a/conditions/sem02/lesson04/tasks.md b/conditions/sem02/lesson04/tasks.md new file mode 100644 index 00000000..b976407a --- /dev/null +++ b/conditions/sem02/lesson04/tasks.md @@ -0,0 +1,79 @@ +## Задача 1. Нечеткий парень + +В этом задании перед вами стоит задача реализовать фильтр размытия, аналогичный фильтрам размытия из различных редакторов фото. Но прежде чем переходить к реализации самого фильтра размытия, необходимо выполнить подготовительные шаги. + +### Часть 1. Паддингтон + +Слово паддинг (англ. *padding*) буквально можно перевести, как отступ. В контексте текущей задачи паддингом мы будем называть рамку вокруг изображения, шириной в заданное число пикселей, заполненную нулями. На картинке ниже приведен пример добавления паддинга шириной в 1 пиксель к входному изображению. + +![padding](./images/padding.jpg) + +Ваша задача - реализовать функцию добавления паддинга. + +Допишите код функции `pad_image` в файле [task1](../../../solutions/sem02/lesson04/task1.py). + +**Входные данные**: +- `image` - двумерный или трехмерный массив - черно-белое или RGB изображение. Элементы массива - восьмибитные целые беззнаковые числа. +- `pad_size` - натуральное число, ширина паддинга. + +**Выходные данные**: +- Исходное изображение с добавленным паддингом. Т.е. `image`, заключенное в рамку из 0 шириной в `pad_size` пикселей. + +*Сторонние эффекты*: +- Если значение `pad_size` меньше единицы, необходимо возбудить исключение `ValueError`. + +**ВАЖНО**: в это задании запрещено использовать функцию `np.pad`. Решения с использованием `np.pad` будут оценены в 0 баллов! + +### Часть 2. Размытие + +Теперь мы готовы реализовывать фильтр размытия. Размытие изображения работает достаточно просто: + +- Сначала задается размер окна размытия - $l_w$, которое можно интерпретировать, как степень размытия. Чем больше окно размытия, тем сильнее результирующая картинка будет размыта. Обычно в качестве размеров окна размытия используются нечетные целые числа. +- Следующий шаг - это применение паддинга к входному изображению размеров $N \times M$, причем ширина паддинга соответствует следующему выражению: $\lfloor\frac{l_w}{2}\rfloor$. +- Затем, по всему изображению с паддингом запускается обход скользящим окном размеров $l_w \times l_w$, причем центр окна всегда находится в пикселях, соответствующих пикселям исходного изображения. Т.е. центр окна размытия проходит пиксели из области $[l_w, l_w + N] \times [l_w, l_w + M]$ +- В каждом положении окна размытия вычисляется среднее значений пикселей, попавших в окно. Результат записывается в новый массив, тех же размеров, что и исходное изображение. + +![blur](./images/conv.png) + +Ваша задача - реализовать функцию для размытия изображений. + +Допишите код функции `blur_image` в файле [task1](../../../solutions/sem02/lesson04/task1.py). + +**Входные данные**: +- `image` - двумерный или трехмерный массив - черно-белое или RGB изображение. Элементы массива - восьмибитные целые беззнаковые числа. +- `kernel_size` - натуральное нечетное число, размер окна размытия. + +**Выходные данные**: +- Размытое изображение. + +*Сторонние эффекты*: +- Если `kernel_size` четное число или `kernel_size` меньше 1, необходимо возбудить исключение `ValueError`. + +Ожидаемый результат: + +![cirlce](./images/compare.png) + +## Задача 2. Не вижу разницы + +Представим, что вы занимаетесь разработкой некоторого алгоритма дорисовки черно-белых изображений. Для дорисовки используется некоторая модель машинного обучения. Вызов модели занимает продолжительное время, и в некоторых случаях этот вызов не оправдан. Например, дорисовка с помощью модели машинного обучения не оправдана, когда изображение является однотонным (очень много больших областей одного и того же цвета). В этом случае можно было бы использовать цвет однотонных областей для дорисовки требуемых частей изображения. + +Для реализации такого подхода необходимо разработать алгоритм, который позволял бы определить самый распространенный цвет на изображении. Однако есть нюанс. Некоторые цвета черно-белого изображения плохо различимы человеком, и их необходимо рассматривать как один и тот же цвет. Определить плохо различимые цвета можно с помощью критерия `|image[i][j] - image[k][l]| < treshold`. Т.е. если значение разности яркости двух пикселей не превышает заранее заданного порога, то эти пиксели считаются пикселями одного цвета. + +Необходимо реализовать функцию для определения самого распространенного цвета черно-белого изображения с учетом оговоренных особенностей восприятия цвета. Также необходимо рассчитать процент пикселей изображения, окрашенных в самый распространенный цвет, чтобы понимать, возможна ли тривиальная дорисовка или нет. + +Допишите код функции pad_image в файле [task2](../../../solutions/sem02/lesson04/task2.py). + +**Входные данные**: +- `image` - двумерный массив - черно-белое изображение. Элементы массива - восьмибитные целые беззнаковые числа. +- `threshold` - натуральное число, порог для выявления неразличимых цветов. + +**Выходные данные**: +- Кортеж. Первый элемент кортежа - восьмибитное целое беззнаковое число, самый распространенный цвет. Второй элемент кортежа - процент пикселей изображения, окрашенных в самый распространенный цвет. + +*Сторонние эффекты*: +- Если значение `threshold` меньше единицы, необходимо возбудить исключение `ValueError`. + + + + + diff --git a/tests/test_lesson03_tasks.py b/deprecated_tests/sem02/tests/test_lesson03_tasks.py similarity index 100% rename from tests/test_lesson03_tasks.py rename to deprecated_tests/sem02/tests/test_lesson03_tasks.py diff --git a/solutions/sem02/lesson03/__init__.py b/solutions/sem02/lesson03/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/solutions/sem02/lesson04/__init__.py b/solutions/sem02/lesson04/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/solutions/sem02/lesson04/images/circle.jpg b/solutions/sem02/lesson04/images/circle.jpg new file mode 100644 index 00000000..6e1e9954 Binary files /dev/null and b/solutions/sem02/lesson04/images/circle.jpg differ diff --git a/solutions/sem02/lesson04/requirements.txt b/solutions/sem02/lesson04/requirements.txt new file mode 100644 index 00000000..039f1d50 --- /dev/null +++ b/solutions/sem02/lesson04/requirements.txt @@ -0,0 +1,3 @@ +matplotlib==3.8.0 +numpy==1.26.1 +opencv-python-headless==4.9.0.80 diff --git a/solutions/sem02/lesson04/task1.py b/solutions/sem02/lesson04/task1.py new file mode 100644 index 00000000..1b5526c1 --- /dev/null +++ b/solutions/sem02/lesson04/task1.py @@ -0,0 +1,27 @@ +import numpy as np + + +def pad_image(image: np.ndarray, pad_size: int) -> np.ndarray: + # ваш код + return image + + +def blur_image( + image: np.ndarray, + kernel_size: int, +) -> np.ndarray: + # ваш код + return image + + +if __name__ == "__main__": + import os + from pathlib import Path + + from utils.utils import compare_images, get_image + + current_directory = Path(__file__).resolve().parent + image = get_image(os.path.join(current_directory, "images", "circle.jpg")) + image_blured = blur_image(image, kernel_size=21) + + compare_images(image, image_blured) diff --git a/solutions/sem02/lesson04/task2.py b/solutions/sem02/lesson04/task2.py new file mode 100644 index 00000000..be9a2288 --- /dev/null +++ b/solutions/sem02/lesson04/task2.py @@ -0,0 +1,10 @@ +import numpy as np + + +def get_dominant_color_info( + image: np.ndarray[np.uint8], + threshold: int = 5, +) -> tuple[np.uint8, float]: + # ваш код + + return 0, 0 diff --git a/solutions/sem02/lesson04/utils/utils.py b/solutions/sem02/lesson04/utils/utils.py new file mode 100644 index 00000000..8a06872e --- /dev/null +++ b/solutions/sem02/lesson04/utils/utils.py @@ -0,0 +1,22 @@ +import cv2 as cv +import matplotlib.pyplot as plt +import numpy as np + + +def get_image(path_to_image: str) -> np.ndarray: + image = cv.imread(path_to_image) + return cv.cvtColor(image, code=cv.COLOR_BGR2RGB) + + +def compare_images(image1: np.ndarray, image2: np.ndarray) -> None: + _, (axis1, axis2) = plt.subplots(1, 2, figsize=(16, 8)) + axis1: plt.Axes = axis1 + axis2: plt.Axes = axis2 + + axis1.imshow(image1) + axis2.imshow(image2) + + axis1.axis("off") + axis2.axis("off") + + plt.show() diff --git a/tests/test_data/lesson04/test_task11_data_res.npy b/tests/test_data/lesson04/test_task11_data_res.npy new file mode 100644 index 00000000..3e88be88 Binary files /dev/null and b/tests/test_data/lesson04/test_task11_data_res.npy differ diff --git a/tests/test_data/lesson04/test_task12_data_res.npy b/tests/test_data/lesson04/test_task12_data_res.npy new file mode 100644 index 00000000..394c3079 Binary files /dev/null and b/tests/test_data/lesson04/test_task12_data_res.npy differ diff --git a/tests/test_data/lesson04/test_task2_data1.npy b/tests/test_data/lesson04/test_task2_data1.npy new file mode 100644 index 00000000..8adda8ab Binary files /dev/null and b/tests/test_data/lesson04/test_task2_data1.npy differ diff --git a/tests/test_lesson04_tasks.py b/tests/test_lesson04_tasks.py new file mode 100644 index 00000000..05b170be --- /dev/null +++ b/tests/test_lesson04_tasks.py @@ -0,0 +1,298 @@ +import os + +import numpy as np +import pytest + +from solutions.sem02.lesson04.task1 import blur_image, pad_image +from solutions.sem02.lesson04.task2 import get_dominant_color_info + +DATA_PATH = os.path.join("tests", "test_data", "lesson04") + + +class TestTask1: + @pytest.mark.parametrize( + "image, pad_size, expected", + [ + pytest.param( + np.array([[5]], dtype=np.float32), + 1, + np.array( + [ + [0, 0, 0], + [0, 5, 0], + [0, 0, 0], + ], + dtype=np.float32, + ), + id="2d_single_pixel_pad_1", + ), + pytest.param( + np.array([[5]], dtype=np.float32), + 2, + np.array( + [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 5, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + dtype=np.float32, + ), + id="2d_single_pixel_pad_2", + ), + pytest.param( + np.array([[1, 2], [3, 4]], dtype=np.uint8), + 1, + np.array([[0, 0, 0, 0], [0, 1, 2, 0], [0, 3, 4, 0], [0, 0, 0, 0]], dtype=np.uint8), + id="2d_pad_1", + ), + pytest.param( + np.array([[1, 2], [3, 4]], dtype=np.uint8), + 2, + np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 1, 2, 0, 0], + [0, 0, 3, 4, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ), + id="2d_pad_2", + ), + pytest.param( + np.array([[1, 2]], dtype=np.uint8), + 2, + np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 1, 2, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ), + id="2d_row_pad_2", + ), + pytest.param( + np.array([[1], [2]], dtype=np.uint8), + 2, + np.array( + [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 2, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ), + id="2d_col_pad_2", + ), + pytest.param( + np.array([[[10, 20, 30], [40, 50, 60]]], dtype=np.uint8), + 1, + np.array( + [ + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [10, 20, 30], [40, 50, 60], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + dtype=np.uint8, + ), + id="3d_rgb_pad_1", + ), + pytest.param( + np.array([[[10, 20, 30], [40, 50, 60]]], dtype=np.uint8), + 2, + np.array( + [ + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [10, 20, 30], [40, 50, 60], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + dtype=np.uint8, + ), + id="3d_rgb_pad_2", + ), + pytest.param( + np.zeros((2, 3), dtype=int), 1, np.zeros((4, 5), dtype=int), id="2d_zeros_pad_1" + ), + pytest.param( + np.arange(4095 * 4095).reshape(4095, 4095) % 256, + 100, + np.load(os.path.join(DATA_PATH, "test_task11_data_res.npy")), + id="large_data", + ), + ], + ) + def test_pad_image(self, image, pad_size, expected): + result = pad_image(image.astype(np.uint8), pad_size) + + assert np.array_equal(result, expected) + assert result.dtype == np.uint8 + + def test_pad_size_validate(self): + image = np.array([[1, 2], [3, 4]]) + + with pytest.raises(ValueError): + pad_image(image, 0) + + with pytest.raises(ValueError): + pad_image(image, -1) + + @pytest.mark.parametrize( + "image, kernel_size, expected", + [ + pytest.param( + np.array([[10, 20], [30, 40]], dtype=np.uint8), + 1, + np.array([[10, 20], [30, 40]], dtype=np.uint8), + id="kernel_size_1", + ), + pytest.param( + np.array([[100, 100], [100, 100]], dtype=np.uint8), + 3, + np.array([[44, 44], [44, 44]], dtype=np.uint8), + id="2d_constant_blur_3", + ), + pytest.param( + np.array([[0, 0, 0], [0, 255, 0], [0, 0, 0]], dtype=np.uint8), + 3, + np.array([[28, 28, 28], [28, 28, 28], [28, 28, 28]], dtype=np.uint8), + id="2d_single_pixel_blur_3", + ), + pytest.param( + np.array([[100]], dtype=np.uint8), + 3, + np.array([[11]], dtype=np.uint8), + id="2d_1x1_blur_3", + ), + pytest.param( + np.array( + [[[100, 68, 50], [179, 30, 245]], [[40, 90, 235], [38, 70, 210]]], + dtype=np.uint8, + ), + 3, + np.array( + [[[39, 28, 82], [39, 28, 82]], [[39, 28, 82], [39, 28, 82]]], dtype=np.uint8 + ), + id="3d_blur_3", + ), + pytest.param( + np.arange(4095 * 4095 * 2).reshape(4095, 4095 * 2) % 256, + 5, + np.load(os.path.join(DATA_PATH, "test_task12_data_res.npy")), + id="large_data", + ), + ], + ) + def test_blur_image(self, image, kernel_size, expected): + result = blur_image(image.astype(np.uint8), kernel_size) + + assert result.shape == expected.shape + assert result.dtype == expected.dtype + assert np.all(np.abs(result.astype(np.int16) - expected.astype(np.int16)) < 2) + + def test_blur_image_validate(self): + image = np.array([[1, 2], [3, 4]], dtype=np.uint8) + + with pytest.raises(ValueError): + blur_image(image, 2) + with pytest.raises(ValueError): + blur_image(image, 4) + + with pytest.raises(ValueError): + blur_image(image, 0) + with pytest.raises(ValueError): + blur_image(image, -1) + + +class TestTask2: + @pytest.mark.parametrize( + "image, threshold, expected_color, expected_ratio", + [ + pytest.param( + np.array([[100, 100], [100, 100]], dtype=np.uint8), + 5, + [100], + 1.0, + id="uniform_image", + ), + pytest.param( + np.array([[50, 52], [51, 100]], dtype=np.uint8), + 3, + [50, 51, 52], + 0.75, + id="close_colors_merged", + ), + pytest.param( + np.array([[0, 0], [10, 11]], dtype=np.uint8), + 10, + [0], + 0.5, + id="distant_colors_not_merged", + ), + pytest.param( + np.array([[21, 20, 10], [20, 30, 20]], dtype=np.uint8), + 1, + [20], + 0.5, + id="threshold_1_only_exact", + ), + pytest.param( + np.array([[0, 100, 200]], dtype=np.uint8), + 10, + [0, 100, 200], + 1 / 3, + id="all_colors_different", + ), + pytest.param( + np.array([[100, 100, 100, 102, 104, 106, 108]], dtype=np.uint8), + 3, + [102], + 5 / 7, + id="chain_within_threshold", + ), + pytest.param( + np.array([[255, 0, 0]], dtype=np.uint8), 20, [0], 2 / 3, id="marginal_values_1" + ), + pytest.param( + np.array([[0, 255, 0]], dtype=np.uint8), 20, [0], 2 / 3, id="marginal_values_2" + ), + pytest.param( + np.array([[0, 0, 255]], dtype=np.uint8), 20, [0], 2 / 3, id="marginal_values_3" + ), + pytest.param( + np.load(os.path.join(DATA_PATH, "test_task2_data1.npy")), + 10, + list(range(100 - 11, 100 + 11)), + 0.166868359375, + id="large_data", + ), + ], + ) + def test_get_dominant_color_info(self, image, threshold, expected_color, expected_ratio): + color, ratio_percent = get_dominant_color_info(image.astype(np.uint8), threshold) + + assert color in expected_color + assert (abs(ratio_percent - expected_ratio * 100) < 1e-6) or ( + abs(ratio_percent - expected_ratio) < 1e-6 + ) + assert isinstance(color, np.uint8) + + def test_get_dominant_color_info_validate(self): + image = np.array([[0, 255]], dtype=np.uint8) + + with pytest.raises(ValueError, match="threshold must be positive"): + get_dominant_color_info(image, 0) + + with pytest.raises(ValueError, match="threshold must be positive"): + get_dominant_color_info(image, -1)