diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..4cba627 --- /dev/null +++ b/.clang-format @@ -0,0 +1,23 @@ +BasedOnStyle: LLVM +Language: Cpp + +IndentWidth: 2 +ContinuationIndentWidth: 2 +TabWidth: 2 +UseTab: Never + +ColumnLimit: 80 +IndentPPDirectives: None +AlignEscapedNewlines: Left +AlignConsecutiveMacros: None + +BinPackArguments: false +BinPackParameters: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false + +BreakBeforeBraces: Attach +PointerAlignment: Right +SortIncludes: Never +ReflowComments: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a321ee9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.cache +/build +/benchmarks/processed_outputs +/benchmarks/pipeline_outputs \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..91c0a90 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,140 @@ +cmake_minimum_required(VERSION 3.20) + +project(ImageConversation LANGUAGES C) + +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(CTest) +include(FetchContent) +find_package(Threads REQUIRED) +find_package(OpenMP REQUIRED) + +option(IMAGE_CONVERSATION_ENABLE_OPENCV + "Fetch the legacy OpenCV C API target" + OFF) + +file(GLOB_RECURSE FILTER_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/filters/*.c" +) + +file(GLOB_RECURSE CONVOLUTION_RUNTIME_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/convolution/*.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/parallel_convolution/*.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/pipeline/*.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/sequentially_convolution/*.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/runtime/*.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/image_helpers/*.c" +) + +set(BUILD_TESTS OFF CACHE BOOL "") +set(BUILD_PERF_TESTS OFF CACHE BOOL "") +set(BUILD_EXAMPLES OFF CACHE BOOL "") +set(BUILD_DOCS OFF CACHE BOOL "") +set(BUILD_opencv_apps OFF CACHE BOOL "") + +set(BUILD_opencv_core ON CACHE BOOL "") +set(BUILD_opencv_imgproc ON CACHE BOOL "") +set(BUILD_opencv_highgui ON CACHE BOOL "") + +set(BUILD_opencv_ts OFF CACHE BOOL "") +set(BUILD_opencv_calib3d OFF CACHE BOOL "") +set(BUILD_opencv_contrib OFF CACHE BOOL "") +set(BUILD_opencv_features2d OFF CACHE BOOL "") +set(BUILD_opencv_flann OFF CACHE BOOL "") +set(BUILD_opencv_gpu OFF CACHE BOOL "") +set(BUILD_opencv_legacy OFF CACHE BOOL "") +set(BUILD_opencv_ml OFF CACHE BOOL "") +set(BUILD_opencv_nonfree OFF CACHE BOOL "") +set(BUILD_opencv_objdetect OFF CACHE BOOL "") +set(BUILD_opencv_ocl OFF CACHE BOOL "") +set(BUILD_opencv_photo OFF CACHE BOOL "") +set(BUILD_opencv_stitching OFF CACHE BOOL "") +set(BUILD_opencv_superres OFF CACHE BOOL "") +set(BUILD_opencv_video OFF CACHE BOOL "") +set(BUILD_opencv_videostab OFF CACHE BOOL "") +set(BUILD_opencv_world OFF CACHE BOOL "") + +set(WITH_QT OFF CACHE BOOL "") +set(WITH_GTK OFF CACHE BOOL "") +set(WITH_OPENGL OFF CACHE BOOL "") +set(WITH_FFMPEG OFF CACHE BOOL "") +set(WITH_GSTREAMER OFF CACHE BOOL "") +set(WITH_V4L OFF CACHE BOOL "") +set(WITH_1394 OFF CACHE BOOL "") +set(WITH_CUDA OFF CACHE BOOL "") +set(WITH_OPENCL OFF CACHE BOOL "") +set(WITH_TBB OFF CACHE BOOL "") +set(WITH_IPP OFF CACHE BOOL "") +set(WITH_TIFF OFF CACHE BOOL "") +set(WITH_JASPER OFF CACHE BOOL "") +set(WITH_OPENEXR OFF CACHE BOOL "") + +FetchContent_Declare( + opencv + GIT_REPOSITORY https://github.com/opencv/opencv + GIT_TAG 2.4.13.6 + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(opencv) + +add_library(opencv_legacy_c_api INTERFACE) +target_include_directories(opencv_legacy_c_api + INTERFACE + ${opencv_SOURCE_DIR}/modules/core/include + ${opencv_SOURCE_DIR}/modules/imgproc/include + ${opencv_SOURCE_DIR}/modules/highgui/include +) +target_link_libraries(opencv_legacy_c_api + INTERFACE + opencv_core + opencv_imgproc + opencv_highgui +) + +add_library(filters_core STATIC + ${FILTER_SOURCES} +) +add_library(filters ALIAS filters_core) + +target_include_directories(filters_core + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +add_library(convolution_runtime STATIC + ${CONVOLUTION_RUNTIME_SOURCES} +) +target_include_directories(convolution_runtime + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/internal +) +target_link_libraries(convolution_runtime + PUBLIC + filters_core + opencv_legacy_c_api + Threads::Threads + OpenMP::OpenMP_C +) + +add_executable(app + src/app.c + src/cli_args.c +) +target_link_libraries(app + PRIVATE + convolution_runtime +) + +if(BUILD_TESTING) + find_package(PkgConfig REQUIRED) + pkg_check_modules(CMOCKA REQUIRED IMPORTED_TARGET cmocka) + + add_subdirectory(test/sequential_tests) + add_subdirectory(test/thread_pool_tests) +endif() diff --git a/README.md b/README.md index e62fdfb..15f4307 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,427 @@ # Image Conversation -Основная ветка намеренно оставлена минимальной. +Image Conversation — консольное приложение для применения фильтров к одному изображению или к набору изображений. -Реализации задач разнесены по отдельным веткам: +## Запуск программы + +### Сборка + +Для обычного запуска программы достаточно собрать таргет `app` в release-конфигурации: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF +cmake --build build --target app -j +``` + +После сборки исполняемый файл находится в `build/bin/app`. + +### Пример для одного изображения + +```bash +./build/bin/app \ + -i input/satoru.jpg -o output/satoru.jpg \ + -f gauss -h 5 -w 5 \ + -f sharpen -h 3 -w 3 \ + -p rows +``` + +### Пример для нескольких изображений + +Для пакетной обработки нужно несколько раз передать пару `-i -o `. Набор фильтров и режим выполнения применяются ко всем изображениям из запуска. + +```bash +./build/bin/app \ + -i input/satoru.jpg -o output/satoru.jpg \ + -i input/sunshine.jpg -o output/sunshine.jpg \ + -f gauss -h 5 -w 5 \ + -f sharpen -h 3 -w 3 \ + -p rows +``` + +### Аргументы запуска + +Поддерживаются один или два фильтра за один запуск. Фильтр задаётся через `-f -h -w `. Для направленных фильтров можно дополнительно указать `-t `. + +Доступные имена фильтров: `blur`, `mean`, `gauss`, `motion`, `edge`, `sharpen`, `emboss`, `median`. + +Последний аргумент выбирает режим выполнения: + +| Режим | Что делает | +|---|---| +| `-s` | последовательная свёртка | +| `-p rows` | параллельная обработка по строкам | +| `-p cols` | параллельная обработка по столбцам | +| `-p pixels` | параллельная обработка по пикселям | +| `-p grid` | параллельная обработка по прямоугольным блокам | + +Справку по формату аргументов можно вывести так: + +```bash +./build/bin/app --help +``` + +## Запуск тестов + +Тесты подключены через CTest. Для их сборки нужна установленная `cmocka`. + +```bash +cmake -S . -B build-tests -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON +cmake --build build-tests -j +ctest --test-dir build-tests --output-on-failure +``` + +## Как запустить бенчмарки у себя + +Бенчмарки лучше запускать на release-сборке: debug-сборка исказит сравнение режимов свёртки. + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF +cmake --build build --target app -j +python3 scripts/benchmark_readme.py +``` + +Скрипт `scripts/benchmark_readme.py` запускает все режимы по 10 раз, сохраняет сырые данные в [`benchmarks/readme_benchmark_results.json`](benchmarks/readme_benchmark_results.json) и обновляет графики в `benchmarks/`. Для запуска нужны Python-пакеты `matplotlib`, `numpy` и `Pillow`. + +Если нужно ограничить число потоков OpenMP, задайте `OMP_NUM_THREADS` перед запуском: + +```bash +OMP_NUM_THREADS=8 python3 scripts/benchmark_readme.py +``` + +## Бенчмарки + +Эта секция содержит уже полученные замеры. Команды для воспроизведения на своём компьютере приведены выше. + +### Что измерялось + +В текущих замерах сравнивается пайплайн из двух фильтров: + +```text +gauss 5x5 -> sharpen 3x3 +``` + +Время указано в миллисекундах. Один замер включает полный проход `image_pipeline_run`: чтение изображения, применение фильтров и запись результата. Каждый режим запускался 10 раз, в таблицах показано среднее время. Подробные значения всех запусков лежат в [`benchmarks/readme_benchmark_results.json`](benchmarks/readme_benchmark_results.json). + +### Окружение + +| Параметр | Значение | +|---|---| +| CPU | Intel(R) Core(TM) Ultra 5 125H | +| Architecture | x86_64 | +| Логических CPU | 18 | +| Список CPU | 0-17 | +| Ядер на сокет | 14 | +| Потоков на ядро | 2 | +| Максимальная частота CPU | 4600 MHz | +| Минимальная частота CPU | 400 MHz | +| L1d cache | 448 KiB | +| L1i cache | 768 KiB | +| L2 cache | 14 MiB | +| L3 cache | 18 MiB | +| RAM | 32260660 kB, примерно 30.8 GiB | +| Потоков OpenMP | 18 | +| Запусков на режим | 10 | + +### Сокращения в таблицах + +| Сокращение | Значение | +|---|---| +| `seq` | последовательная свёртка | +| `rows` | параллельная свёртка по строкам | +| `cols` | параллельная свёртка по столбцам | +| `pixels` | параллельная свёртка по пикселям | +| `grid` | параллельная свёртка по прямоугольным блокам | +| `ms` | миллисекунды | +| `Min` / `Max` | минимальное и максимальное время среди 10 запусков | +| `95% CI` | 95% доверительный интервал среднего значения | +| `CV` | coefficient of variation, относительный разброс `s / mean` | +| `x`, `speedup` | ускорение относительно последовательного режима `seq` | + +### Краткие выводы + +На маленьких изображениях все параллельные режимы близки друг к другу: накладные расходы на запуск потоков и синхронизацию уже заметны, поэтому разница между стратегиями небольшая. + +На крупных изображениях лучше всего показал себя режим `rows`. Для `musashi.jpg` он дал ускорение примерно `6.64x`, а на пайплайне из десяти 4K-изображений — `5.81x`. Режимы `cols`, `pixels` и `grid` тоже дают существенное ускорение, но в этих замерах чаще уступают разбиению по строкам. + +Основная причина преимущества `rows` на больших изображениях — лучшая кэш-локальность. Изображение хранится построчно: соседние пиксели одной строки лежат рядом в памяти, а расстояние между соседними строками равно количеству байт между началами двух этих строк с учетом возможного выравнивания. В режиме `rows` каждый поток получает непрерывный диапазон строк, последовательно читает и пишет большие линейные участки памяти, что обеспечивает лучшую кэш-локальность. В режиме `cols` потоки обрабатывают вертикальные полосы: внутри каждой строки доступ по-прежнему последовательный, но все потоки проходят одни и те же строки разными полосами, чаще пересекаются на границах кэш-линий и создают больше давления на кэш/TLB. В режиме `pixels` единица работы слишком мелкая, а `convolution_apply_pixel` выполняет проверки для каждого пикселя. В режиме `grid` локальность внутри плитки хорошая, но появляется больше мелких прямоугольных областей и накладных расходов на разбиение; на маленьких изображениях это иногда помогает, а на больших чаще проигрывает простому линейному проходу по строкам. + +Если `cols` или `grid` в отдельной строке таблицы оказываются немного быстрее `rows`, это не обязательно ошибка в коде или в замерах. Например, для `stariy_bog.png` средние `cols = 81.0 ms` и `rows = 81.2 ms` отличаются меньше, чем доверительный интервал измерений. Для маленьких изображений накладные расходы потоков, планирование OpenMP, состояние CPU frequency scaling и фоновые процессы могут быть сравнимы с самой полезной работой. Поэтому небольшую разницу внутри погрешности стоит читать как статистически незначимую, а устойчивый вывод лучше делать по крупным изображениям и пакетному сценарию. + +### Одно изображение + +В этом сценарии один запуск программы обрабатывает одно изображение из `input/`. + +#### Сводная таблица + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ИзображениеРазмерСреднее время, msЛучший параллельный режим
seqrowscolspixelsgrid
satoru.jpg225x22553.113.714.514.913.3grid, 3.99x
sunshine.jpg300x16853.912.712.912.813.2rows, 4.24x
stariy_bog.png914x480424.981.281.081.982.8cols, 5.25x
musashi.jpg2560x14403518.7530.3540.0556.0536.6rows, 6.64x
sea.png3840x21607929.21585.41860.41830.51814.1rows, 5.00x
+ +#### Оценка погрешности + +Погрешность ниже посчитана как 95% доверительный интервал среднего по 10 запускам: `mean ± 2.262 * s / sqrt(10)`, где `s` — выборочное стандартное отклонение. В скобках указан `CV = s / mean`, то есть относительный разброс. + +| Изображение | `seq`, ms | `rows`, ms | `cols`, ms | `pixels`, ms | `grid`, ms | +|---|---:|---:|---:|---:|---:| +| `input/satoru.jpg` | 53.1 ± 2.3 (6.0%) | 13.7 ± 0.9 (9.0%) | 14.5 ± 2.1 (20.1%) | 14.9 ± 1.3 (12.3%) | 13.3 ± 0.9 (9.1%) | +| `input/sunshine.jpg` | 53.9 ± 1.9 (5.0%) | 12.7 ± 1.1 (12.5%) | 12.9 ± 1.1 (11.5%) | 12.8 ± 1.3 (13.8%) | 13.2 ± 0.7 (7.0%) | +| `input/stariy_bog.png` | 424.9 ± 2.8 (0.9%) | 81.2 ± 4.9 (8.4%) | 81.0 ± 3.0 (5.1%) | 81.9 ± 1.9 (3.2%) | 82.8 ± 3.1 (5.3%) | +| `input/musashi.jpg` | 3518.7 ± 58.6 (2.3%) | 530.3 ± 5.3 (1.4%) | 540.0 ± 8.9 (2.3%) | 556.0 ± 21.2 (5.3%) | 536.6 ± 18.3 (4.8%) | +| `input/sea.png` | 7929.2 ± 41.1 (0.7%) | 1585.4 ± 213.9 (18.9%) | 1860.4 ± 47.3 (3.6%) | 1830.5 ± 25.8 (2.0%) | 1814.1 ± 33.6 (2.6%) | + +#### Подробно по каждому изображению + +##### `satoru.jpg`, 225x225 + + + + + + + + + + + + + + + + + + + + + +
РежимВремя, msУскорение относительно seq
СреднееMinMax
seq53.149.059.01.00x
rows13.711.715.73.88x
cols14.511.621.33.66x
pixels14.913.018.83.56x
grid13.311.815.33.99x
+ +![Benchmark for satoru.jpg](benchmarks/readme_single_satoru_gauss_5x5_then_sharpen_3x3_benchmark.png) + +##### `sunshine.jpg`, 300x168 + + + + + + + + + + + + + + + + + + + + + +
РежимВремя, msУскорение относительно seq
СреднееMinMax
seq53.951.159.31.00x
rows12.79.215.04.24x
cols12.911.115.74.16x
pixels12.810.316.44.21x
grid13.212.115.34.09x
+ +![Benchmark for sunshine.jpg](benchmarks/readme_single_sunshine_gauss_5x5_then_sharpen_3x3_benchmark.png) + +##### `stariy_bog.png`, 914x480 + + + + + + + + + + + + + + + + + + + + + +
РежимВремя, msУскорение относительно seq
СреднееMinMax
seq424.9415.5429.51.00x
rows81.270.190.75.23x
cols81.074.988.35.25x
pixels81.978.886.45.19x
grid82.877.491.45.13x
+ +![Benchmark for stariy_bog.png](benchmarks/readme_single_stariy_bog_gauss_5x5_then_sharpen_3x3_benchmark.png) + +##### `musashi.jpg`, 2560x1440 + + + + + + + + + + + + + + + + + + + + + +
РежимВремя, msУскорение относительно seq
СреднееMinMax
seq3518.73457.53730.41.00x
rows530.3519.6542.96.64x
cols540.0522.7567.36.52x
pixels556.0521.0619.96.33x
grid536.6517.9603.16.56x
+ +![Benchmark for musashi.jpg](benchmarks/readme_single_musashi_gauss_5x5_then_sharpen_3x3_benchmark.png) + +##### `sea.png`, 3840x2160 + + + + + + + + + + + + + + + + + + + + + +
РежимВремя, msУскорение относительно seq
СреднееMinMax
seq7929.27856.48061.11.00x
rows1585.41237.31871.25.00x
cols1860.41761.91990.64.26x
pixels1830.51799.21916.14.33x
grid1814.11730.61890.54.37x
+ +![Benchmark for sea.png](benchmarks/readme_single_sea_gauss_5x5_then_sharpen_3x3_benchmark.png) + +### Пайплайн из 10 4K-изображений + +В этом сценарии один запуск программы получает сразу 10 изображений размером 3840x2160. Это ближе к пакетной обработке: накладные расходы распределяются на больший объём работы, а разница между стратегиями распараллеливания становится заметнее. + + + + + + + + + + + + + + + + + + + + + +
РежимВремя, msУскорение относительно seq
СреднееMinMax
seq76841.276084.977188.11.00x
rows13222.710178.515360.25.81x
cols14879.314739.115006.55.16x
pixels15510.814846.216740.94.95x
grid15559.014962.516204.34.94x
+ +Оценка погрешности для пакетного сценария: + +| Режим | Среднее ± 95% CI, ms | Станд. отклонение, ms | CV | +|---|---:|---:|---:| +| `seq` | 76841.2 ± 225.5 | 315.2 | 0.4% | +| `rows` | 13222.7 ± 1615.3 | 2258.0 | 17.1% | +| `cols` | 14879.3 ± 64.9 | 90.8 | 0.6% | +| `pixels` | 15510.8 ± 511.8 | 715.5 | 4.6% | +| `grid` | 15559.0 ± 328.1 | 458.6 | 2.9% | + +У `rows` в пакетном сценарии разброс заметно выше: первые четыре запуска были около 10.2-11.1 с, следующие — около 14.8-15.4 с. Поэтому среднее `rows` быстрее остальных режимов, но именно этот результат имеет самую широкую погрешность и требует аккуратной интерпретации. + +![Benchmark for 10-image pipeline](benchmarks/readme_pipeline_gauss_5x5_then_sharpen_3x3_benchmark.png) + +
+Список изображений из пакетного сценария + +| # | Файл | Размер | +|---:|---|---:| +| 1 | `171406-odin_udar_chelovek_dvojnoe_proniknovenie-prefektura_sajtama-odin_udar_chelovek-anime-rukav-3840x2160.jpg` | 3840x2160 | +| 2 | `172836-ikona-asfalt-dorozhnoe_pokrytie-most-zdanie-3840x2160.png` | 3840x2160 | +| 3 | `173960-galaktika_andromedy-galaktika-mlechnyj_put-zemlya-zvezda-3840x2160.jpg` | 3840x2160 | +| 4 | `175978-yastreb-hishhnaya_ptica-sokol-nauka-biologiya-3840x2160.jpg` | 3840x2160 | +| 5 | `176372-oblako-gora-voda-rastenie-prirodnyj_landshaft-3840x2160.jpg` | 3840x2160 | +| 6 | `176515-samolet-samolety-polet-reaktivnyj_samolet-aviaciya-3840x2160.jpg` | 3840x2160 | +| 7 | `176585-albert_ejnshtejn_iskusstvo-art-poster-dizajn-nauka-3840x2160.jpg` | 3840x2160 | +| 8 | `179158-kon-belye-pechen-nazemnye_zhivotnye-prirodnyj_landshaft-3840x2160.jpg` | 3840x2160 | +| 9 | `181366-nytol_herbal_30_tabletok-nosok-bunionektomiya-dostupnyj-prigonka-3840x2160.jpg` | 3840x2160 | +| 10 | `img3.akspic.ru-nebo-kosmicheskoe_prostranstvo-film-gorizont-atmosfera-3840x2160.jpg` | 3840x2160 | + +
-- `feat/sequential_convolution` — последовательная свёртка. -- `feat/parallel_convolution` — параллельная свёртка поверх последовательной реализации. -- `feat/image_pipepline` — pipeline обработки изображений поверх параллельной реализации. diff --git a/docs/task3/README.md b/docs/task3/README.md new file mode 100644 index 0000000..b96b6d5 --- /dev/null +++ b/docs/task3/README.md @@ -0,0 +1,39 @@ +# Бенчмарк размера очередей пайплайна обработки изображений + +![График зависимости времени от размера computer очереди](./pipeline_queue_benchmark.png) + +Измерялась зависимость полного времени обработки пайплайна от размера очереди `computer`. Всего в пайплайне три очереди: `reader → computer → writer`. + +## Параметры эксперимента + +| Параметр | Значение | +|---|---:| +| Изображений | `10` (`3840x2160`) | +| Всего потоков | `18` | +| Reader threads | `2` | +| Writer threads | `2` | +| Computer budget | `14` | +| Размер reader queue | `4` | +| Размер writer queue | `4` | +| Размеры computer queue | `2, 3, 4, 5` | +| Запусков на кейс | `10` | +| Фильтры | `motion_blur 9x9 + gauss 5x5` | + +## Режимы + +Все 4 режима — параллельные (`cols`, `raw`, `pixel`, `grid`). +Каждый запускает 2 computer workers, каждый с `omp_set_num_threads(7)`. +Reader и writer — по 2 pthread-потока. + +## Таблица результатов + +| Computer queue | cols, мс | raw, мс | pixel, мс | grid, мс | +|---:|---:|---:|---:|---:| +| 1 | `7421.71 +- 1282.75` | `8612.90 +- 743.29` | `8346.87 +- 276.93` | `8299.32 +- 341.48` | +| 2 | `8313.90 +- 388.42` | `8586.23 +- 658.12` | `8269.55 +- 782.90` | `8271.16 +- 501.81` | +| 3 | `8054.89 +- 54.17` | `8056.21 +- 131.04` | `8123.54 +- 145.74` | `8312.45 +- 650.10` | +| 4 | `8683.15 +- 337.37` | `8507.49 +- 1295.75` | `8126.86 +- 223.49` | `8081.86 +- 162.74` | + +## Лучшие конфигурации + +- **cols**: минимум 7421.7 мс при computer_queue=1- **raw**: минимум 8056.2 мс при computer_queue=3- **pixel**: минимум 8123.5 мс при computer_queue=3- **grid**: минимум 8081.9 мс при computer_queue=4 diff --git a/docs/task3/pipeline_modes_benchmark.png b/docs/task3/pipeline_modes_benchmark.png new file mode 100644 index 0000000..8d2e4c8 Binary files /dev/null and b/docs/task3/pipeline_modes_benchmark.png differ diff --git a/docs/task3/pipeline_queue_benchmark.png b/docs/task3/pipeline_queue_benchmark.png new file mode 100644 index 0000000..040ed79 Binary files /dev/null and b/docs/task3/pipeline_queue_benchmark.png differ diff --git a/input/171406-odin_udar_chelovek_dvojnoe_proniknovenie-prefektura_sajtama-odin_udar_chelovek-anime-rukav-3840x2160.jpg b/input/171406-odin_udar_chelovek_dvojnoe_proniknovenie-prefektura_sajtama-odin_udar_chelovek-anime-rukav-3840x2160.jpg new file mode 100644 index 0000000..9c2ce56 Binary files /dev/null and b/input/171406-odin_udar_chelovek_dvojnoe_proniknovenie-prefektura_sajtama-odin_udar_chelovek-anime-rukav-3840x2160.jpg differ diff --git a/input/172836-ikona-asfalt-dorozhnoe_pokrytie-most-zdanie-3840x2160.png b/input/172836-ikona-asfalt-dorozhnoe_pokrytie-most-zdanie-3840x2160.png new file mode 100644 index 0000000..538e2a1 Binary files /dev/null and b/input/172836-ikona-asfalt-dorozhnoe_pokrytie-most-zdanie-3840x2160.png differ diff --git a/input/173960-galaktika_andromedy-galaktika-mlechnyj_put-zemlya-zvezda-3840x2160.jpg b/input/173960-galaktika_andromedy-galaktika-mlechnyj_put-zemlya-zvezda-3840x2160.jpg new file mode 100644 index 0000000..d7976ee Binary files /dev/null and b/input/173960-galaktika_andromedy-galaktika-mlechnyj_put-zemlya-zvezda-3840x2160.jpg differ diff --git a/input/175978-yastreb-hishhnaya_ptica-sokol-nauka-biologiya-3840x2160.jpg b/input/175978-yastreb-hishhnaya_ptica-sokol-nauka-biologiya-3840x2160.jpg new file mode 100644 index 0000000..4a37dfe Binary files /dev/null and b/input/175978-yastreb-hishhnaya_ptica-sokol-nauka-biologiya-3840x2160.jpg differ diff --git a/input/176372-oblako-gora-voda-rastenie-prirodnyj_landshaft-3840x2160.jpg b/input/176372-oblako-gora-voda-rastenie-prirodnyj_landshaft-3840x2160.jpg new file mode 100644 index 0000000..99b9f81 Binary files /dev/null and b/input/176372-oblako-gora-voda-rastenie-prirodnyj_landshaft-3840x2160.jpg differ diff --git a/input/176515-samolet-samolety-polet-reaktivnyj_samolet-aviaciya-3840x2160.jpg b/input/176515-samolet-samolety-polet-reaktivnyj_samolet-aviaciya-3840x2160.jpg new file mode 100644 index 0000000..5c8e462 Binary files /dev/null and b/input/176515-samolet-samolety-polet-reaktivnyj_samolet-aviaciya-3840x2160.jpg differ diff --git a/input/176585-albert_ejnshtejn_iskusstvo-art-poster-dizajn-nauka-3840x2160.jpg b/input/176585-albert_ejnshtejn_iskusstvo-art-poster-dizajn-nauka-3840x2160.jpg new file mode 100644 index 0000000..9217e87 Binary files /dev/null and b/input/176585-albert_ejnshtejn_iskusstvo-art-poster-dizajn-nauka-3840x2160.jpg differ diff --git a/input/179158-kon-belye-pechen-nazemnye_zhivotnye-prirodnyj_landshaft-3840x2160.jpg b/input/179158-kon-belye-pechen-nazemnye_zhivotnye-prirodnyj_landshaft-3840x2160.jpg new file mode 100644 index 0000000..a1dc170 Binary files /dev/null and b/input/179158-kon-belye-pechen-nazemnye_zhivotnye-prirodnyj_landshaft-3840x2160.jpg differ diff --git a/input/181366-nytol_herbal_30_tabletok-nosok-bunionektomiya-dostupnyj-prigonka-3840x2160.jpg b/input/181366-nytol_herbal_30_tabletok-nosok-bunionektomiya-dostupnyj-prigonka-3840x2160.jpg new file mode 100644 index 0000000..d30a817 Binary files /dev/null and b/input/181366-nytol_herbal_30_tabletok-nosok-bunionektomiya-dostupnyj-prigonka-3840x2160.jpg differ diff --git a/input/img3.akspic.ru-nebo-kosmicheskoe_prostranstvo-film-gorizont-atmosfera-3840x2160.jpg b/input/img3.akspic.ru-nebo-kosmicheskoe_prostranstvo-film-gorizont-atmosfera-3840x2160.jpg new file mode 100644 index 0000000..45e05a2 Binary files /dev/null and b/input/img3.akspic.ru-nebo-kosmicheskoe_prostranstvo-film-gorizont-atmosfera-3840x2160.jpg differ diff --git a/input/musashi.jpg b/input/musashi.jpg new file mode 100644 index 0000000..5f2320c Binary files /dev/null and b/input/musashi.jpg differ diff --git a/input/satoru.jpg b/input/satoru.jpg new file mode 100644 index 0000000..20c56ff Binary files /dev/null and b/input/satoru.jpg differ diff --git a/input/sea.png b/input/sea.png new file mode 100644 index 0000000..50ddda1 Binary files /dev/null and b/input/sea.png differ diff --git a/input/stariy_bog.png b/input/stariy_bog.png new file mode 100644 index 0000000..a8df030 Binary files /dev/null and b/input/stariy_bog.png differ diff --git a/input/sunshine.jpg b/input/sunshine.jpg new file mode 100644 index 0000000..7fc4963 Binary files /dev/null and b/input/sunshine.jpg differ diff --git a/internal/sequentially_convolution/helper_functions.h b/internal/sequentially_convolution/helper_functions.h new file mode 100644 index 0000000..6c9a653 --- /dev/null +++ b/internal/sequentially_convolution/helper_functions.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include + +static inline unsigned char clamp_to_u8(double value) { + if (value < 0.0) { + return 0U; + } + if (value > 255.0) { + return 255U; + } + return (unsigned char)(value + 0.5); +} + +static inline size_t wrap_index(long value, size_t limit) { + long wrapped = value % (long)limit; + if (wrapped < 0) { + wrapped += (long)limit; + } + return (size_t)wrapped; +} + +static inline size_t clamp_index(long value, size_t limit) { + if (value < 0) { + return 0U; + } + if ((size_t)value >= limit) { + return limit - 1U; + } + return (size_t)value; +} + +static inline size_t reflect_index(long value, size_t limit) { + if (limit <= 1U) { + return 0U; + } + + const long period = 2L * ((long)limit - 1L); + long reflected = value % period; + + if (reflected < 0) { + reflected += period; + } + if (reflected >= (long)limit) { + reflected = period - reflected; + } + + return (size_t)reflected; +} + +static inline size_t +resolve_index(long value, size_t limit, filter_border_mode_t border_mode) { + switch (border_mode) { + case FILTER_BORDER_CLAMP: + return clamp_index(value, limit); + case FILTER_BORDER_REFLECT: + return reflect_index(value, limit); + case FILTER_BORDER_WRAP: + default: + return wrap_index(value, limit); + } +} diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app.c b/src/app.c new file mode 100644 index 0000000..6f3b0da --- /dev/null +++ b/src/app.c @@ -0,0 +1,110 @@ +#include "cli_args.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define MAX_ERROR_MESSAGE_LENGTH 32 + +static double elapsed_ms(const struct timespec *start, + const struct timespec *end) { + const double seconds = (double)(end->tv_sec - start->tv_sec) * 1000.0; + const double nanoseconds = + (double)(end->tv_nsec - start->tv_nsec) / 1000000.0; + + return seconds + nanoseconds; +} + +static image_convolution_runner_t +select_convolution_runner(execution_mode_t mode) { + switch (mode) { + case EXECUTION_MODE_SEQ: + return sequential_convolution; + case EXECUTION_MODE_ROWS: + return parallel_convolution_rows; + case EXECUTION_MODE_PIXELS: + return parallel_convolution_pixels; + case EXECUTION_MODE_COLS: + return parallel_convolution_cols; + case EXECUTION_MODE_GRID: + return parallel_convolution_rectangle; + default: + return NULL; + } +} + +static void fill_pipeline_request(image_pipeline_request_t *pipeline_request, + const cli_request_t *cli_request, + size_t image_index, + image_convolution_runner_t runner) { + pipeline_request->input_path = cli_request->images[image_index].input_path; + pipeline_request->output_path = cli_request->images[image_index].output_path; + pipeline_request->filter_count = cli_request->filter_count; + pipeline_request->run_convolution = runner; + + for (size_t i = 0; i < cli_request->filter_count; ++i) { + pipeline_request->filters[i] = (filter_request_t){ + .kind = cli_request->filters[i].kind, + .width = cli_request->filters[i].width, + .height = cli_request->filters[i].height, + .direction = cli_request->filters[i].direction, + .border_mode = cli_request->filters[i].border_mode, + }; + } +} + +int main(int argc, char **argv) { + cli_request_t request; + char error_message[MAX_ERROR_MESSAGE_LENGTH]; + cli_parse_status_t parse_status = + cli_parse_args(argc, argv, &request, error_message, sizeof(error_message)); + + if (parse_status == CLI_PARSE_HELP) { + return 0; + } + + if (parse_status != CLI_PARSE_OK) { + puts(error_message); + return -1; + } + + image_convolution_runner_t runner = select_convolution_runner(request.mode); + if (runner == NULL) { + cli_request_destroy(&request); + fputs("invalid execution mode\n", stderr); + return -1; + } + + image_pipeline_request_t *pipeline_requests = + (image_pipeline_request_t *)malloc(sizeof(*pipeline_requests) * + request.image_count); + if (pipeline_requests == NULL) { + cli_request_destroy(&request); + fputs("failed to allocate pipeline requests\n", stderr); + return -1; + } + + for (size_t i = 0; i < request.image_count; ++i) { + fill_pipeline_request(&pipeline_requests[i], &request, i, runner); + } + + struct timespec start_time; + struct timespec end_time; + + timespec_get(&start_time, TIME_UTC); + + const int pipeline_result = + image_pipeline_run(pipeline_requests, request.image_count); + + timespec_get(&end_time, TIME_UTC); + printf("processing time: %.3f ms\n", elapsed_ms(&start_time, &end_time)); + + free(pipeline_requests); + cli_request_destroy(&request); + return pipeline_result == 0 ? 0 : -1; +} diff --git a/src/app/execution_config.h b/src/app/execution_config.h new file mode 100644 index 0000000..2bcd6f9 --- /dev/null +++ b/src/app/execution_config.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +typedef enum execution_mode { + EXECUTION_MODE_SEQ = 0, + EXECUTION_MODE_ROWS, + EXECUTION_MODE_PIXELS, + EXECUTION_MODE_COLS, + EXECUTION_MODE_GRID, +} execution_mode_t; + +typedef struct execution_config { + execution_mode_t mode; + size_t threads; + size_t grid_rows; + size_t grid_cols; +} execution_config_t; + +static inline execution_config_t make_execution_config(execution_mode_t mode, + size_t threads, + size_t grid_rows, + size_t grid_cols) { + return (execution_config_t){ + .mode = mode, + .threads = threads, + .grid_rows = grid_rows, + .grid_cols = grid_cols, + }; +} diff --git a/src/cli_args.c b/src/cli_args.c new file mode 100644 index 0000000..c711d32 --- /dev/null +++ b/src/cli_args.c @@ -0,0 +1,291 @@ +#include "cli_args.h" + +#include +#include +#include +#include +#include + +#define INITIAL_IMAGE_CAPACITY 4U +#define MIN_ARG_COUNT 12 + +static void cli_set_invalid(char *error_message, size_t error_message_size) { + if (error_message != NULL && error_message_size != 0U) { + snprintf(error_message, error_message_size, "invalid"); + } +} + +static cli_parse_status_t cli_invalid(char *error_message, + size_t error_message_size) { + cli_set_invalid(error_message, error_message_size); + return CLI_PARSE_ERROR; +} + +static bool cli_parse_size(const char *text, size_t *value) { + char *end = NULL; + unsigned long parsed = 0; + + if (text == NULL || value == NULL || *text == '\0') { + return false; + } + + parsed = strtoul(text, &end, 10); + if (*end != '\0' || parsed == 0UL) { + return false; + } + + *value = (size_t)parsed; + return true; +} + +static bool cli_parse_filter(const char *text, filter_kind_t *kind) { + if (strcmp(text, "blur") == 0) { + *kind = FILTER_KIND_BLUR; + } else if (strcmp(text, "mean") == 0) { + *kind = FILTER_KIND_MEAN; + } else if (strcmp(text, "gauss") == 0) { + *kind = FILTER_KIND_GAUSSIAN_BLUR; + } else if (strcmp(text, "motion") == 0) { + *kind = FILTER_KIND_MOTION_BLUR; + } else if (strcmp(text, "edge") == 0) { + *kind = FILTER_KIND_EDGE_DETECT; + } else if (strcmp(text, "sharpen") == 0) { + *kind = FILTER_KIND_SHARPEN; + } else if (strcmp(text, "emboss") == 0) { + *kind = FILTER_KIND_EMBOSS; + } else if (strcmp(text, "median") == 0) { + *kind = FILTER_KIND_MEDIAN; + } else { + return false; + } + + return true; +} + +static bool cli_parse_type(const char *text, filter_direction_t *direction) { + if (strcmp(text, "horizontal") == 0) { + *direction = FILTER_DIRECTION_HORIZONTAL; + } else if (strcmp(text, "vertical") == 0) { + *direction = FILTER_DIRECTION_VERTICAL; + } else if (strcmp(text, "diagonal") == 0) { + *direction = FILTER_DIRECTION_DIAGONAL_45; + } else if (strcmp(text, "omni") == 0) { + *direction = FILTER_DIRECTION_OMNIDIRECTIONAL; + } else { + return false; + } + + return true; +} + +static bool cli_parse_execution_mode(const char *text, execution_mode_t *mode) { + if (strcmp(text, "cols") == 0) { + *mode = EXECUTION_MODE_COLS; + } else if (strcmp(text, "rows") == 0 || strcmp(text, "raws") == 0) { + *mode = EXECUTION_MODE_ROWS; + } else if (strcmp(text, "pixels") == 0) { + *mode = EXECUTION_MODE_PIXELS; + } else if (strcmp(text, "grid") == 0 || strcmp(text, "rectangle") == 0 || + strcmp(text, "random") == 0) { + *mode = EXECUTION_MODE_GRID; + } else { + return false; + } + + return true; +} + +static void cli_init_request(cli_request_t *request) { + memset(request, 0, sizeof(*request)); + request->mode = EXECUTION_MODE_SEQ; + for (size_t i = 0; i < CLI_MAX_FILTERS; ++i) { + request->filters[i].direction = FILTER_DIRECTION_NONE; + request->filters[i].border_mode = FILTER_BORDER_WRAP; + } +} + +void cli_request_destroy(cli_request_t *request) { + if (request == NULL) { + return; + } + + free(request->images); + request->images = NULL; + request->image_count = 0U; + request->image_capacity = 0U; +} + +static bool cli_request_add_image(cli_request_t *request, + const char *input_path, + const char *output_path) { + if (request == NULL || input_path == NULL || output_path == NULL) { + return false; + } + + if (request->image_count == request->image_capacity) { + const size_t max_capacity = SIZE_MAX / sizeof(*request->images); + size_t new_capacity = request->image_capacity == 0U + ? INITIAL_IMAGE_CAPACITY + : request->image_capacity * 2U; + + if (new_capacity <= request->image_capacity || + new_capacity > max_capacity) { + return false; + } + + cli_image_io_t *new_images = (cli_image_io_t *)realloc( + request->images, sizeof(*request->images) * new_capacity); + if (new_images == NULL) { + return false; + } + + request->images = new_images; + request->image_capacity = new_capacity; + } + + request->images[request->image_count] = (cli_image_io_t){ + .input_path = input_path, + .output_path = output_path, + }; + ++request->image_count; + return true; +} + +static bool cli_parse_image_io_pairs(int argc, + char **argv, + int *index, + cli_request_t *request) { + bool parsed_any = false; + + while (*index < argc && strcmp(argv[*index], "-i") == 0) { + if (*index + 3 >= argc || strcmp(argv[*index + 2], "-o") != 0) { + return false; + } + + if (!cli_request_add_image(request, argv[*index + 1], argv[*index + 3])) { + return false; + } + + *index += 4; + parsed_any = true; + } + + return parsed_any; +} + +static bool cli_parse_filter_spec(int argc, + char **argv, + int *index, + cli_filter_spec_t *filter) { + if (*index + 5 >= argc) { + return false; + } + + if (strcmp(argv[*index], "-f") != 0 || strcmp(argv[*index + 2], "-h") != 0 || + strcmp(argv[*index + 4], "-w") != 0) { + return false; + } + + if (!cli_parse_filter(argv[*index + 1], &filter->kind) || + !cli_parse_size(argv[*index + 3], &filter->height) || + !cli_parse_size(argv[*index + 5], &filter->width)) { + return false; + } + + *index += 6; + + if (*index + 1 < argc && strcmp(argv[*index], "-t") == 0) { + if (!cli_parse_type(argv[*index + 1], &filter->direction)) { + return false; + } + *index += 2; + } + + return true; +} + +cli_parse_status_t cli_parse_args(int argc, + char **argv, + cli_request_t *request, + char *error_message, + size_t error_message_size) { + int index = 1; + + if (request == NULL || argv == NULL) { + return cli_invalid(error_message, error_message_size); + } + + cli_init_request(request); + + if (error_message != NULL && error_message_size != 0U) { + error_message[0] = '\0'; + } + + if (argc == 2 && strcmp(argv[1], "--help") == 0) { + cli_print_help(stdout, argv[0]); + return CLI_PARSE_HELP; + } + + if (argc < MIN_ARG_COUNT) { + return cli_invalid(error_message, error_message_size); + } + + if (!cli_parse_image_io_pairs(argc, argv, &index, request)) { + cli_request_destroy(request); + return cli_invalid(error_message, error_message_size); + } + + if (!cli_parse_filter_spec(argc, argv, &index, &request->filters[0])) { + cli_request_destroy(request); + return cli_invalid(error_message, error_message_size); + } + + request->filter_count = 1U; + + if (index < argc && strcmp(argv[index], "-f") == 0) { + if (!cli_parse_filter_spec(argc, argv, &index, &request->filters[1])) { + cli_request_destroy(request); + return cli_invalid(error_message, error_message_size); + } + request->filter_count = CLI_MAX_FILTERS; + } + + if (index >= argc) { + cli_request_destroy(request); + return cli_invalid(error_message, error_message_size); + } + + if (strcmp(argv[index], "-s") == 0 && index + 1 == argc) { + request->mode = EXECUTION_MODE_SEQ; + } else if (strcmp(argv[index], "-p") == 0 && index + 2 == argc) { + if (!cli_parse_execution_mode(argv[index + 1], &request->mode)) { + cli_request_destroy(request); + return cli_invalid(error_message, error_message_size); + } + } else { + cli_request_destroy(request); + return cli_invalid(error_message, error_message_size); + } + + return CLI_PARSE_OK; +} + +void cli_print_help(FILE *stream, const char *program_name) { + const char *name = program_name != NULL ? program_name : "main"; + + fprintf( + stream, + "Usage:\n" + " %s (-i -o )+ -f -h -w " + "[-t ] -s\n" + " %s (-i -o )+ -f -h -w " + "[-t ] -p " + "\n" + " %s (-i -o )+ -f -h -w " + "[-t ] " + "-f -h -w [-t ] " + "(-s | -p )\n", + name, + name, + name); +} diff --git a/src/cli_args.h b/src/cli_args.h new file mode 100644 index 0000000..d68b1dd --- /dev/null +++ b/src/cli_args.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include +#include + +#define CLI_MAX_FILTERS 2U + +typedef enum cli_parse_status { + CLI_PARSE_OK = 0, + CLI_PARSE_HELP, + CLI_PARSE_ERROR, +} cli_parse_status_t; + +typedef struct cli_filter_spec { + filter_kind_t kind; + size_t width; + size_t height; + filter_direction_t direction; + filter_border_mode_t border_mode; +} cli_filter_spec_t; + +typedef struct cli_image_io { + const char *input_path; + const char *output_path; +} cli_image_io_t; + +typedef struct cli_request { + cli_image_io_t *images; + size_t image_count; + size_t image_capacity; + + execution_mode_t mode; + cli_filter_spec_t filters[CLI_MAX_FILTERS]; + size_t filter_count; +} cli_request_t; + +cli_parse_status_t cli_parse_args(int argc, + char **argv, + cli_request_t *request, + char *error_message, + size_t error_message_size); + +void cli_request_destroy(cli_request_t *request); + +void cli_print_help(FILE *stream, const char *program_name); diff --git a/src/convolution/convolution.c b/src/convolution/convolution.c new file mode 100644 index 0000000..4b7bea9 --- /dev/null +++ b/src/convolution/convolution.c @@ -0,0 +1,163 @@ +#include "convolution.h" +#include + +#include +#include +#include + +static int validate_convolution_input(const filter_t *filter, + const image_view_t *image_view) { + if (filter == NULL || image_view == NULL || image_view->data == NULL) { + return -1; + } + if (!filter_is_convolution(filter) || !filter_has_explicit_kernel(filter)) { + return -1; + } + if (filter->width == 0U || filter->height == 0U) { + return -1; + } + if (image_view->width == 0U || image_view->height == 0U || + image_view->channels == 0U) { + return -1; + } + if (image_view->width > (SIZE_MAX / image_view->channels)) { + return -1; + } + if (image_view->stride < image_view->width * image_view->channels) { + return -1; + } + if (image_view->height > (SIZE_MAX / image_view->stride)) { + return -1; + } + + return 0; +} + +static int convolution_context_is_valid(const convolution_context_t *context) { + return context != NULL && context->filter != NULL && + context->source != NULL && context->destination != NULL && + context->width != 0U && context->height != 0U && + context->stride != 0U && context->channels != 0U; +} + +static void convolution_apply_pixel_unchecked( + const convolution_context_t *context, size_t y, size_t x) { + const filter_t *filter = context->filter; + unsigned char *dst_pixel = + context->destination + y * context->stride + x * context->channels; + + for (size_t channel = 0; channel < context->channels; ++channel) { + if (context->channels == 4U && channel == 3U) { + dst_pixel[channel] = + context->source[y * context->stride + x * context->channels + channel]; + continue; + } + + double sum = 0.0; + + for (size_t filter_y = 0; filter_y < filter->height; ++filter_y) { + for (size_t filter_x = 0; filter_x < filter->width; ++filter_x) { + const long src_x = + (long)x - (long)(filter->width / 2U) + (long)filter_x; + const long src_y = + (long)y - (long)(filter->height / 2U) + (long)filter_y; + + const size_t image_x = + resolve_index(src_x, context->width, filter->border_mode); + const size_t image_y = + resolve_index(src_y, context->height, filter->border_mode); + + const unsigned char *src_pixel = context->source + + image_y * context->stride + + image_x * context->channels; + const double kernel_value = + filter->kernel[filter_y * filter->width + filter_x]; + + sum += (double)src_pixel[channel] * kernel_value; + } + } + + dst_pixel[channel] = clamp_to_u8(filter->factor * sum + filter->bias); + } +} + +int convolution_run(const filter_t *filter, + image_view_t *image_view, + convolution_executor_t executor) { + if (executor == NULL || validate_convolution_input(filter, image_view) != 0) { + return -1; + } + + const size_t buffer_size = image_view->height * image_view->stride; + unsigned char *source_copy = (unsigned char *)malloc(buffer_size); + if (source_copy == NULL) { + return -1; + } + + memcpy(source_copy, image_view->data, buffer_size); + + const convolution_context_t context = { + .filter = filter, + .source = source_copy, + .destination = image_view->data, + .height = image_view->height, + .width = image_view->width, + .stride = image_view->stride, + .channels = image_view->channels, + }; + + const int result = executor(&context); + free(source_copy); + return result; +} + +int convolution_apply_pixel(const convolution_context_t *context, + size_t y, + size_t x) { + if (!convolution_context_is_valid(context) || y >= context->height || + x >= context->width) { + return -1; + } + + convolution_apply_pixel_unchecked(context, y, x); + return 0; +} + +int convolution_apply_rect(const convolution_context_t *context, + size_t y_begin, + size_t y_end, + size_t x_begin, + size_t x_end) { + if (!convolution_context_is_valid(context) || y_begin > y_end || + x_begin > x_end || y_end > context->height || x_end > context->width) { + return -1; + } + + for (size_t y = y_begin; y < y_end; ++y) { + for (size_t x = x_begin; x < x_end; ++x) { + convolution_apply_pixel_unchecked(context, y, x); + } + } + + return 0; +} + +int convolution_apply_rows(const convolution_context_t *context, + size_t y_begin, + size_t y_end) { + if (!convolution_context_is_valid(context)) { + return -1; + } + + return convolution_apply_rect(context, y_begin, y_end, 0U, context->width); +} + +int convolution_apply_cols(const convolution_context_t *context, + size_t x_begin, + size_t x_end) { + if (!convolution_context_is_valid(context)) { + return -1; + } + + return convolution_apply_rect(context, 0U, context->height, x_begin, x_end); +} diff --git a/src/convolution/convolution.h b/src/convolution/convolution.h new file mode 100644 index 0000000..f8689dd --- /dev/null +++ b/src/convolution/convolution.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include +#include + +typedef struct convolution_context { + const filter_t *filter; + const unsigned char *source; + unsigned char *destination; + size_t height; + size_t width; + size_t stride; + size_t channels; +} convolution_context_t; + +typedef int (*convolution_executor_t)(const convolution_context_t *context); + +int convolution_run(const filter_t *filter, + image_view_t *image_view, + convolution_executor_t executor); + +int convolution_apply_pixel(const convolution_context_t *context, + size_t y, + size_t x); + +int convolution_apply_rect(const convolution_context_t *context, + size_t y_begin, + size_t y_end, + size_t x_begin, + size_t x_end); + +int convolution_apply_rows(const convolution_context_t *context, + size_t y_begin, + size_t y_end); + +int convolution_apply_cols(const convolution_context_t *context, + size_t x_begin, + size_t x_end); diff --git a/src/filters/blur_filter/blur_filter.c b/src/filters/blur_filter/blur_filter.c new file mode 100644 index 0000000..2dd8817 --- /dev/null +++ b/src/filters/blur_filter/blur_filter.c @@ -0,0 +1,60 @@ +#include "blur_filter.h" + +// clang-format off +static const double blur_kernel_3x3[] = { + 0.0, 0.2, 0.0, + 0.2, 0.2, 0.2, + 0.0, 0.2, 0.0 +}; + +static const double blur_factor_3x3 = 1.0; +static const double blur_bias_3x3 = 0.0; + +static const double blur_kernel_5x5[] = { + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 1.0, 1.0, 1.0, 0.0, + 1.0, 1.0, 1.0, 1.0, 1.0, + 0.0, 1.0, 1.0, 1.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, +}; + +static const double blur_factor_5x5 = 1.0 / 13.0; +static const double blur_bias_5x5 = 0.0; +// clang-format on + +filter_status_t +init_blur_filter(filter_t *filter, size_t width, size_t height) { + + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + blur_kernel_3x3, + blur_factor_3x3, + blur_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + blur_kernel_5x5, + blur_factor_5x5, + blur_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/blur_filter/blur_filter.h b/src/filters/blur_filter/blur_filter.h new file mode 100644 index 0000000..9ed1bd5 --- /dev/null +++ b/src/filters/blur_filter/blur_filter.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +filter_status_t init_blur_filter(filter_t *filter, size_t width, size_t height); diff --git a/src/filters/emboss_filter/emboss_filter.c b/src/filters/emboss_filter/emboss_filter.c new file mode 100644 index 0000000..dabc9a8 --- /dev/null +++ b/src/filters/emboss_filter/emboss_filter.c @@ -0,0 +1,71 @@ +#include "emboss_filter.h" + +// clang-format off +static const double emboss_kernel_3x3[] = { + -1, -1, 0, + -1, 0, 1, + 0, 1, 1, +}; + +static const double emboss_factor_3x3 = 1.0; +static const double emboss_bias_3x3 = 128.0; + +static const double emboss_kernel_5x5[] = { + -1, -1, -1, -1, 0, + -1, -1, -1, 0, 1, + -1, -1, 0, 1, 1, + -1, 0, 1, 1, 1, + 0, 1, 1, 1, 1, +}; + +static const double emboss_factor_5x5 = 1.0; +static const double emboss_bias_5x5 = 128.0; +// clang-format on + +filter_status_t init_emboss_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (direction == FILTER_DIRECTION_NONE) { + direction = FILTER_DIRECTION_DIAGONAL_45; + } + + if (direction != FILTER_DIRECTION_DIAGONAL_45) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_EMBOSS, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + emboss_kernel_3x3, + emboss_factor_3x3, + emboss_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_EMBOSS, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + emboss_kernel_5x5, + emboss_factor_5x5, + emboss_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/emboss_filter/emboss_filter.h b/src/filters/emboss_filter/emboss_filter.h new file mode 100644 index 0000000..25e59f0 --- /dev/null +++ b/src/filters/emboss_filter/emboss_filter.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +filter_status_t init_emboss_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction); diff --git a/src/filters/filter.c b/src/filters/filter.c new file mode 100644 index 0000000..9d73a64 --- /dev/null +++ b/src/filters/filter.c @@ -0,0 +1,75 @@ +#include "filter.h" + +#include "blur_filter/blur_filter.h" +#include "emboss_filter/emboss_filter.h" +#include "find_edges_filter/find_edges_filter.h" +#include "gauss_blur_filter/gauss_blur_filter.h" +#include "mean_filter/mean_filter.h" +#include "median_filter/median_filter.h" +#include "motion_blur_filter/motion_blur_filter.h" +#include "sharpen_filter/sharpen_filter.h" + +filter_status_t filter_init_builtin(filter_t *filter, + const filter_request_t *request) { + filter_status_t status = FILTER_STATUS_UNSUPPORTED_KIND; + + if (!filter || !request) { + return FILTER_STATUS_NULL_POINTER; + } + + switch (request->kind) { + case FILTER_KIND_BLUR: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_blur_filter(filter, request->width, request->height); + break; + case FILTER_KIND_MEAN: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_mean_filter(filter, request->width, request->height); + break; + case FILTER_KIND_GAUSSIAN_BLUR: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_gauss_blur_filter(filter, request->width, request->height); + break; + case FILTER_KIND_MOTION_BLUR: + if (request->direction != FILTER_DIRECTION_NONE && + request->direction != FILTER_DIRECTION_DIAGONAL_45) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_motion_blur_filter(filter, request->width, request->height); + break; + case FILTER_KIND_EDGE_DETECT: + status = init_find_edges_filter( + filter, request->width, request->height, request->direction); + break; + case FILTER_KIND_SHARPEN: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_sharpen_filter(filter, request->width, request->height); + break; + case FILTER_KIND_EMBOSS: + status = init_emboss_filter( + filter, request->width, request->height, request->direction); + break; + case FILTER_KIND_MEDIAN: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_median_filter(filter, request->width, request->height); + break; + default: + return FILTER_STATUS_UNSUPPORTED_KIND; + } + + if (status == FILTER_STATUS_OK) { + filter->border_mode = request->border_mode; + } + + return status; +} diff --git a/src/filters/filter.h b/src/filters/filter.h new file mode 100644 index 0000000..d3b6350 --- /dev/null +++ b/src/filters/filter.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include + +typedef enum filter_status { + FILTER_STATUS_OK = 0, + FILTER_STATUS_NULL_POINTER, + FILTER_STATUS_INVALID_ARGUMENT, + FILTER_STATUS_UNSUPPORTED_SIZE, + FILTER_STATUS_UNSUPPORTED_DIRECTION, + FILTER_STATUS_UNSUPPORTED_KIND +} filter_status_t; + +typedef enum filter_category { + FILTER_CATEGORY_SMOOTHING = 0, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_CATEGORY_EDGE_ENHANCEMENT, +} filter_category_t; + +typedef enum filter_operator { + FILTER_OPERATOR_CONVOLUTION = 0, + FILTER_OPERATOR_RANK_SELECTION, +} filter_operator_t; + +typedef enum filter_kind { + FILTER_KIND_BLUR = 0, + FILTER_KIND_MEAN, + FILTER_KIND_GAUSSIAN_BLUR, + FILTER_KIND_MOTION_BLUR, + FILTER_KIND_EDGE_DETECT, + FILTER_KIND_SHARPEN, + FILTER_KIND_EMBOSS, + FILTER_KIND_MEDIAN, +} filter_kind_t; + +typedef enum filter_direction { + FILTER_DIRECTION_NONE = 0, + FILTER_DIRECTION_HORIZONTAL, + FILTER_DIRECTION_VERTICAL, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_DIRECTION_OMNIDIRECTIONAL, +} filter_direction_t; + +typedef enum filter_border_mode { + FILTER_BORDER_WRAP = 0, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, +} filter_border_mode_t; + +typedef struct filter_request { + filter_kind_t kind; + size_t width; + size_t height; + filter_direction_t direction; + filter_border_mode_t border_mode; +} filter_request_t; + +typedef struct filter { + filter_category_t category; + filter_operator_t filter_operator; + filter_kind_t kind; + filter_direction_t direction; + filter_border_mode_t border_mode; + const double *kernel; + double factor; + double bias; + size_t width; + size_t height; + size_t rank_index; +} filter_t; + +static inline bool filter_size_is_odd(size_t value) { + return value != 0U && (value % 2U) == 1U; +} + +static inline size_t filter_median_rank(size_t width, size_t height) { + return (width * height) / 2U; +} + +static inline filter_request_t +make_filter_request(filter_kind_t kind, size_t width, size_t height) { + return (filter_request_t){ + .kind = kind, + .width = width, + .height = height, + .direction = FILTER_DIRECTION_NONE, + .border_mode = FILTER_BORDER_WRAP, + }; +} + +static inline filter_t make_convolution_filter(filter_kind_t kind, + filter_category_t category, + filter_direction_t direction, + filter_border_mode_t border_mode, + const double *kernel, + double factor, + double bias, + size_t width, + size_t height) { + return (filter_t){ + .category = category, + .filter_operator = FILTER_OPERATOR_CONVOLUTION, + .kind = kind, + .direction = direction, + .border_mode = border_mode, + .kernel = kernel, + .factor = factor, + .bias = bias, + .width = width, + .height = height, + .rank_index = 0U, + }; +} + +static inline filter_t make_rank_filter(filter_kind_t kind, + filter_category_t category, + filter_direction_t direction, + filter_border_mode_t border_mode, + size_t width, + size_t height, + size_t rank_index) { + return (filter_t){ + .category = category, + .filter_operator = FILTER_OPERATOR_RANK_SELECTION, + .kind = kind, + .direction = direction, + .border_mode = border_mode, + .kernel = NULL, + .factor = 0.0, + .bias = 0.0, + .width = width, + .height = height, + .rank_index = rank_index, + }; +} + +static inline bool filter_is_convolution(const filter_t *filter) { + return filter != NULL && + filter->filter_operator == FILTER_OPERATOR_CONVOLUTION; +} + +static inline bool filter_is_rank_selection(const filter_t *filter) { + return filter != NULL && + filter->filter_operator == FILTER_OPERATOR_RANK_SELECTION; +} + +static inline bool filter_has_explicit_kernel(const filter_t *filter) { + return filter_is_convolution(filter) && filter->kernel != NULL; +} + +filter_status_t filter_init_builtin(filter_t *filter, + const filter_request_t *request); diff --git a/src/filters/find_edges_filter/find_edges_filter.c b/src/filters/find_edges_filter/find_edges_filter.c new file mode 100644 index 0000000..7e79b31 --- /dev/null +++ b/src/filters/find_edges_filter/find_edges_filter.c @@ -0,0 +1,121 @@ +#include "find_edges_filter.h" + +// clang-format off +static const double find_edge_kernel_horizontal[] = { + 0, 0, -1, 0, 0, + 0, 0, -1, 0, 0, + 0, 0, 2, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, +}; + +static const double find_edge_horizontal_factor = 1.0; +static const double find_edge_horizontal_bias = 0.0; + +static const double find_edge_kernel_vertical[] = { + 0, 0, -1, 0, 0, + 0, 0, -1, 0, 0, + 0, 0, 4, 0, 0, + 0, 0, -1, 0, 0, + 0, 0, -1, 0, 0, +}; + +static const double find_edge_vertical_factor = 1.0; +static const double find_edge_vertical_bias = 0.0; + +static const double find_edge_kernel_diagonal45deg[] = { + -1, 0, 0, 0, 0, + 0, -2, 0, 0, 0, + 0, 0, 6, 0, 0, + 0, 0, 0, -2, 0, + 0, 0, 0, 0, -1, +}; + +static const double find_edge_diagonal45deg_factor = 1.0; +static const double find_edge_diagonal45deg_bias = 0.0; + + +static const double find_edge_kernel_any_direction[] = { + -1, -1, -1, + -1, 8, -1, + -1, -1, -1 +}; + +static const double find_edge_any_direction_factor = 1.0; +static const double find_edge_any_direction_bias = 0.0; + +// clang-format on + +filter_status_t init_find_edges_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (direction == FILTER_DIRECTION_NONE) { + direction = FILTER_DIRECTION_OMNIDIRECTIONAL; + } + + switch (direction) { + case FILTER_DIRECTION_HORIZONTAL: + if (width != 5 || height != 5) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_HORIZONTAL, + FILTER_BORDER_WRAP, + find_edge_kernel_horizontal, + find_edge_horizontal_factor, + find_edge_horizontal_bias, + width, + height); + return FILTER_STATUS_OK; + case FILTER_DIRECTION_VERTICAL: + if (width != 5 || height != 5) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_VERTICAL, + FILTER_BORDER_WRAP, + find_edge_kernel_vertical, + find_edge_vertical_factor, + find_edge_vertical_bias, + width, + height); + return FILTER_STATUS_OK; + case FILTER_DIRECTION_DIAGONAL_45: + if (width != 5 || height != 5) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + find_edge_kernel_diagonal45deg, + find_edge_diagonal45deg_factor, + find_edge_diagonal45deg_bias, + width, + height); + return FILTER_STATUS_OK; + case FILTER_DIRECTION_OMNIDIRECTIONAL: + if (width != 3 || height != 3) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_OMNIDIRECTIONAL, + FILTER_BORDER_WRAP, + find_edge_kernel_any_direction, + find_edge_any_direction_factor, + find_edge_any_direction_bias, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } +} diff --git a/src/filters/find_edges_filter/find_edges_filter.h b/src/filters/find_edges_filter/find_edges_filter.h new file mode 100644 index 0000000..b0866d8 --- /dev/null +++ b/src/filters/find_edges_filter/find_edges_filter.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +filter_status_t init_find_edges_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction); diff --git a/src/filters/gauss_blur_filter/gauss_blur_filter.c b/src/filters/gauss_blur_filter/gauss_blur_filter.c new file mode 100644 index 0000000..2f16b79 --- /dev/null +++ b/src/filters/gauss_blur_filter/gauss_blur_filter.c @@ -0,0 +1,59 @@ +#include "gauss_blur_filter.h" + +// clang-format off +static const double gauss_blur_kernel_3x3[] = { + 1, 2, 1, + 2, 4, 2, + 1, 2, 1, +}; + +static const double blur_factor_3x3 = 1.0 / 16.0; +static const double blur_bias_3x3 = 0.0; + +static const double gauss_blur_kernel_5x5[] = { + 1, 4, 6, 4, 1, + 4, 16, 24, 16, 4, + 6, 24, 36, 24, 6, + 4, 16, 24, 16, 4, + 1, 4, 6, 4, 1, +}; + +static const double blur_factor_5x5 = 1.0 / 256.0; +static const double blur_bias_5x5 = 0.0; +// clang-format on + +filter_status_t +init_gauss_blur_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_GAUSSIAN_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + gauss_blur_kernel_3x3, + blur_factor_3x3, + blur_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_GAUSSIAN_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + gauss_blur_kernel_5x5, + blur_factor_5x5, + blur_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/gauss_blur_filter/gauss_blur_filter.h b/src/filters/gauss_blur_filter/gauss_blur_filter.h new file mode 100644 index 0000000..9d61e35 --- /dev/null +++ b/src/filters/gauss_blur_filter/gauss_blur_filter.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +filter_status_t +init_gauss_blur_filter(filter_t *filter, size_t width, size_t height); diff --git a/src/filters/mean_filter/mean_filter.c b/src/filters/mean_filter/mean_filter.c new file mode 100644 index 0000000..0df228f --- /dev/null +++ b/src/filters/mean_filter/mean_filter.c @@ -0,0 +1,60 @@ +#include "mean_filter.h" + +// clang-format off +static const double mean_kernel_3x3[] = { + 1, 1, 1, + 1, 1, 1, + 1, 1, 1, +}; + +static const double mean_factor_3x3 = 1.0 / 9.0; +static const double mean_bias_3x3 = 0.0; + +static const double mean_kernel_5x5[] = { + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, +}; + +static const double mean_factor_5x5 = 1.0 / 25.0; +static const double mean_bias_5x5 = 0.0; +// clang-format on + +filter_status_t init_mean_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_MEAN, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + mean_kernel_3x3, + mean_factor_3x3, + mean_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_MEAN, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + mean_kernel_5x5, + mean_factor_5x5, + mean_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/mean_filter/mean_filter.h b/src/filters/mean_filter/mean_filter.h new file mode 100644 index 0000000..5492c79 --- /dev/null +++ b/src/filters/mean_filter/mean_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_mean_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/filters/median_filter/median_filter.c b/src/filters/median_filter/median_filter.c new file mode 100644 index 0000000..b28ebac --- /dev/null +++ b/src/filters/median_filter/median_filter.c @@ -0,0 +1,22 @@ +#include "median_filter.h" + +filter_status_t +init_median_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (!filter_size_is_odd(width) || !filter_size_is_odd(height)) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + *filter = make_rank_filter(FILTER_KIND_MEDIAN, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + width, + height, + filter_median_rank(width, height)); + + return FILTER_STATUS_OK; +} diff --git a/src/filters/median_filter/median_filter.h b/src/filters/median_filter/median_filter.h new file mode 100644 index 0000000..11e313e --- /dev/null +++ b/src/filters/median_filter/median_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_median_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/filters/motion_blur_filter/motion_blur_filter.c b/src/filters/motion_blur_filter/motion_blur_filter.c new file mode 100644 index 0000000..2f8017a --- /dev/null +++ b/src/filters/motion_blur_filter/motion_blur_filter.c @@ -0,0 +1,44 @@ +#include "motion_blur_filter.h" + +// clang-format off + +static const double blur_kernel_9x9[] = { + 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, +}; + +static const double blur_factor_9x9 = 1.0 / 9.0; +static const double blur_bias = 0.0; +// clang-format on + +filter_status_t +init_motion_blur_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + switch (width) { + case 9: + *filter = make_convolution_filter(FILTER_KIND_MOTION_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + blur_kernel_9x9, + blur_factor_9x9, + blur_bias, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/motion_blur_filter/motion_blur_filter.h b/src/filters/motion_blur_filter/motion_blur_filter.h new file mode 100644 index 0000000..62a3426 --- /dev/null +++ b/src/filters/motion_blur_filter/motion_blur_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_motion_blur_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/filters/sharpen_filter/sharpen_filter.c b/src/filters/sharpen_filter/sharpen_filter.c new file mode 100644 index 0000000..8acf2c9 --- /dev/null +++ b/src/filters/sharpen_filter/sharpen_filter.c @@ -0,0 +1,61 @@ +#include "sharpen_filter.h" + +// clang-format off +static const double sharpen_kernel_3x3[] = { + -1, -1, -1, + -1, 9, -1, + -1, -1, -1, +}; + +static const double sharpen_factor_3x3 = 1.0; +static const double sharpen_bias_3x3 = 0.0; + +static const double sharpen_kernel_5x5[] = { + -1, -1, -1, -1, -1, + -1, 2, 2, 2, -1, + -1, 2, 8, 2, -1, + -1, 2, 2, 2, -1, + -1, -1, -1, -1, -1, +}; + +static const double sharpen_factor_5x5 = 1.0 / 8.0; +static const double sharpen_bias_5x5 = 0.0; +// clang-format on + +filter_status_t +init_sharpen_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_SHARPEN, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + sharpen_kernel_3x3, + sharpen_factor_3x3, + sharpen_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_SHARPEN, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + sharpen_kernel_5x5, + sharpen_factor_5x5, + sharpen_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/sharpen_filter/sharpen_filter.h b/src/filters/sharpen_filter/sharpen_filter.h new file mode 100644 index 0000000..61f40c2 --- /dev/null +++ b/src/filters/sharpen_filter/sharpen_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_sharpen_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/image_helpers/image_helpers.h b/src/image_helpers/image_helpers.h new file mode 100644 index 0000000..5b6e3aa --- /dev/null +++ b/src/image_helpers/image_helpers.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +typedef struct image_view { + unsigned char *data; + size_t height; + size_t width; + size_t stride; + size_t channels; +} image_view_t; diff --git a/src/parallel_convolution/parallel_convolution.c b/src/parallel_convolution/parallel_convolution.c new file mode 100644 index 0000000..e90840b --- /dev/null +++ b/src/parallel_convolution/parallel_convolution.c @@ -0,0 +1,109 @@ +#include "parallel_convolution.h" + +#include +#include + +#define TILE_HEIGHT 32U +#define TILE_WIDTH 128U + +static size_t min_size(size_t lhs, size_t rhs) { + return lhs < rhs ? lhs : rhs; +} + +static int rows_executor(const convolution_context_t *context) { + int failed = 0; + +#pragma omp parallel reduction(| : failed) + { + const size_t thread_index = (size_t)omp_get_thread_num(); + const size_t thread_count = (size_t)omp_get_num_threads(); + + const size_t first_row = thread_index * context->height / thread_count; + const size_t last_row = + (thread_index + 1U) * context->height / thread_count; + + if (convolution_apply_rows(context, first_row, last_row) != 0) { + failed = 1; + } + } + + return failed ? -1 : 0; +} + +static int cols_executor(const convolution_context_t *context) { + int failed = 0; + +#pragma omp parallel reduction(| : failed) + { + const size_t thread_index = (size_t)omp_get_thread_num(); + const size_t thread_count = (size_t)omp_get_num_threads(); + + const size_t first_col = thread_index * context->width / thread_count; + const size_t last_col = (thread_index + 1U) * context->width / thread_count; + + if (convolution_apply_cols(context, first_col, last_col) != 0) { + failed = 1; + } + } + + return failed ? -1 : 0; +} + +static int pixels_executor(const convolution_context_t *context) { + int failed = 0; + const size_t pixel_count = context->height * context->width; + +#pragma omp parallel for schedule(static) reduction(| : failed) + for (size_t pixel_index = 0; pixel_index < pixel_count; ++pixel_index) { + const size_t y = pixel_index / context->width; + const size_t x = pixel_index % context->width; + + if (convolution_apply_pixel(context, y, x) != 0) { + failed = 1; + } + } + + return failed ? -1 : 0; +} + +static int tiles_executor(const convolution_context_t *context) { + int failed = 0; + +#pragma omp parallel for collapse(2) schedule(static) reduction(| : failed) + for (size_t first_row = 0; first_row < context->height; + first_row += TILE_HEIGHT) { + for (size_t first_col = 0; first_col < context->width; + first_col += TILE_WIDTH) { + const size_t last_row = + min_size(first_row + TILE_HEIGHT, context->height); + const size_t last_col = min_size(first_col + TILE_WIDTH, context->width); + + if (convolution_apply_rect( + context, first_row, last_row, first_col, last_col) != 0) { + failed = 1; + } + } + } + + return failed ? -1 : 0; +} + +int parallel_convolution_rows(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, rows_executor); +} + +int parallel_convolution_cols(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, cols_executor); +} + +int parallel_convolution_pixels(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, pixels_executor); +} + +int parallel_convolution_rectangle(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, tiles_executor); +} diff --git a/src/parallel_convolution/parallel_convolution.h b/src/parallel_convolution/parallel_convolution.h new file mode 100644 index 0000000..68c994b --- /dev/null +++ b/src/parallel_convolution/parallel_convolution.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + +int parallel_convolution_rows(const filter_t *filter, image_view_t *image_view); + +int parallel_convolution_cols(const filter_t *filter, image_view_t *image_view); + +int parallel_convolution_pixels(const filter_t *filter, + image_view_t *image_view); + +int parallel_convolution_rectangle(const filter_t *filter, + image_view_t *image_view); diff --git a/src/pipeline/image_pipeline.h b/src/pipeline/image_pipeline.h new file mode 100644 index 0000000..c85fc91 --- /dev/null +++ b/src/pipeline/image_pipeline.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#define IMAGE_PIPELINE_MAX_FILTERS 2U + +typedef int (*image_convolution_runner_t)(const filter_t *filter, + image_view_t *image_view); + +typedef struct image_pipeline_request { + const char *input_path; + const char *output_path; + + filter_request_t filters[IMAGE_PIPELINE_MAX_FILTERS]; + size_t filter_count; + + image_convolution_runner_t run_convolution; +} image_pipeline_request_t; \ No newline at end of file diff --git a/src/pipeline/image_pipeline_runner.c b/src/pipeline/image_pipeline_runner.c new file mode 100644 index 0000000..e3c7ff6 --- /dev/null +++ b/src/pipeline/image_pipeline_runner.c @@ -0,0 +1,257 @@ +#include "image_pipeline_runner.h" + +#include +#include +#include +#include + +#include +#include + +#include + +#define PIPELINE_WORKER_COUNT 3U +#define PIPELINE_MAX_ACTIVE_READERS 1U +#define PIPELINE_MAX_ACTIVE_COMPUTERS 1U +#define PIPELINE_MAX_ACTIVE_WRITERS 1U + +typedef enum image_job_status { + IMAGE_JOB_OK = 0, + IMAGE_JOB_READ_FAILED, + IMAGE_JOB_COMPUTE_FAILED, + IMAGE_JOB_WRITE_FAILED, + IMAGE_JOB_ENQUEUE_FAILED, +} image_job_status_t; + +typedef struct image_pipeline { + thread_pool_t pool; + atomic_bool failed; +} image_pipeline_t; + +typedef struct image_job { + image_pipeline_t *pipeline; + image_pipeline_request_t request; + + IplImage *image; + image_view_t image_view; + + image_job_status_t status; +} image_job_t; + +static void read_image_task(void *args); +static void compute_image_task(void *args); +static void write_image_task(void *args); + +static void pipeline_mark_failed(image_pipeline_t *pipeline) { + if (pipeline != NULL) { + atomic_store(&pipeline->failed, true); + } +} + +static void image_job_destroy(image_job_t *job) { + if (job == NULL) { + return; + } + + if (job->image != NULL) { + cvReleaseImage(&job->image); + } + + free(job); +} + +static bool pipeline_request_is_valid(const image_pipeline_request_t *request) { + return request != NULL && request->input_path != NULL && + request->output_path != NULL && request->filter_count != 0U && + request->filter_count <= IMAGE_PIPELINE_MAX_FILTERS && + request->run_convolution != NULL; +} + +static image_job_t *image_job_create(image_pipeline_t *pipeline, + const image_pipeline_request_t *request) { + if (pipeline == NULL || !pipeline_request_is_valid(request)) { + return NULL; + } + + image_job_t *job = (image_job_t *)calloc(1U, sizeof(*job)); + if (job == NULL) { + return NULL; + } + + job->pipeline = pipeline; + job->request = *request; + job->status = IMAGE_JOB_OK; + return job; +} + +static bool +enqueue_job_task(image_job_t *job, task_type_t task_type, task_fn_t task_fn) { + if (job == NULL || job->pipeline == NULL || task_fn == NULL) { + return false; + } + + task_t task = { + .fn = task_fn, + .fn_type = task_type, + .args = job, + }; + + return thread_pool_enqueue(&job->pipeline->pool, &task); +} + +static void cleanup_after_enqueue_failure(image_job_t *job, + const char *stage_name) { + if (job == NULL) { + return; + } + + if (job->status == IMAGE_JOB_OK) { + job->status = IMAGE_JOB_ENQUEUE_FAILED; + } + pipeline_mark_failed(job->pipeline); + + if (stage_name != NULL) { + fprintf(stderr, + "failed to enqueue %s task: %s\n", + stage_name, + job->request.input_path != NULL ? job->request.input_path : ""); + } + + image_job_destroy(job); +} + +static void enqueue_compute_or_cleanup(image_job_t *job) { + if (!enqueue_job_task(job, COMPUTE_TASK, compute_image_task)) { + cleanup_after_enqueue_failure(job, "compute"); + } +} + +static void enqueue_writer_or_cleanup(image_job_t *job) { + if (!enqueue_job_task(job, WRITER_TASK, write_image_task)) { + cleanup_after_enqueue_failure(job, "writer"); + } +} + +static int apply_filters(const image_pipeline_request_t *request, + image_view_t *image_view) { + if (request == NULL || image_view == NULL || + request->run_convolution == NULL) { + return -1; + } + + for (size_t i = 0; i < request->filter_count; ++i) { + filter_t filter; + + if (filter_init_builtin(&filter, &request->filters[i]) != + FILTER_STATUS_OK) { + return -1; + } + + if (!filter_is_convolution(&filter) || + request->run_convolution(&filter, image_view) != 0) { + return -1; + } + } + + return 0; +} + +static void read_image_task(void *args) { + image_job_t *job = args; + if (job == NULL) { + return; + } + + job->image = cvLoadImage(job->request.input_path, CV_LOAD_IMAGE_UNCHANGED); + if (job->image == NULL) { + job->status = IMAGE_JOB_READ_FAILED; + pipeline_mark_failed(job->pipeline); + fprintf(stderr, "failed to load image: %s\n", job->request.input_path); + enqueue_writer_or_cleanup(job); + return; + } + + job->image_view = (image_view_t){ + .data = (unsigned char *)job->image->imageData, + .height = (size_t)job->image->height, + .width = (size_t)job->image->width, + .stride = (size_t)job->image->widthStep, + .channels = (size_t)job->image->nChannels, + }; + + enqueue_compute_or_cleanup(job); +} + +static void compute_image_task(void *args) { + image_job_t *job = args; + if (job == NULL) { + return; + } + + if (job->status == IMAGE_JOB_OK && + apply_filters(&job->request, &job->image_view) != 0) { + job->status = IMAGE_JOB_COMPUTE_FAILED; + pipeline_mark_failed(job->pipeline); + fprintf(stderr, "failed to apply filters: %s\n", job->request.input_path); + } + + enqueue_writer_or_cleanup(job); +} + +static void write_image_task(void *args) { + image_job_t *job = args; + if (job == NULL) { + return; + } + + if (job->status == IMAGE_JOB_OK) { + if (!cvSaveImage(job->request.output_path, job->image, NULL)) { + job->status = IMAGE_JOB_WRITE_FAILED; + pipeline_mark_failed(job->pipeline); + fprintf(stderr, "failed to save image: %s\n", job->request.output_path); + } + } + + image_job_destroy(job); +} + +int image_pipeline_run(const image_pipeline_request_t *requests, + size_t request_count) { + if (requests == NULL || request_count == 0U) { + return -1; + } + + image_pipeline_t pipeline; + atomic_init(&pipeline.failed, false); + + const thread_pool_config_t config = { + .worker_count = PIPELINE_WORKER_COUNT, + .reader_queue_capacity = request_count, + .writer_queue_capacity = request_count, + .computer_queue_capacity = request_count, + .max_active_readers = PIPELINE_MAX_ACTIVE_READERS, + .max_active_writers = PIPELINE_MAX_ACTIVE_WRITERS, + .max_active_computers = PIPELINE_MAX_ACTIVE_COMPUTERS, + }; + + if (!thread_pool_init(&pipeline.pool, &config)) { + return -1; + } + + for (size_t i = 0; i < request_count; ++i) { + image_job_t *job = image_job_create(&pipeline, &requests[i]); + if (job == NULL) { + pipeline_mark_failed(&pipeline); + continue; + } + + if (!enqueue_job_task(job, READER_TASK, read_image_task)) { + cleanup_after_enqueue_failure(job, "reader"); + } + } + + thread_pool_wait(&pipeline.pool); + thread_pool_destroy(&pipeline.pool); + + return !atomic_load(&pipeline.failed) ? 0 : -1; +} \ No newline at end of file diff --git a/src/pipeline/image_pipeline_runner.h b/src/pipeline/image_pipeline_runner.h new file mode 100644 index 0000000..3e727db --- /dev/null +++ b/src/pipeline/image_pipeline_runner.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "image_pipeline.h" + +int image_pipeline_run(const image_pipeline_request_t *requests, + size_t request_count); \ No newline at end of file diff --git a/src/runtime/task_queue.c b/src/runtime/task_queue.c new file mode 100644 index 0000000..847b751 --- /dev/null +++ b/src/runtime/task_queue.c @@ -0,0 +1,56 @@ +#include "task_queue.h" +#include + +bool task_queue_init(task_queue_t *queue, size_t capacity) { + if (capacity == 0 || !queue) { + return false; + } + queue->ring_buffer = (task_t *)malloc(sizeof(task_t) * capacity); + if (!queue->ring_buffer) { + return false; + } + queue->capacity = capacity; + queue->count = 0; + queue->head = 0; + queue->tail = 0; + return true; +} + +void task_queue_destroy(task_queue_t *queue) { + if (!queue) { + return; + } + free(queue->ring_buffer); + queue->ring_buffer = NULL; + queue->capacity = 0; + queue->count = 0; + queue->head = 0; + queue->tail = 0; +} + +bool task_queue_try_push(task_queue_t *queue, const task_t *task) { + if (!queue || !queue->ring_buffer || !task) { + return false; + } + + if (queue->count == queue->capacity) { + return false; + } + queue->ring_buffer[queue->head] = *task; + queue->head = (queue->head + 1) % queue->capacity; + ++queue->count; + return true; +} + +bool task_queue_try_pop(task_queue_t *queue, task_t *out_task) { + if (!queue || !queue->ring_buffer || !out_task) { + return false; + } + if (queue->count == 0) { + return false; + } + *out_task = queue->ring_buffer[queue->tail]; + queue->tail = (queue->tail + 1) % queue->capacity; + --queue->count; + return true; +} diff --git a/src/runtime/task_queue.h b/src/runtime/task_queue.h new file mode 100644 index 0000000..73a5951 --- /dev/null +++ b/src/runtime/task_queue.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +typedef enum task_type { + READER_TASK = 1, + COMPUTE_TASK, + WRITER_TASK, +} task_type_t; + +typedef void (*task_fn_t)(void *args); + +typedef struct task { + task_fn_t fn; + task_type_t fn_type; + void *args; +} task_t; + +typedef struct _task_queue { + + task_t *ring_buffer; + size_t capacity; + + size_t head; + size_t tail; + + size_t count; + +} task_queue_t; + +bool task_queue_init(task_queue_t *queue, size_t capacity); + +void task_queue_destroy(task_queue_t *queue); + +bool task_queue_try_pop(task_queue_t *queue, task_t *out_task); + +bool task_queue_try_push(task_queue_t *queue, const task_t *task); diff --git a/src/runtime/thread_pool.c b/src/runtime/thread_pool.c new file mode 100644 index 0000000..7064585 --- /dev/null +++ b/src/runtime/thread_pool.c @@ -0,0 +1,289 @@ +#include "thread_pool.h" + +#include + +static bool has_runnable_tasks(const thread_pool_t *pool) { + return (pool->writers_queue.count != 0U && + pool->active_writers < pool->max_active_writers) || + (pool->computers_queue.count != 0U && + pool->active_computers < pool->max_active_computers) || + (pool->readers_queue.count != 0U && + pool->active_readers < pool->max_active_readers); +} + +static bool has_queued_tasks(const thread_pool_t *pool) { + return pool->readers_queue.count != 0U || pool->writers_queue.count != 0U || + pool->computers_queue.count != 0U; +} + +static void destroy_sync_objects(thread_pool_t *pool) { + pthread_cond_destroy(&pool->all_done); + pthread_cond_destroy(&pool->has_space); + pthread_cond_destroy(&pool->has_work); + pthread_mutex_destroy(&pool->pool_mutex); +} + +static void *worker_main(void *args); + +bool thread_pool_init(thread_pool_t *pool, const thread_pool_config_t *config) { + if (pool == NULL || config == NULL || config->worker_count == 0U || + config->reader_queue_capacity == 0U || + config->writer_queue_capacity == 0U || + config->computer_queue_capacity == 0U || + config->max_active_readers == 0U || config->max_active_writers == 0U || + config->max_active_computers == 0U) { + return false; + } + + pool->workers = NULL; + pool->worker_count = 0U; + pool->active_tasks = 0U; + pool->active_readers = 0U; + pool->active_writers = 0U; + pool->active_computers = 0U; + pool->max_active_readers = config->max_active_readers; + pool->max_active_writers = config->max_active_writers; + pool->max_active_computers = config->max_active_computers; + pool->stopping = false; + + if (!task_queue_init(&pool->readers_queue, config->reader_queue_capacity)) { + return false; + } + if (!task_queue_init(&pool->writers_queue, config->writer_queue_capacity)) { + task_queue_destroy(&pool->readers_queue); + return false; + } + if (!task_queue_init(&pool->computers_queue, + config->computer_queue_capacity)) { + task_queue_destroy(&pool->writers_queue); + task_queue_destroy(&pool->readers_queue); + return false; + } + if (pthread_mutex_init(&pool->pool_mutex, NULL) != 0) { + task_queue_destroy(&pool->computers_queue); + task_queue_destroy(&pool->writers_queue); + task_queue_destroy(&pool->readers_queue); + return false; + } + if (pthread_cond_init(&pool->has_work, NULL) != 0) { + pthread_mutex_destroy(&pool->pool_mutex); + task_queue_destroy(&pool->computers_queue); + task_queue_destroy(&pool->writers_queue); + task_queue_destroy(&pool->readers_queue); + return false; + } + if (pthread_cond_init(&pool->has_space, NULL) != 0) { + pthread_cond_destroy(&pool->has_work); + pthread_mutex_destroy(&pool->pool_mutex); + task_queue_destroy(&pool->computers_queue); + task_queue_destroy(&pool->writers_queue); + task_queue_destroy(&pool->readers_queue); + return false; + } + if (pthread_cond_init(&pool->all_done, NULL) != 0) { + pthread_cond_destroy(&pool->has_space); + pthread_cond_destroy(&pool->has_work); + pthread_mutex_destroy(&pool->pool_mutex); + task_queue_destroy(&pool->computers_queue); + task_queue_destroy(&pool->writers_queue); + task_queue_destroy(&pool->readers_queue); + return false; + } + + pool->workers = malloc(sizeof(*pool->workers) * config->worker_count); + if (pool->workers == NULL) { + destroy_sync_objects(pool); + task_queue_destroy(&pool->computers_queue); + task_queue_destroy(&pool->writers_queue); + task_queue_destroy(&pool->readers_queue); + return false; + } + + for (size_t i = 0; i < config->worker_count; ++i) { + if (pthread_create(&pool->workers[i], NULL, worker_main, pool) != 0) { + thread_pool_destroy(pool); + return false; + } + ++pool->worker_count; + } + + return true; +} + +bool thread_pool_queue_for_task(const task_t *task, + task_queue_t **q, + thread_pool_t *pool) { + if (task == NULL || q == NULL || pool == NULL || task->fn == NULL) { + return false; + } + if (task->fn_type == WRITER_TASK) { + *q = &pool->writers_queue; + return true; + } + if (task->fn_type == COMPUTE_TASK) { + *q = &pool->computers_queue; + return true; + } + if (task->fn_type == READER_TASK) { + *q = &pool->readers_queue; + return true; + } + return false; +} + +bool thread_pool_has_pending_tasks(thread_pool_t *pool) { + return pool != NULL && (has_queued_tasks(pool) || pool->active_tasks != 0U); +} + +bool thread_pool_enqueue(thread_pool_t *pool, const task_t *task) { + task_queue_t *q = NULL; + if (!thread_pool_queue_for_task(task, &q, pool)) { + return false; + } + + pthread_mutex_lock(&pool->pool_mutex); + while (q->count == q->capacity && !pool->stopping) { + pthread_cond_wait(&pool->has_space, &pool->pool_mutex); + } + if (pool->stopping) { + pthread_mutex_unlock(&pool->pool_mutex); + return false; + } + + const bool pushed = task_queue_try_push(q, task); + if (pushed) { + pthread_cond_signal(&pool->has_work); + } + pthread_mutex_unlock(&pool->pool_mutex); + return pushed; +} + +bool thread_pool_try_enqueue(thread_pool_t *pool, const task_t *task) { + task_queue_t *q = NULL; + if (!thread_pool_queue_for_task(task, &q, pool)) { + return false; + } + + pthread_mutex_lock(&pool->pool_mutex); + if (pool->stopping || q->count == q->capacity) { + pthread_mutex_unlock(&pool->pool_mutex); + return false; + } + + const bool pushed = task_queue_try_push(q, task); + if (pushed) { + pthread_cond_signal(&pool->has_work); + } + pthread_mutex_unlock(&pool->pool_mutex); + return pushed; +} + +bool pool_thread_pop_task(thread_pool_t *pool, task_t *out_task) { + if (pool == NULL || out_task == NULL) { + return false; + } + if ((pool->active_writers < pool->max_active_writers) && + task_queue_try_pop(&pool->writers_queue, out_task)) { + return true; + } + if ((pool->active_computers < pool->max_active_computers) && + task_queue_try_pop(&pool->computers_queue, out_task)) { + return true; + } + return ((pool->active_readers < pool->max_active_readers) && + task_queue_try_pop(&pool->readers_queue, out_task)); +} + +static void *worker_main(void *args) { + thread_pool_t *pool = args; + + for (;;) { + task_t task; + + pthread_mutex_lock(&pool->pool_mutex); + while (!has_runnable_tasks(pool) && !pool->stopping) { + pthread_cond_wait(&pool->has_work, &pool->pool_mutex); + } + if (pool->stopping && !has_queued_tasks(pool)) { + pthread_mutex_unlock(&pool->pool_mutex); + return NULL; + } + + if (!pool_thread_pop_task(pool, &task)) { + pthread_mutex_unlock(&pool->pool_mutex); + continue; + } + + ++pool->active_tasks; + if (task.fn_type == READER_TASK) { + ++pool->active_readers; + } else if (task.fn_type == WRITER_TASK) { + ++pool->active_writers; + } else { + ++pool->active_computers; + } + pthread_cond_signal(&pool->has_space); + pthread_mutex_unlock(&pool->pool_mutex); + + task.fn(task.args); + + pthread_mutex_lock(&pool->pool_mutex); + --pool->active_tasks; + if (task.fn_type == READER_TASK) { + --pool->active_readers; + } else if (task.fn_type == WRITER_TASK) { + --pool->active_writers; + } else { + --pool->active_computers; + } + pthread_cond_broadcast(&pool->has_work); + if (!thread_pool_has_pending_tasks(pool)) { + pthread_cond_broadcast(&pool->all_done); + } + pthread_mutex_unlock(&pool->pool_mutex); + } +} + +void thread_pool_wait(thread_pool_t *pool) { + if (pool == NULL) { + return; + } + + pthread_mutex_lock(&pool->pool_mutex); + while (thread_pool_has_pending_tasks(pool)) { + pthread_cond_wait(&pool->all_done, &pool->pool_mutex); + } + pthread_mutex_unlock(&pool->pool_mutex); +} + +void thread_pool_shutdown(thread_pool_t *pool) { + if (pool == NULL) { + return; + } + + pthread_mutex_lock(&pool->pool_mutex); + pool->stopping = true; + pthread_cond_broadcast(&pool->has_work); + pthread_cond_broadcast(&pool->has_space); + pthread_mutex_unlock(&pool->pool_mutex); +} + +void thread_pool_destroy(thread_pool_t *pool) { + if (pool == NULL) { + return; + } + + thread_pool_shutdown(pool); + for (size_t i = 0; i < pool->worker_count; ++i) { + pthread_join(pool->workers[i], NULL); + } + + free(pool->workers); + pool->workers = NULL; + pool->worker_count = 0U; + + destroy_sync_objects(pool); + task_queue_destroy(&pool->readers_queue); + task_queue_destroy(&pool->writers_queue); + task_queue_destroy(&pool->computers_queue); +} diff --git a/src/runtime/thread_pool.h b/src/runtime/thread_pool.h new file mode 100644 index 0000000..ad44d12 --- /dev/null +++ b/src/runtime/thread_pool.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include "task_queue.h" + +typedef struct thread_pool_config { + size_t worker_count; + + size_t reader_queue_capacity; + size_t writer_queue_capacity; + size_t computer_queue_capacity; + + size_t max_active_readers; + size_t max_active_writers; + size_t max_active_computers; + +} thread_pool_config_t; +typedef struct _thread_pool { + + pthread_t *workers; + + task_queue_t readers_queue; + task_queue_t writers_queue; + task_queue_t computers_queue; + + pthread_cond_t has_work; + pthread_cond_t has_space; + pthread_cond_t all_done; + + pthread_mutex_t pool_mutex; + + size_t active_tasks; + + size_t active_readers; + size_t active_writers; + size_t active_computers; + + size_t max_active_readers; + size_t max_active_writers; + size_t max_active_computers; + + size_t worker_count; + bool stopping; + +} thread_pool_t; + +bool thread_pool_enqueue(thread_pool_t *pool, const task_t *task); + +bool thread_pool_has_pending_tasks(thread_pool_t *pool); + +bool thread_pool_try_enqueue(thread_pool_t *pool, const task_t *task); + +bool thread_pool_queue_for_task(const task_t *task, + task_queue_t **q, + thread_pool_t *pool); + +bool thread_pool_init(thread_pool_t *pool, const thread_pool_config_t *config); + +bool pool_thread_pop_task(thread_pool_t *pool, task_t *out_task); + +void thread_pool_wait(thread_pool_t *pool); + +void thread_pool_destroy(thread_pool_t *pool); + +void thread_pool_shutdown(thread_pool_t *pool); diff --git a/src/sequentially_convolution/sequentially_convolution.c b/src/sequentially_convolution/sequentially_convolution.c new file mode 100644 index 0000000..2f5ca09 --- /dev/null +++ b/src/sequentially_convolution/sequentially_convolution.c @@ -0,0 +1,12 @@ +#include "sequentially_convolution.h" + +#include + +static int sequential_executor(const convolution_context_t *context) { + return convolution_apply_rect( + context, 0U, context->height, 0U, context->width); +} + +int sequential_convolution(const filter_t *filter, image_view_t *image_view) { + return convolution_run(filter, image_view, sequential_executor); +} diff --git a/src/sequentially_convolution/sequentially_convolution.h b/src/sequentially_convolution/sequentially_convolution.h new file mode 100644 index 0000000..d4731bb --- /dev/null +++ b/src/sequentially_convolution/sequentially_convolution.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include + +int sequential_convolution(const filter_t *filter, image_view_t *image_view); diff --git a/test/sequential_tests/CMakeLists.txt b/test/sequential_tests/CMakeLists.txt new file mode 100644 index 0000000..d5d04b7 --- /dev/null +++ b/test/sequential_tests/CMakeLists.txt @@ -0,0 +1,23 @@ +add_executable(sequential_tests + sequential_tests.c +) + +target_link_libraries(sequential_tests + PRIVATE + convolution_runtime + PkgConfig::CMOCKA +) + +add_test(NAME sequential_tests COMMAND sequential_tests) + +add_executable(sequential_opencv_reference_tests + sequential_opencv_reference_tests.c +) + +target_link_libraries(sequential_opencv_reference_tests + PRIVATE + convolution_runtime + PkgConfig::CMOCKA +) + +add_test(NAME sequential_opencv_reference_tests COMMAND sequential_opencv_reference_tests) diff --git a/test/sequential_tests/sequential_opencv_reference_tests.c b/test/sequential_tests/sequential_opencv_reference_tests.c new file mode 100644 index 0000000..be2b8d5 --- /dev/null +++ b/test/sequential_tests/sequential_opencv_reference_tests.c @@ -0,0 +1,419 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_IMAGE_BYTES 2048U +#define TEST_NAME_SIZE 128U +#define RANDOM_PIXEL_MASK 63U +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +typedef struct test_image { + unsigned char data[MAX_IMAGE_BYTES]; + image_view_t view; + size_t size; +} test_image_t; + +typedef struct kernel_sample { + const char *name; + const double *values; + size_t width; + size_t height; + double factor; + double bias; +} kernel_sample_t; + +static uint32_t random_state = 0x12345678U; + +static unsigned char random_byte(void) { + random_state = random_state * 1664525U + 1013904223U; + return (unsigned char)(random_state >> 24U); +} + +static void init_image(test_image_t *image, + size_t width, + size_t height, + size_t channels, + size_t padding) { + const size_t stride = width * channels + padding; + + image->size = height * stride; + image->view = (image_view_t){ + .data = image->data, + .height = height, + .width = width, + .stride = stride, + .channels = channels, + }; + + memset(image->data, 0, sizeof(image->data)); +} + +static unsigned char *pixel(test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static const unsigned char * +const_pixel(const test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static void fill_random(test_image_t *image) { + for (size_t y = 0; y < image->view.height; ++y) { + for (size_t x = 0; x < image->view.width; ++x) { + unsigned char *current_pixel = pixel(image, x, y); + + for (size_t channel = 0; channel < image->view.channels; ++channel) { + current_pixel[channel] = random_byte() & RANDOM_PIXEL_MASK; + } + } + + for (size_t i = image->view.width * image->view.channels; + i < image->view.stride; + ++i) { + image->data[y * image->view.stride + i] = random_byte(); + } + } +} + +static void copy_image(test_image_t *to, const test_image_t *from) { + memcpy(to->data, from->data, from->size); + to->size = from->size; + to->view = from->view; + to->view.data = to->data; +} + +static unsigned char clamp_to_byte(double value) { + if (value < 0.0) { + return 0U; + } + if (value > 255.0) { + return 255U; + } + return (unsigned char)(value + 0.5); +} + +static int cv_border(filter_border_mode_t border) { + switch (border) { + case FILTER_BORDER_WRAP: + return IPL_BORDER_WRAP; + case FILTER_BORDER_REFLECT: + return IPL_BORDER_REFLECT_101; + case FILTER_BORDER_CLAMP: + default: + return IPL_BORDER_REPLICATE; + } +} + +static const char *border_name(filter_border_mode_t border) { + switch (border) { + case FILTER_BORDER_WRAP: + return "wrap"; + case FILTER_BORDER_REFLECT: + return "reflect"; + case FILTER_BORDER_CLAMP: + default: + return "clamp"; + } +} + +static filter_t make_test_filter(const kernel_sample_t *kernel, + filter_border_mode_t border) { + return make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + border, + kernel->values, + kernel->factor, + kernel->bias, + kernel->width, + kernel->height); +} + +static CvMat *make_cv_kernel(const kernel_sample_t *kernel) { + CvMat *cv_kernel = + cvCreateMat((int)kernel->height, (int)kernel->width, CV_64FC1); + + if (cv_kernel == NULL) { + return NULL; + } + + for (size_t y = 0; y < kernel->height; ++y) { + for (size_t x = 0; x < kernel->width; ++x) { + const double value = kernel->values[y * kernel->width + x]; + cvmSet(cv_kernel, (int)y, (int)x, value * kernel->factor); + } + } + + return cv_kernel; +} + +static CvMat *run_opencv_filter(const test_image_t *source, + const kernel_sample_t *kernel, + filter_border_mode_t border) { + const int channels = (int)source->view.channels; + const int input_type = CV_MAKETYPE(CV_8U, channels); + const int result_type = CV_MAKETYPE(CV_64F, channels); + const int radius_x = (int)(kernel->width / 2U); + const int radius_y = (int)(kernel->height / 2U); + + CvMat source_header; + CvMat *padded = NULL; + CvMat *result = NULL; + CvMat *cv_kernel = NULL; + + cvInitMatHeader(&source_header, + (int)source->view.height, + (int)source->view.width, + input_type, + (void *)source->data, + (int)source->view.stride); + + padded = cvCreateMat((int)source->view.height + 2 * radius_y, + (int)source->view.width + 2 * radius_x, + input_type); + result = cvCreateMat((int)source->view.height + 2 * radius_y, + (int)source->view.width + 2 * radius_x, + result_type); + cv_kernel = make_cv_kernel(kernel); + + if (padded == NULL || result == NULL || cv_kernel == NULL) { + cvReleaseMat(&padded); + cvReleaseMat(&result); + cvReleaseMat(&cv_kernel); + return NULL; + } + + /* cvFilter2D from OpenCV 2 C API always uses its own border mode. + To compare the same border handling as our code, pad the image first. */ + cvCopyMakeBorder(&source_header, + padded, + cvPoint(radius_x, radius_y), + cv_border(border), + cvScalarAll(0.0)); + cvFilter2D(padded, result, cv_kernel, cvPoint(radius_x, radius_y)); + + cvReleaseMat(&padded); + cvReleaseMat(&cv_kernel); + return result; +} + +static int padding_is_same(const test_image_t *before, + const test_image_t *after) { + const size_t row_pixels = after->view.width * after->view.channels; + + for (size_t y = 0; y < after->view.height; ++y) { + const unsigned char *before_padding = + before->data + y * before->view.stride + row_pixels; + const unsigned char *after_padding = + after->data + y * after->view.stride + row_pixels; + const size_t padding_size = after->view.stride - row_pixels; + + if (memcmp(before_padding, after_padding, padding_size) != 0) { + return 0; + } + } + + return 1; +} + +static int compare_with_opencv(const test_image_t *source, + const test_image_t *actual, + const kernel_sample_t *kernel, + const CvMat *opencv_result, + const char *test_name) { + const int radius_x = (int)(kernel->width / 2U); + const int radius_y = (int)(kernel->height / 2U); + + for (size_t y = 0; y < actual->view.height; ++y) { + const double *opencv_row = + (const double *)(const void *)(opencv_result->data.ptr + + ((int)y + radius_y) * opencv_result->step); + + for (size_t x = 0; x < actual->view.width; ++x) { + const unsigned char *source_pixel = const_pixel(source, x, y); + const unsigned char *actual_pixel = const_pixel(actual, x, y); + + for (size_t channel = 0; channel < actual->view.channels; ++channel) { + const double cv_value = + opencv_row[((int)x + radius_x) * actual->view.channels + channel] + + kernel->bias; + const unsigned char expected = + (actual->view.channels == 4U && channel == 3U) + ? source_pixel[channel] + : clamp_to_byte(cv_value); + + if (actual_pixel[channel] != expected) { + fail_msg("%s: pixel (%zu, %zu), channel %zu: got %u, expected %u", + test_name, + x, + y, + channel, + (unsigned)actual_pixel[channel], + (unsigned)expected); + return 0; + } + } + } + } + + if (!padding_is_same(source, actual)) { + fail_msg("%s: image row padding changed", test_name); + return 0; + } + + return 1; +} + +static void check_case(size_t width, + size_t height, + size_t channels, + const kernel_sample_t *kernel, + filter_border_mode_t border) { + const size_t padding = (width + height + channels) % 4U; + char test_name[TEST_NAME_SIZE]; + test_image_t source; + test_image_t actual; + CvMat *opencv_result = NULL; + + snprintf(test_name, + sizeof(test_name), + "%zux%zux%zu, %s, %s border", + width, + height, + channels, + kernel->name, + border_name(border)); + + init_image(&source, width, height, channels, padding); + fill_random(&source); + copy_image(&actual, &source); + + filter_t filter = make_test_filter(kernel, border); + assert_int_equal(sequential_convolution(&filter, &actual.view), 0); + + opencv_result = run_opencv_filter(&source, kernel, border); + assert_non_null(opencv_result); + + assert_true( + compare_with_opencv(&source, &actual, kernel, opencv_result, test_name)); + cvReleaseMat(&opencv_result); +} + +static void sequential_convolution_matches_opencv(void **state) { + (void)state; + + static const double identity[] = {1.0}; + static const double gaussian_3x3[] = { + 1.0, + 2.0, + 1.0, + 2.0, + 4.0, + 2.0, + 1.0, + 2.0, + 1.0, + }; + static const double sharpen_3x3[] = { + 0.0, + -1.0, + 0.0, + -1.0, + 5.0, + -1.0, + 0.0, + -1.0, + 0.0, + }; + static const double tall_3x5[] = { + 0.0, + 1.0, + 0.0, + -1.0, + 2.0, + -1.0, + 0.0, + 3.0, + 0.0, + -1.0, + 2.0, + -1.0, + 0.0, + 1.0, + 0.0, + }; + static const double wide_5x3[] = { + 0.0, + -1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 2.0, + 3.0, + 2.0, + 1.0, + 0.0, + 1.0, + 0.0, + -1.0, + 0.0, + }; + + const size_t widths[] = {1U, 2U, 3U, 5U, 8U, 13U}; + const size_t heights[] = {1U, 2U, 4U, 7U}; + const size_t channels[] = {1U, 3U, 4U}; + const filter_border_mode_t borders[] = { + FILTER_BORDER_WRAP, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, + }; + const kernel_sample_t kernels[] = { + {"identity", identity, 1U, 1U, 1.0, 0.0}, + {"gaussian 3x3", gaussian_3x3, 3U, 3U, 1.0 / 16.0, 0.0}, + {"sharpen 3x3", sharpen_3x3, 3U, 3U, 1.0, 0.0}, + {"3x5", tall_3x5, 3U, 5U, 1.0, 3.0}, + {"5x3", wide_5x3, 5U, 3U, 1.0, 7.0}, + }; + + for (size_t width_index = 0; width_index < ARRAY_SIZE(widths); + ++width_index) { + for (size_t height_index = 0; height_index < ARRAY_SIZE(heights); + ++height_index) { + for (size_t channel_index = 0; channel_index < ARRAY_SIZE(channels); + ++channel_index) { + for (size_t kernel_index = 0; kernel_index < ARRAY_SIZE(kernels); + ++kernel_index) { + for (size_t border_index = 0; border_index < ARRAY_SIZE(borders); + ++border_index) { + check_case(widths[width_index], + heights[height_index], + channels[channel_index], + &kernels[kernel_index], + borders[border_index]); + } + } + } + } + } +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(sequential_convolution_matches_opencv), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/sequential_tests/sequential_tests.c b/test/sequential_tests/sequential_tests.c new file mode 100644 index 0000000..916dd72 --- /dev/null +++ b/test/sequential_tests/sequential_tests.c @@ -0,0 +1,363 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_IMAGE_BYTES 4096U +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +typedef struct test_image { + unsigned char data[MAX_IMAGE_BYTES]; + image_view_t view; + size_t size; +} test_image_t; + +static uint32_t random_state = 0x12345678U; + +static unsigned char random_byte(void) { + random_state = random_state * 1664525U + 1013904223U; + return (unsigned char)(random_state >> 24U); +} + +static void init_image(test_image_t *image, + size_t width, + size_t height, + size_t channels, + size_t padding) { + const size_t stride = width * channels + padding; + + image->size = height * stride; + image->view = (image_view_t){ + .data = image->data, + .height = height, + .width = width, + .stride = stride, + .channels = channels, + }; + + memset(image->data, 0xCD, sizeof(image->data)); +} + +static void fill_random(test_image_t *image) { + for (size_t i = 0; i < image->size; ++i) { + image->data[i] = random_byte(); + } +} + +static void copy_image(test_image_t *to, const test_image_t *from) { + memcpy(to->data, from->data, from->size); +} + +static int same_image(const test_image_t *left, const test_image_t *right) { + if (left->size != right->size) { + return 0; + } + return memcmp(left->data, right->data, left->size) == 0; +} + +static filter_t make_filter(const double *kernel, + size_t width, + size_t height, + double factor, + filter_border_mode_t border_mode) { + return make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + border_mode, + kernel, + factor, + 0.0, + width, + height); +} + +static void apply_filter(test_image_t *image, + const double *kernel, + size_t width, + size_t height, + double factor, + filter_border_mode_t border_mode) { + filter_t filter = make_filter(kernel, width, height, factor, border_mode); + assert_int_equal(sequential_convolution(&filter, &image->view), 0); +} + +static void make_identity_kernel(double *kernel, size_t size) { + memset(kernel, 0, size * size * sizeof(*kernel)); + kernel[(size / 2U) * size + size / 2U] = 1.0; +} + +static void make_shift_kernel(double *kernel, int dx, int dy) { + const int center = 1; + + memset(kernel, 0, 9U * sizeof(*kernel)); + kernel[(size_t)(center + dy) * 3U + (size_t)(center + dx)] = 1.0; +} + +static unsigned char *pixel(test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static const unsigned char * +const_pixel(const test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static void set_one_channel_image(test_image_t *image, + const unsigned char *values) { + for (size_t y = 0; y < image->view.height; ++y) { + for (size_t x = 0; x < image->view.width; ++x) { + *pixel(image, x, y) = values[y * image->view.width + x]; + } + } +} + +static int one_channel_image_has_values(const test_image_t *image, + const unsigned char *values) { + for (size_t y = 0; y < image->view.height; ++y) { + for (size_t x = 0; x < image->view.width; ++x) { + if (*const_pixel(image, x, y) != values[y * image->view.width + x]) { + return 0; + } + } + } + return 1; +} + +static void identity_filter_keeps_image(void **state) { + (void)state; + const size_t kernel_sizes[] = {1U, 3U, 5U, 7U}; + const filter_border_mode_t borders[] = { + FILTER_BORDER_WRAP, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, + }; + + for (size_t channels = 1U; channels <= 4U; ++channels) { + if (channels == 2U) { + continue; + } + + for (size_t i = 0; i < ARRAY_SIZE(kernel_sizes); + ++i) { + double kernel[49]; + test_image_t original; + test_image_t actual; + + init_image(&original, 8U, 5U, channels, 3U); + init_image(&actual, 8U, 5U, channels, 3U); + + fill_random(&original); + make_identity_kernel(kernel, kernel_sizes[i]); + + for (size_t b = 0; b < ARRAY_SIZE(borders); ++b) { + copy_image(&actual, &original); + apply_filter( + &actual, kernel, kernel_sizes[i], kernel_sizes[i], 1.0, borders[b]); + assert_true(same_image(&original, &actual)); + } + } + } + +} + +static void zero_filter_makes_rgb_black_and_keeps_alpha(void **state) { + (void)state; + double zero_kernel[9] = {0.0}; + test_image_t original; + test_image_t actual; + + init_image(&original, 7U, 4U, 4U, 2U); + init_image(&actual, 7U, 4U, 4U, 2U); + + fill_random(&original); + copy_image(&actual, &original); + + apply_filter(&actual, zero_kernel, 3U, 3U, 1.0, FILTER_BORDER_REFLECT); + + for (size_t y = 0; y < actual.view.height; ++y) { + for (size_t x = 0; x < actual.view.width; ++x) { + const unsigned char *before = const_pixel(&original, x, y); + const unsigned char *after = const_pixel(&actual, x, y); + + assert_int_equal(after[0], 0U); + assert_int_equal(after[1], 0U); + assert_int_equal(after[2], 0U); + assert_int_equal(after[3], before[3]); + } + } + +} + +static void shift_filter_respects_border_modes(void **state) { + (void)state; + double shift_right[9]; + + // clang-format off + const unsigned char source_values[] = { + 1U, 2U, 3U, + 4U, 5U, 6U, + }; + const unsigned char wrap_expected[] = { + 2U, 3U, 1U, + 5U, 6U, 4U, + }; + const unsigned char clamp_expected[] = { + 2U, 3U, 3U, + 5U, 6U, 6U, + }; + const unsigned char reflect_expected[] = { + 2U, 3U, 2U, + 5U, 6U, 5U, + }; + // clang-format on + + test_image_t image; + + init_image(&image, 3U, 2U, 1U, 1U); + + make_shift_kernel(shift_right, 1, 0); + + set_one_channel_image(&image, source_values); + apply_filter(&image, shift_right, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + assert_true(one_channel_image_has_values(&image, wrap_expected)); + + set_one_channel_image(&image, source_values); + apply_filter(&image, shift_right, 3U, 3U, 1.0, FILTER_BORDER_CLAMP); + assert_true(one_channel_image_has_values(&image, clamp_expected)); + + set_one_channel_image(&image, source_values); + apply_filter(&image, shift_right, 3U, 3U, 1.0, FILTER_BORDER_REFLECT); + assert_true(one_channel_image_has_values(&image, reflect_expected)); + +} + +static void opposite_shifts_compose_to_identity_with_wrap(void **state) { + (void)state; + double shift_right[9]; + double shift_left[9]; + double shift_down[9]; + double shift_up[9]; + test_image_t original; + test_image_t actual; + + init_image(&original, 9U, 7U, 3U, 4U); + init_image(&actual, 9U, 7U, 3U, 4U); + + make_shift_kernel(shift_right, 1, 0); + make_shift_kernel(shift_left, -1, 0); + make_shift_kernel(shift_down, 0, 1); + make_shift_kernel(shift_up, 0, -1); + + fill_random(&original); + + copy_image(&actual, &original); + apply_filter(&actual, shift_right, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + apply_filter(&actual, shift_left, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + assert_true(same_image(&original, &actual)); + + copy_image(&actual, &original); + apply_filter(&actual, shift_down, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + apply_filter(&actual, shift_up, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + assert_true(same_image(&original, &actual)); + +} + +static void zero_padded_kernel_gives_same_result(void **state) { + (void)state; + + // clang-format off + const double kernel_3x3[9] = { + 0.0, 1.0, 0.0, + 1.0, 4.0, 1.0, + 0.0, 1.0, 0.0, + }; + const double same_kernel_padded_to_5x5[25] = { + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 1.0, 4.0, 1.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + }; + // clang-format on + + const filter_border_mode_t borders[] = { + FILTER_BORDER_WRAP, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, + }; + test_image_t source; + test_image_t small_kernel_result; + test_image_t padded_kernel_result; + + init_image(&source, 10U, 6U, 3U, 2U); + init_image(&small_kernel_result, 10U, 6U, 3U, 2U); + init_image(&padded_kernel_result, 10U, 6U, 3U, 2U); + + fill_random(&source); + + for (size_t i = 0; i < ARRAY_SIZE(borders); ++i) { + copy_image(&small_kernel_result, &source); + copy_image(&padded_kernel_result, &source); + + apply_filter(&small_kernel_result, kernel_3x3, 3U, 3U, 1.0, borders[i]); + apply_filter(&padded_kernel_result, + same_kernel_padded_to_5x5, + 5U, + 5U, + 1.0, + borders[i]); + assert_true(same_image(&small_kernel_result, &padded_kernel_result)); + } + +} + +static void known_wrap_mean_3x3(void **state) { + (void)state; + double mean_kernel[9]; + test_image_t image; + + init_image(&image, 3U, 3U, 1U, 0U); + + // clang-format off + const unsigned char values[] = { + 1U, 2U, 3U, + 4U, 5U, 6U, + 7U, 8U, 9U, + }; + const unsigned char expected[] = { + 5U, 5U, 5U, + 5U, 5U, 5U, + 5U, 5U, 5U, + }; + // clang-format on + + for (size_t i = 0; i < 9U; ++i) { + mean_kernel[i] = 1.0; + } + + set_one_channel_image(&image, values); + apply_filter(&image, mean_kernel, 3U, 3U, 1.0 / 9.0, FILTER_BORDER_WRAP); + assert_true(one_channel_image_has_values(&image, expected)); + +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(identity_filter_keeps_image), + cmocka_unit_test(zero_filter_makes_rgb_black_and_keeps_alpha), + cmocka_unit_test(shift_filter_respects_border_modes), + cmocka_unit_test(opposite_shifts_compose_to_identity_with_wrap), + cmocka_unit_test(zero_padded_kernel_gives_same_result), + cmocka_unit_test(known_wrap_mean_3x3), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/thread_pool_tests/CMakeLists.txt b/test/thread_pool_tests/CMakeLists.txt new file mode 100644 index 0000000..e0fd0f7 --- /dev/null +++ b/test/thread_pool_tests/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(thread_pool_tests + thread_pool_test.c +) + +target_link_libraries(thread_pool_tests + PRIVATE + convolution_runtime + PkgConfig::CMOCKA +) + +add_test(NAME thread_pool_tests COMMAND thread_pool_tests) diff --git a/test/thread_pool_tests/thread_pool_test.c b/test/thread_pool_tests/thread_pool_test.c new file mode 100644 index 0000000..dffc1a8 --- /dev/null +++ b/test/thread_pool_tests/thread_pool_test.c @@ -0,0 +1,81 @@ +#include + +#include +#include +#include + +#include + +#define THREAD_POOL_WORKERS 2U +#define TASK_COUNT 32U + +typedef struct counter_task_args { + size_t *counter; + pthread_mutex_t *mutex; +} counter_task_args_t; + +static void increment_counter(void *args) { + counter_task_args_t *task_args = args; + + pthread_mutex_lock(task_args->mutex); + ++(*task_args->counter); + pthread_mutex_unlock(task_args->mutex); +} + +static task_t make_counter_task(counter_task_args_t *args, size_t index) { + const task_type_t task_types[] = { + READER_TASK, + COMPUTE_TASK, + WRITER_TASK, + }; + + return (task_t){ + .fn = increment_counter, + .fn_type = task_types[index % (sizeof(task_types) / sizeof(task_types[0]))], + .args = args, + }; +} + +static void thread_pool_runs_all_tasks(void **state) { + (void)state; + + thread_pool_t pool; + pthread_mutex_t counter_mutex; + counter_task_args_t args[TASK_COUNT]; + size_t counter = 0U; + + const thread_pool_config_t config = { + .worker_count = THREAD_POOL_WORKERS, + .reader_queue_capacity = TASK_COUNT, + .computer_queue_capacity = TASK_COUNT, + .writer_queue_capacity = TASK_COUNT, + .max_active_readers = 1U, + .max_active_computers = 1U, + .max_active_writers = 1U, + }; + + assert_int_equal(pthread_mutex_init(&counter_mutex, NULL), 0); + assert_true(thread_pool_init(&pool, &config)); + + for (size_t i = 0; i < TASK_COUNT; ++i) { + args[i].counter = &counter; + args[i].mutex = &counter_mutex; + + task_t task = make_counter_task(&args[i], i); + assert_true(thread_pool_enqueue(&pool, &task)); + } + + thread_pool_wait(&pool); + thread_pool_destroy(&pool); + pthread_mutex_destroy(&counter_mutex); + + assert_int_equal(counter, TASK_COUNT); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(thread_pool_runs_all_tasks), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}