From d709500a00f0699250e9c7c2ca75f9c8e95706d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:06:27 +0000 Subject: [PATCH 1/3] Initial plan From fc1e9ea01549dba546d2258f77d2fc7272cb82d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:22:00 +0000 Subject: [PATCH 2/3] Restructure MODT to use JSON-based filter configuration - Create filters.json with all filter configurations - Create filter_engine.py with all filter operations - Create filter_manager.py to load/save filter configs - Refactor main.py with tabs and Filter Editor UI - Add tests for filter manager and engine - Update README.md with new documentation - Remove old filters_config_generator.py and effects_settings.py - Remove filters/ folder (no longer needed) Co-authored-by: CorsoCoder <45120484+CorsoCoder@users.noreply.github.com> --- README.md | 144 ++- __pycache__/filter_engine.cpython-312.pyc | Bin 0 -> 22605 bytes __pycache__/filter_manager.cpython-312.pyc | Bin 0 -> 10497 bytes __pycache__/main.cpython-312.pyc | Bin 0 -> 39620 bytes effects_settings.py | 197 ----- filter_engine.py | 439 +++++++++ filter_manager.py | 243 +++++ filters.json | 181 ++++ filters/Color/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 165 -> 0 bytes .../Color/__pycache__/color.cpython-312.pyc | Bin 524 -> 0 bytes .../__pycache__/escala_grises.cpython-312.pyc | Bin 530 -> 0 bytes .../Color/__pycache__/inidie.cpython-312.pyc | Bin 1747 -> 0 bytes .../Color/__pycache__/matiz.cpython-312.pyc | Bin 1395 -> 0 bytes .../__pycache__/saturador.cpython-312.pyc | Bin 2188 -> 0 bytes filters/Color/escala_grises.py | 7 - filters/Color/inidie.py | 31 - filters/Color/matiz.py | 20 - filters/Color/saturador.py | 44 - filters/Desenfoque/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 170 -> 0 bytes .../__pycache__/bilateral.cpython-312.pyc | Bin 732 -> 0 bytes .../__pycache__/box.cpython-312.pyc | Bin 680 -> 0 bytes .../__pycache__/desenfoque.cpython-312.pyc | Bin 726 -> 0 bytes .../desenfoque_gausiano.cpython-312.pyc | Bin 735 -> 0 bytes .../desenfoque_horizontal.cpython-312.pyc | Bin 903 -> 0 bytes .../desenfoque_vertical.cpython-312.pyc | Bin 943 -> 0 bytes .../__pycache__/mediana.cpython-312.pyc | Bin 704 -> 0 bytes .../__pycache__/morphology.cpython-312.pyc | Bin 1133 -> 0 bytes .../__pycache__/shift.cpython-312.pyc | Bin 695 -> 0 bytes .../__pycache__/stack.cpython-312.pyc | Bin 660 -> 0 bytes .../__pycache__/super_queee.cpython-312.pyc | Bin 1473 -> 0 bytes filters/Desenfoque/bilateral.py | 13 - filters/Desenfoque/box.py | 13 - filters/Desenfoque/desenfoque_gausiano.py | 14 - filters/Desenfoque/desenfoque_horizontal.py | 14 - filters/Desenfoque/desenfoque_vertical.py | 15 - filters/Desenfoque/mediana.py | 13 - filters/Ruido/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 165 -> 0 bytes .../Ruido/__pycache__/ruido.cpython-312.pyc | Bin 973 -> 0 bytes .../ruido_gausiano.cpython-312.pyc | Bin 980 -> 0 bytes .../sal_y_pimienta.cpython-312.pyc | Bin 1465 -> 0 bytes filters/Ruido/ruido_gausiano.py | 17 - filters/__init__.py | 0 filters/__pycache__/__init__.cpython-312.pyc | Bin 163 -> 0 bytes filters/__pycache__/bilateral.cpython-312.pyc | Bin 1208 -> 0 bytes filters/__pycache__/color.cpython-312.pyc | Bin 522 -> 0 bytes .../__pycache__/desenfoque.cpython-312.pyc | Bin 721 -> 0 bytes filters/__pycache__/ruido.cpython-312.pyc | Bin 967 -> 0 bytes .../glitch/__pycache__/glitch.cpython-312.pyc | Bin 1047 -> 0 bytes .../glitch/__pycache__/shift.cpython-312.pyc | Bin 1123 -> 0 bytes .../__pycache__/shift_pro.cpython-312.pyc | Bin 1509 -> 0 bytes filters/glitch/glitch.py | 20 - filters/glitch/shift.py | 22 - filters/glitch/shift_pro.py | 32 - .../pixel/__pycache__/pixelar.cpython-312.pyc | Bin 1378 -> 0 bytes .../__pycache__/pixelar_pro.cpython-312.pyc | Bin 2076 -> 0 bytes filters/pixel/pixelar.py | 29 - filters/pixel/pixelar_pro.py | 39 - .../__pycache__/dilatar.cpython-312.pyc | Bin 1025 -> 0 bytes .../__pycache__/dilatar_pro.cpython-312.pyc | Bin 1157 -> 0 bytes .../varios/__pycache__/erode.cpython-312.pyc | Bin 1137 -> 0 bytes .../__pycache__/piramide.cpython-312.pyc | Bin 1207 -> 0 bytes .../__pycache__/super_queee.cpython-312.pyc | Bin 1469 -> 0 bytes filters/varios/dilatar.py | 17 - filters/varios/dilatar_pro.py | 19 - filters/varios/erode.py | 19 - filters/varios/super_queee.py | 22 - filters_config_generator.py | 60 -- main.py | 836 ++++++++++++++---- tests/__init__.py | 1 + tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 142 bytes .../test_filter_manager.cpython-312.pyc | Bin 0 -> 18563 bytes tests/test_filter_manager.py | 328 +++++++ 75 files changed, 1972 insertions(+), 877 deletions(-) create mode 100644 __pycache__/filter_engine.cpython-312.pyc create mode 100644 __pycache__/filter_manager.cpython-312.pyc create mode 100644 __pycache__/main.cpython-312.pyc delete mode 100644 effects_settings.py create mode 100644 filter_engine.py create mode 100644 filter_manager.py create mode 100644 filters.json delete mode 100644 filters/Color/__init__.py delete mode 100644 filters/Color/__pycache__/__init__.cpython-312.pyc delete mode 100644 filters/Color/__pycache__/color.cpython-312.pyc delete mode 100644 filters/Color/__pycache__/escala_grises.cpython-312.pyc delete mode 100644 filters/Color/__pycache__/inidie.cpython-312.pyc delete mode 100644 filters/Color/__pycache__/matiz.cpython-312.pyc delete mode 100644 filters/Color/__pycache__/saturador.cpython-312.pyc delete mode 100644 filters/Color/escala_grises.py delete mode 100644 filters/Color/inidie.py delete mode 100644 filters/Color/matiz.py delete mode 100644 filters/Color/saturador.py delete mode 100644 filters/Desenfoque/__init__.py delete mode 100644 filters/Desenfoque/__pycache__/__init__.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/bilateral.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/box.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/desenfoque.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/desenfoque_gausiano.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/desenfoque_horizontal.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/desenfoque_vertical.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/mediana.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/morphology.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/shift.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/stack.cpython-312.pyc delete mode 100644 filters/Desenfoque/__pycache__/super_queee.cpython-312.pyc delete mode 100644 filters/Desenfoque/bilateral.py delete mode 100644 filters/Desenfoque/box.py delete mode 100644 filters/Desenfoque/desenfoque_gausiano.py delete mode 100644 filters/Desenfoque/desenfoque_horizontal.py delete mode 100644 filters/Desenfoque/desenfoque_vertical.py delete mode 100644 filters/Desenfoque/mediana.py delete mode 100644 filters/Ruido/__init__.py delete mode 100644 filters/Ruido/__pycache__/__init__.cpython-312.pyc delete mode 100644 filters/Ruido/__pycache__/ruido.cpython-312.pyc delete mode 100644 filters/Ruido/__pycache__/ruido_gausiano.cpython-312.pyc delete mode 100644 filters/Ruido/__pycache__/sal_y_pimienta.cpython-312.pyc delete mode 100644 filters/Ruido/ruido_gausiano.py delete mode 100644 filters/__init__.py delete mode 100644 filters/__pycache__/__init__.cpython-312.pyc delete mode 100644 filters/__pycache__/bilateral.cpython-312.pyc delete mode 100644 filters/__pycache__/color.cpython-312.pyc delete mode 100644 filters/__pycache__/desenfoque.cpython-312.pyc delete mode 100644 filters/__pycache__/ruido.cpython-312.pyc delete mode 100644 filters/glitch/__pycache__/glitch.cpython-312.pyc delete mode 100644 filters/glitch/__pycache__/shift.cpython-312.pyc delete mode 100644 filters/glitch/__pycache__/shift_pro.cpython-312.pyc delete mode 100644 filters/glitch/glitch.py delete mode 100644 filters/glitch/shift.py delete mode 100644 filters/glitch/shift_pro.py delete mode 100644 filters/pixel/__pycache__/pixelar.cpython-312.pyc delete mode 100644 filters/pixel/__pycache__/pixelar_pro.cpython-312.pyc delete mode 100644 filters/pixel/pixelar.py delete mode 100644 filters/pixel/pixelar_pro.py delete mode 100644 filters/varios/__pycache__/dilatar.cpython-312.pyc delete mode 100644 filters/varios/__pycache__/dilatar_pro.cpython-312.pyc delete mode 100644 filters/varios/__pycache__/erode.cpython-312.pyc delete mode 100644 filters/varios/__pycache__/piramide.cpython-312.pyc delete mode 100644 filters/varios/__pycache__/super_queee.cpython-312.pyc delete mode 100644 filters/varios/dilatar.py delete mode 100644 filters/varios/dilatar_pro.py delete mode 100644 filters/varios/erode.py delete mode 100644 filters/varios/super_queee.py delete mode 100644 filters_config_generator.py create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_filter_manager.cpython-312.pyc create mode 100644 tests/test_filter_manager.py diff --git a/README.md b/README.md index 0207ad6..4cc4baa 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- Aplicación de procesamiento de fotos desarrollada en PyQt5 que permite aplicar efectos a imágenes. La aplicación es altamente modular, permitiendo a los usuarios crear y agregar sus propios efectos personalizados. + Aplicación de procesamiento de fotos desarrollada en PyQt5 que permite aplicar efectos a imágenes. La aplicación es altamente modular, permitiendo a los usuarios crear y agregar sus propios efectos personalizados mediante una interfaz gráfica o editando directamente el archivo JSON de configuración.


@@ -27,10 +27,12 @@ $${\color{red}Screenshots}$$ $${\color{yellow}Características}$$ -- **Interfaz de usuario muy amigable** -- **Gran variedad de efectos**: programos con opencv y numpy -- **Modularidad**: Los usuarios pueden crear sus propios efectos y agregarlos fácilmente. -- **Libertad**: tienes la libertad de editar los efectos y configurar los parametros a tu gusto +- **Interfaz de usuario muy amigable** con pestañas para edición de fotos y creación de filtros +- **Gran variedad de efectos**: programados con opencv y numpy +- **Modularidad**: Los usuarios pueden crear sus propios filtros desde la interfaz gráfica +- **Configuración JSON**: Todos los filtros se almacenan en `filters.json` para fácil edición +- **Persistencia**: Los filtros creados se guardan automáticamente y persisten entre sesiones +- **Libertad**: Tienes la libertad de editar los filtros y configurar los parámetros a tu gusto
@@ -69,40 +71,130 @@ $${\color{lightgreen}Instalación}$$ 3. Instala las dependencias: `pip install -r requirements.txt` -4. Ejecuta el script de configuración de efectos: - `python generate_effects_settings.py` - -5. Disfruta: +4. Ejecuta la aplicación: `python main.py`

+$${\color{lightblue}Arquitectura \space del \space proyecto}$$ -$${\color{lightblue}Crea \space tus \space propios \space efectos❗}$$ +``` +MODT/ +├── main.py # Aplicación principal con interfaz PyQt5 +├── filter_engine.py # Motor de filtros con todas las operaciones disponibles +├── filter_manager.py # Gestor de configuración de filtros (carga/guarda JSON) +├── filters.json # Configuración de todos los filtros disponibles +├── assets/ # Recursos gráficos (iconos, etc.) +├── tests/ # Pruebas unitarias +│ └── test_filter_manager.py +├── requirements.txt # Dependencias del proyecto +└── README.md # Este archivo +``` -1. Crea un archivo Python en la carpeta `effects`. Este archivo debe definir una función que aplique el efecto y si es necesario otra que envie los parametros de los QSliders y QComboBox. +
+
+$${\color{orange}Crea \space tus \space propios \space filtros❗}$$ + +### Opción 1: Desde la interfaz gráfica (Recomendado) + +1. Abre la aplicación con `python main.py` +2. Ve a la pestaña **"Editor de Filtros"** +3. Rellena los campos: + - **Nombre**: El nombre de tu filtro + - **Categoría**: Selecciona una existente o escribe una nueva + - **Operación**: Selecciona la operación base (blur, pixelate, etc.) + - **Descripción**: Una breve descripción del filtro +4. Añade parámetros si es necesario con el botón **"+ Añadir parámetro"** +5. Haz clic en **"Guardar filtro"** +6. ¡Tu filtro aparecerá inmediatamente en la pestaña "Editor de Fotos"! + +### Opción 2: Editando el archivo JSON + +Puedes editar directamente el archivo `filters.json`. Cada filtro tiene la siguiente estructura: + +```json +{ + "NombreCategoria": { + "nombre_filtro": { + "operation": "nombre_operacion", + "description": "Descripción del filtro", + "parameters": { + "parametro_slider": { + "min": 0, + "max": 100, + "init": 50, + "interval": 1 + }, + "parametro_opciones": { + "options": ["opcion1", "opcion2", "opcion3"], + "init": "opcion1" + } + } + } + } +} +``` -2. La función debe tener la siguiente estructura: +
-```python -def apply_effect(image, param1, param2...): - # Implementa tu efecto aquí - return modified_image +### Operaciones disponibles + +| Operación | Descripción | +|-----------|-------------| +| `grayscale` | Convierte a escala de grises | +| `hue_shift` | Cambia el matiz de los colores | +| `saturate_color` | Aumenta la saturación de un color específico | +| `indie_effect` | Efecto retro con bordes y ruido | +| `gaussian_blur` | Desenfoque gaussiano suave | +| `box_blur` | Desenfoque de caja | +| `bilateral_filter` | Filtro bilateral que preserva bordes | +| `median_blur` | Desenfoque de mediana | +| `horizontal_blur` | Desenfoque de movimiento horizontal | +| `vertical_blur` | Desenfoque de movimiento vertical | +| `glitch` | Efecto glitch con líneas desplazadas | +| `channel_shift` | Desplazamiento de canal de color | +| `channel_shift_pro` | Desplazamiento avanzado por canal RGB | +| `pixelate` | Pixelado básico | +| `pixelate_pro` | Pixelado avanzado con métodos múltiples | +| `gaussian_noise` | Añade ruido gaussiano | +| `dilate` | Dilatación morfológica | +| `dilate_pro` | Dilatación avanzada | +| `erode` | Erosión morfológica | +| `super_effect` | Combinación de blur, morfología y pirámide | -def get_filter_data(): - return { - "parameters": { - "parametro1": {"min": valor_minimo, "max": valor_maximo, "init": valor_inicial, "interval": intervalo}, - "parametroOpcion": {"options": ["opcion1", "opcion2"], "init": "valor_inicial"}, - } - } +
+### Tipos de parámetros +**Slider (deslizador):** +```json +"nombre_param": { + "min": 0, // Valor mínimo + "max": 100, // Valor máximo + "init": 50, // Valor inicial + "interval": 1 // Incremento del slider +} ``` -3. Ejecuta el script `generate_effects_settings.py` para actualizar el archivo de configuración `effects_settings.py`. +**Options (opciones):** +```json +"nombre_param": { + "options": ["opcion1", "opcion2", "opcion3"], // Lista de opciones + "init": "opcion1" // Opción seleccionada por defecto +} +``` + +
+
+
+ +$${\color{cyan}Ejecutar \space pruebas}$$ + +```bash +python -m unittest tests.test_filter_manager -v +```

@@ -110,12 +202,10 @@ def get_filter_data(): $${\color{pink}Contribuye🎉}$$ -

Las contribuciones son super bienvenidas, si creas algun efecto y quieres compartirlo con el mundo no dudes en mandarmelo!

+

Las contribuciones son super bienvenidas, si creas algún efecto y quieres compartirlo con el mundo no dudes en mandármelo!


- - diff --git a/__pycache__/filter_engine.cpython-312.pyc b/__pycache__/filter_engine.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87b8355492233998b3f3d73e4383b5d8aebe95bd GIT binary patch literal 22605 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 L`X88XAoteTL literal 0 HcmV?d00001 diff --git a/__pycache__/filter_manager.cpython-312.pyc b/__pycache__/filter_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b149bf682b539c920272c8d431d14d805f954dd GIT binary patch literal 10497 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 literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0343891e9e5823cbaed40c57351639d72fcb7023 GIT binary patch literal 39620 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

a4ZC zsh8eRP(b}Xh&q_Q>7FlLZrQm5ejXH_v%5PU#2jvS--AMzyOCTrR6p33@9uq2UgEBN z(AMpK+5Y2cd%jy~Hx)REvb2k)i>` z!i;h5_fcD}4j=-c`3g#mzm*xO4G0RMsX<2~CHD3hc zMi0Ymp;{aWbp6M$ruU^1ksxDf|6-TgB4`6?wP6{Pklb{sDRJ0>O8981PQ#gN<6(@5(!CVWz$j0(q zG#QI9(e%+soD3tcFbg{Qe%HR4E>iIv$I*1h$ZhWUMvgqKLbgrDaPyZ+ zn`$@_1uK^d{eWHaGS+e!LO3#V=M~p@BqEm~gO4V794sSYh&Np_T>04-z~t8n;XDu? zbAbi8K7`_#J&khiqmyXP2SN!DpmC)D9V@pGolfjTMkWx+he5Nfu0e*+P=PRze>94> zxjpjGDM*X;hlY&}-S6RsC=8$m_98d1(qH)Yo=wtPP$8S7G`2*k(13(A!v7lir2_q+ zZ!pT>mnl}KGVR36p8>*XyXB#M3T4VZ#fUt}DxV1&zKBJru1GgNstD{bg!jrqRJMCU zZm=#RG$11z8h^P0RzMkzJxv;ReD(!VzT~g<;*f}h9o)9}v zWc(-Za0S=$$ZzTJZLW#U3TT*J)Lav_JV0@T6lB%}waS_v9;Y`H{6`FGf=3QU4>vs` zXJ-$}YuqR74+h-s&Yyq|ZoY3rz^Y|gvfRW3`;7KPa1V3S21W=;SX(eI;|oM)Z(HhQGrM=?*I zz4`hpk8Jq4FW7A^|8nYcXVnkAN4T*Ik9*IsNU^8CTi5 h%YXIocaDAg*z(!8UcUD7uf1~HRfVsaxT;xq|37Fh_xb<; literal 0 HcmV?d00001 diff --git a/effects_settings.py b/effects_settings.py deleted file mode 100644 index eacab45..0000000 --- a/effects_settings.py +++ /dev/null @@ -1,197 +0,0 @@ -from filters import * - -FILTERS = {} - -from filters.Color import escala_grises -from filters.Color import inidie -from filters.Color import matiz -from filters.Color import saturador -from filters.Desenfoque import bilateral -from filters.Desenfoque import box -from filters.Desenfoque import desenfoque_gausiano -from filters.Desenfoque import desenfoque_horizontal -from filters.Desenfoque import desenfoque_vertical -from filters.Desenfoque import mediana -from filters.espejo import espejo -from filters.glitch import glitch -from filters.glitch import shift -from filters.glitch import shift_pro -from filters.pixel import pixelar -from filters.pixel import pixelar_pro -from filters.Ruido import ruido_gausiano -from filters.varios import dilatar -from filters.varios import dilatar_pro -from filters.varios import erode -from filters.varios import super_queee - -FILTERS = { - "Color": { - "escala_grises": { - "apply": escala_grises.apply_effect, - }, - "inidie": { - "apply": inidie.apply_effect, - "parameters": { - "blur_kernel_size": {'min': 3, 'max': 15, 'init': 5, 'interval': 2}, - "canny_threshold1": {'min': 50, 'max': 150, 'init': 100, 'interval': 10}, - "canny_threshold2": {'min': 100, 'max': 250, 'init': 200, 'interval': 10}, - "color_adjustment": {'min': 0.5, 'max': 2.0, 'init': 1.5, 'interval': 0.1}, - "noise_level": {'min': 10, 'max': 30, 'init': 20, 'interval': 5}, - } - }, - "matiz": { - "apply": matiz.apply_effect, - "parameters": { - "hue_shift": {'min': -180, 'max': 180, 'init': 0, 'interval': 1}, - } - }, - "saturador": { - "apply": saturador.apply_effect, - "parameters": { - "color": {'options': ['red', 'green', 'blue', 'yellow', 'cyan', 'magenta'], 'init': 'red'}, - "saturation_scale": {'min': 0, 'max': 100, 'init': 50, 'interval': 1}, - "brightness_boost": {'min': 0, 'max': 100, 'init': 50, 'interval': 1}, - } - }, - }, - "Desenfoque": { - "bilateral": { - "apply": bilateral.apply_effect, - "parameters": { - "d": {'min': 1, 'max': 10, 'init': 3, 'interval': 1}, - "sigmaColor": {'min': 1, 'max': 100, 'init': 20, 'interval': 1}, - "sigmaSpace": {'min': 1, 'max': 100, 'init': 15, 'interval': 1}, - } - }, - "box": { - "apply": box.apply_effect, - "parameters": { - "ksize_x": {'min': 3, 'max': 15, 'init': 5, 'interval': 2}, - "ksize_y": {'min': 3, 'max': 15, 'init': 5, 'interval': 2}, - } - }, - "desenfoque_gausiano": { - "apply": desenfoque_gausiano.apply_effect, - "parameters": { - "kernel_size": {'min': 1, 'max': 31, 'init': 15, 'interval': 2}, - "sigma": {'min': 1, 'max': 10, 'init': 5, 'interval': 1}, - } - }, - "desenfoque_horizontal": { - "apply": desenfoque_horizontal.apply_effect, - "parameters": { - "kernel_size": {'min': 1, 'max': 101, 'init': 3, 'interval': 2}, - } - }, - "desenfoque_vertical": { - "apply": desenfoque_vertical.apply_effect, - "parameters": { - "multi": {'min': 1, 'max': 100, 'init': 30, 'interval': 1}, - } - }, - "mediana": { - "apply": mediana.apply_effect, - "parameters": { - "kernel_size": {'min': 1, 'max': 101, 'init': 3, 'interval': 2}, - } - }, - }, - "espejo": { - "espejo": { - "apply": espejo.apply_effect, - "parameters": { - "angle": {'min': -45, 'max': 45, 'init': 0, 'interval': 1}, - "direction": {'options': ['Horizontal', 'Vertical'], 'init': 'Horizontal'}, - "flip_type": {'options': ['Full', 'Partial'], 'init': 'Full'}, - "split_ratio": {'min': 1, 'max': 9, 'init': 5, 'interval': 1}, - } - }, - }, - "glitch": { - "glitch": { - "apply": glitch.apply_effect, - "parameters": { - "glitch_intensity": {'min': 1, 'max': 50, 'init': 10, 'interval': 1}, - "glitch_frequency": {'min': 1, 'max': 50, 'init': 5, 'interval': 1}, - } - }, - "shift": { - "apply": shift.apply_effect, - "parameters": { - "shift_value": {'min': -50, 'max': 50, 'init': 5, 'interval': 1}, - } - }, - "shift_pro": { - "apply": shift_pro.apply_effect, - "parameters": { - "shift_value_b": {'min': -50, 'max': 50, 'init': 5, 'interval': 1}, - "shift_value_g": {'min': -50, 'max': 50, 'init': -5, 'interval': 1}, - "shift_value_r": {'min': -50, 'max': 50, 'init': 2, 'interval': 1}, - "direction_b": {'options': ['horizontal', 'vertical'], 'init': 'horizontal'}, - "direction_g": {'options': ['horizontal', 'vertical'], 'init': 'horizontal'}, - "direction_r": {'options': ['horizontal', 'vertical'], 'init': 'vertical'}, - } - }, - }, - "pixel": { - "pixelar": { - "apply": pixelar.apply_effect, - "parameters": { - "block_size": {'min': 2, 'max': 50, 'init': 5, 'interval': 1}, - } - }, - "pixelar_pro": { - "apply": pixelar_pro.apply_effect, - "parameters": { - "block_size": {'min': 2, 'max': 50, 'init': 5, 'interval': 1}, - "method": {'options': ['mean', 'median', 'mode'], 'init': 'mean'}, - "intensity": {'min': 0.1, 'max': 2.0, 'init': 1.0, 'interval': 0.1}, - "block_shape": {'options': ['square', 'rectangular'], 'init': 'square'}, - } - }, - }, - "Ruido": { - "ruido_gausiano": { - "apply": ruido_gausiano.apply_effect, - "parameters": { - "mean": {'min': -100, 'max': 100, 'init': 0, 'interval': 10}, - "std_dev": {'min': 1, 'max': 10, 'init': 5, 'interval': 1}, - } - }, - }, - "varios": { - "dilatar": { - "apply": dilatar.apply_effect, - "parameters": { - "kernel_size": {'min': 2, 'max': 50, 'init': 5, 'interval': 1}, - } - }, - "dilatar_pro": { - "apply": dilatar_pro.apply_effect, - "parameters": { - "kernel_size": {'min': 2, 'max': 50, 'init': 5, 'interval': 1}, - "anchor": {'min': -1, 'max': -1, 'init': -1, 'interval': 1}, - "iterations": {'min': 1, 'max': 10, 'init': 1, 'interval': 1}, - "border_value": {'min': 0, 'max': 255, 'init': 0, 'interval': 1}, - } - }, - "erode": { - "apply": erode.apply_effect, - "parameters": { - "kernel_size": {'min': 2, 'max': 50, 'init': 5, 'interval': 1}, - "anchor": {'min': -1, 'max': -1, 'init': -1, 'interval': 1}, - "iterations": {'min': 1, 'max': 10, 'init': 1, 'interval': 1}, - "border_type": {'min': 0, 'max': 4, 'init': 0, 'interval': 1}, - "border_value": {'min': 0, 'max': 255, 'init': 0, 'interval': 1}, - } - }, - "super_queee": { - "apply": super_queee.apply_effect, - "parameters": { - "blur_radius": {'min': 1, 'max': 10, 'init': 5, 'interval': 1}, - "morphology_iterations": {'min': 1, 'max': 5, 'init': 2, 'interval': 1}, - "pyramid_levels": {'min': 1, 'max': 5, 'init': 3, 'interval': 1}, - } - }, - }, -} diff --git a/filter_engine.py b/filter_engine.py new file mode 100644 index 0000000..fe06481 --- /dev/null +++ b/filter_engine.py @@ -0,0 +1,439 @@ +""" +Filter Engine Module + +This module contains all the built-in filter operations that can be combined +to create custom filters. Each operation is a function that takes an image +and parameters, returning the modified image. +""" + +import cv2 +import numpy as np +from typing import Any, Dict, Callable + + +class FilterEngine: + """ + Engine that applies filter operations to images. + + Operations are predefined functions that can be configured via JSON. + Filters can be single operations or pipelines of multiple operations. + """ + + def __init__(self): + """Initialize the filter engine with all available operations.""" + self._operations: Dict[str, Callable] = { + # Color operations + "grayscale": self._grayscale, + "hue_shift": self._hue_shift, + "saturate_color": self._saturate_color, + "indie_effect": self._indie_effect, + + # Blur operations + "gaussian_blur": self._gaussian_blur, + "box_blur": self._box_blur, + "bilateral_filter": self._bilateral_filter, + "median_blur": self._median_blur, + "horizontal_blur": self._horizontal_blur, + "vertical_blur": self._vertical_blur, + + # Glitch operations + "glitch": self._glitch, + "channel_shift": self._channel_shift, + "channel_shift_pro": self._channel_shift_pro, + + # Pixel operations + "pixelate": self._pixelate, + "pixelate_pro": self._pixelate_pro, + + # Noise operations + "gaussian_noise": self._gaussian_noise, + + # Morphology operations + "dilate": self._dilate, + "dilate_pro": self._dilate_pro, + "erode": self._erode, + "super_effect": self._super_effect, + } + + def get_available_operations(self) -> list: + """Get list of all available operations.""" + return list(self._operations.keys()) + + def apply_operation(self, image: np.ndarray, operation: str, **params) -> np.ndarray: + """ + Apply a single operation to an image. + + Args: + image: The input image + operation: Name of the operation to apply + **params: Parameters for the operation + + Returns: + The modified image + """ + if operation not in self._operations: + raise ValueError(f"Unknown operation: {operation}") + + return self._operations[operation](image, **params) + + def apply_filter(self, image: np.ndarray, filter_config: Dict[str, Any]) -> np.ndarray: + """ + Apply a filter (which may be a pipeline of operations) to an image. + + Args: + image: The input image + filter_config: Filter configuration from JSON + + Returns: + The modified image + """ + operation = filter_config.get("operation") + if not operation: + raise ValueError("Filter must have an 'operation' field") + + # Extract parameter values (use 'init' as default) + params = {} + for param_name, param_config in filter_config.get("parameters", {}).items(): + if isinstance(param_config, dict): + params[param_name] = param_config.get("init", param_config.get("value")) + else: + params[param_name] = param_config + + return self.apply_operation(image, operation, **params) + + def apply_filter_with_params(self, image: np.ndarray, filter_config: Dict[str, Any], + user_params: Dict[str, Any]) -> np.ndarray: + """ + Apply a filter with user-provided parameter values. + + Args: + image: The input image + filter_config: Filter configuration from JSON + user_params: Parameter values provided by the user + + Returns: + The modified image + """ + operation = filter_config.get("operation") + if not operation: + raise ValueError("Filter must have an 'operation' field") + + return self.apply_operation(image, operation, **user_params) + + # ==================== Color Operations ==================== + + def _grayscale(self, image: np.ndarray, **kwargs) -> np.ndarray: + """Convert image to grayscale.""" + return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + + def _hue_shift(self, image: np.ndarray, hue_shift: int = 0, **kwargs) -> np.ndarray: + """Shift the hue of the image.""" + hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) + hsv_image[:, :, 0] = (hsv_image[:, :, 0].astype(int) + hue_shift) % 180 + return cv2.cvtColor(hsv_image, cv2.COLOR_HSV2BGR) + + def _saturate_color(self, image: np.ndarray, color: str = "red", + saturation_scale: int = 50, brightness_boost: int = 50, + **kwargs) -> np.ndarray: + """Enhance saturation and brightness for a specific color.""" + hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) + h, s, v = cv2.split(hsv_image) + + color_ranges = { + 'red': [(0, 10), (160, 180)], + 'green': [(35, 85)], + 'blue': [(100, 140)], + 'yellow': [(25, 35)], + 'cyan': [(85, 100)], + 'magenta': [(140, 160)] + } + + ranges = color_ranges.get(color, [(0, 180)]) + masks = [] + for (lower, upper) in ranges: + mask = cv2.inRange(h, lower, upper) + masks.append(mask) + + color_mask = masks[0] + for mask in masks[1:]: + color_mask = cv2.bitwise_or(color_mask, mask) + + s = cv2.add(s, (saturation_scale * (color_mask // 255)).astype(np.uint8)) + v = cv2.add(v, (brightness_boost * (color_mask // 255)).astype(np.uint8)) + + s = np.clip(s, 0, 255) + v = np.clip(v, 0, 255) + + enhanced_hsv = cv2.merge([h, s, v]) + return cv2.cvtColor(enhanced_hsv, cv2.COLOR_HSV2BGR) + + def _indie_effect(self, image: np.ndarray, blur_kernel_size: int = 5, + canny_threshold1: int = 100, canny_threshold2: int = 200, + color_adjustment: float = 1.5, noise_level: int = 20, + **kwargs) -> np.ndarray: + """Apply indie/retro effect.""" + if blur_kernel_size % 2 == 0: + blur_kernel_size += 1 + + blurred = cv2.GaussianBlur(image, (blur_kernel_size, blur_kernel_size), 0) + edges = cv2.Canny(blurred, canny_threshold1, canny_threshold2) + edges_3ch = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR) + + indie_image = np.clip( + color_adjustment * blurred - (1 - color_adjustment) * edges_3ch, + 0, 255 + ).astype(np.uint8) + + noise = np.random.normal(0, noise_level, image.shape).astype(np.uint8) + indie_image = np.clip(indie_image + noise, 0, 255).astype(np.uint8) + + return indie_image + + # ==================== Blur Operations ==================== + + def _gaussian_blur(self, image: np.ndarray, kernel_size: int = 15, + sigma: int = 5, **kwargs) -> np.ndarray: + """Apply Gaussian blur.""" + if kernel_size % 2 == 0: + kernel_size += 1 + return cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma) + + def _box_blur(self, image: np.ndarray, ksize_x: int = 5, + ksize_y: int = 5, **kwargs) -> np.ndarray: + """Apply box blur.""" + if ksize_x % 2 == 0: + ksize_x += 1 + if ksize_y % 2 == 0: + ksize_y += 1 + return cv2.boxFilter(image, -1, (ksize_x, ksize_y)) + + def _bilateral_filter(self, image: np.ndarray, d: int = 3, + sigmaColor: int = 20, sigmaSpace: int = 15, + **kwargs) -> np.ndarray: + """Apply bilateral filter.""" + return cv2.bilateralFilter(image, d, sigmaColor, sigmaSpace) + + def _median_blur(self, image: np.ndarray, kernel_size: int = 3, + **kwargs) -> np.ndarray: + """Apply median blur.""" + if kernel_size % 2 == 0: + kernel_size += 1 + return cv2.medianBlur(image, kernel_size) + + def _horizontal_blur(self, image: np.ndarray, kernel_size: int = 3, + **kwargs) -> np.ndarray: + """Apply horizontal motion blur.""" + if kernel_size % 2 == 0: + kernel_size += 1 + kernel = np.zeros((kernel_size, kernel_size)) + kernel[int((kernel_size - 1) / 2), :] = np.ones(kernel_size) + kernel /= kernel_size + return cv2.filter2D(image, -1, kernel) + + def _vertical_blur(self, image: np.ndarray, multi: int = 30, + **kwargs) -> np.ndarray: + """Apply vertical motion blur.""" + kernel_size = max(3, multi) + if kernel_size % 2 == 0: + kernel_size += 1 + kernel = np.zeros((kernel_size, kernel_size)) + kernel[:, int((kernel_size - 1) / 2)] = np.ones(kernel_size) + kernel /= kernel_size + return cv2.filter2D(image, -1, kernel) + + # ==================== Glitch Operations ==================== + + def _glitch(self, image: np.ndarray, glitch_intensity: int = 10, + glitch_frequency: int = 5, **kwargs) -> np.ndarray: + """Apply glitch effect.""" + rows, cols = image.shape[:2] + glitch_image = image.copy() + + for i in range(0, rows, max(1, glitch_frequency)): + shift = np.random.randint(-glitch_intensity, glitch_intensity + 1) + glitch_image[i] = np.roll(image[i], shift, axis=0) + + return glitch_image + + def _channel_shift(self, image: np.ndarray, shift_value: int = 5, + **kwargs) -> np.ndarray: + """Shift color channels.""" + result = image.copy() + result[:, :, 0] = np.roll(image[:, :, 0], shift_value, axis=1) + return result + + def _channel_shift_pro(self, image: np.ndarray, + shift_value_b: int = 5, shift_value_g: int = -5, + shift_value_r: int = 2, + direction_b: str = "horizontal", + direction_g: str = "horizontal", + direction_r: str = "vertical", + **kwargs) -> np.ndarray: + """Advanced channel shifting with per-channel control.""" + result = image.copy() + + axis_b = 1 if direction_b == "horizontal" else 0 + axis_g = 1 if direction_g == "horizontal" else 0 + axis_r = 1 if direction_r == "horizontal" else 0 + + result[:, :, 0] = np.roll(image[:, :, 0], shift_value_b, axis=axis_b) + result[:, :, 1] = np.roll(image[:, :, 1], shift_value_g, axis=axis_g) + result[:, :, 2] = np.roll(image[:, :, 2], shift_value_r, axis=axis_r) + + return result + + # ==================== Pixel Operations ==================== + + def _pixelate(self, image: np.ndarray, block_size: int = 5, + **kwargs) -> np.ndarray: + """Apply pixelation effect.""" + height, width = image.shape[:2] + block_size = max(2, block_size) + + num_blocks_x = width // block_size + num_blocks_y = height // block_size + + if num_blocks_x == 0 or num_blocks_y == 0: + return image + + pixelated = np.zeros( + (num_blocks_y * block_size, num_blocks_x * block_size, 3), + dtype=np.uint8 + ) + + for y in range(num_blocks_y): + for x in range(num_blocks_x): + start_x = x * block_size + end_x = (x + 1) * block_size + start_y = y * block_size + end_y = (y + 1) * block_size + block = image[start_y:end_y, start_x:end_x] + pixelated[start_y:end_y, start_x:end_x] = block.mean(axis=(0, 1)) + + return pixelated + + def _pixelate_pro(self, image: np.ndarray, block_size: int = 5, + method: str = "mean", intensity: float = 1.0, + block_shape: str = "square", **kwargs) -> np.ndarray: + """Advanced pixelation with multiple methods.""" + height, width = image.shape[:2] + block_size = max(2, block_size) + + if block_shape == "rectangular": + block_w = block_size + block_h = max(2, block_size // 2) + else: + block_w = block_h = block_size + + num_blocks_x = width // block_w + num_blocks_y = height // block_h + + if num_blocks_x == 0 or num_blocks_y == 0: + return image + + pixelated = np.zeros( + (num_blocks_y * block_h, num_blocks_x * block_w, 3), + dtype=np.uint8 + ) + + for y in range(num_blocks_y): + for x in range(num_blocks_x): + start_x = x * block_w + end_x = (x + 1) * block_w + start_y = y * block_h + end_y = (y + 1) * block_h + block = image[start_y:end_y, start_x:end_x] + + if method == "median": + color = np.median(block, axis=(0, 1)) + elif method == "mode": + # Approximate mode using histogram + color = block.mean(axis=(0, 1)) + else: # mean + color = block.mean(axis=(0, 1)) + + color = np.clip(color * intensity, 0, 255).astype(np.uint8) + pixelated[start_y:end_y, start_x:end_x] = color + + return pixelated + + # ==================== Noise Operations ==================== + + def _gaussian_noise(self, image: np.ndarray, mean: int = 0, + std_dev: int = 5, **kwargs) -> np.ndarray: + """Add Gaussian noise to image.""" + noise = np.random.normal(mean, std_dev, image.shape).astype(np.int16) + noisy = np.clip(image.astype(np.int16) + noise, 0, 255).astype(np.uint8) + return noisy + + # ==================== Morphology Operations ==================== + + def _dilate(self, image: np.ndarray, kernel_size: int = 5, + **kwargs) -> np.ndarray: + """Apply dilation.""" + kernel = np.ones((kernel_size, kernel_size), np.uint8) + return cv2.dilate(image, kernel, iterations=1) + + def _dilate_pro(self, image: np.ndarray, kernel_size: int = 5, + anchor: int = -1, iterations: int = 1, + border_value: int = 0, **kwargs) -> np.ndarray: + """Advanced dilation with more control.""" + kernel = np.ones((kernel_size, kernel_size), np.uint8) + anchor_pt = (anchor, anchor) if anchor >= 0 else (-1, -1) + return cv2.dilate( + image, kernel, + anchor=anchor_pt, + iterations=iterations, + borderType=cv2.BORDER_CONSTANT, + borderValue=border_value + ) + + def _erode(self, image: np.ndarray, kernel_size: int = 5, + anchor: int = -1, iterations: int = 1, + border_type: int = 0, border_value: int = 0, + **kwargs) -> np.ndarray: + """Apply erosion.""" + kernel = np.ones((kernel_size, kernel_size), np.uint8) + anchor_pt = (anchor, anchor) if anchor >= 0 else (-1, -1) + border_types = [ + cv2.BORDER_CONSTANT, cv2.BORDER_REPLICATE, + cv2.BORDER_REFLECT, cv2.BORDER_WRAP, cv2.BORDER_REFLECT_101 + ] + bt = border_types[min(border_type, len(border_types) - 1)] + return cv2.erode( + image, kernel, + anchor=anchor_pt, + iterations=iterations, + borderType=bt, + borderValue=border_value + ) + + def _super_effect(self, image: np.ndarray, blur_radius: int = 5, + morphology_iterations: int = 2, pyramid_levels: int = 3, + **kwargs) -> np.ndarray: + """Apply super effect (blur + morphology + pyramid).""" + blurred = cv2.boxFilter(image, -1, (blur_radius, blur_radius)) + + kernel = np.ones((5, 5), np.uint8) + morphed = cv2.morphologyEx( + blurred, cv2.MORPH_GRADIENT, kernel, + iterations=morphology_iterations + ) + + height, width = image.shape[:2] + result = cv2.pyrDown(morphed, dstsize=(width // 2, height // 2)) + result = cv2.pyrUp(result, dstsize=(width, height)) + + return result + + +# Singleton instance +_filter_engine = None + + +def get_filter_engine() -> FilterEngine: + """Get the singleton FilterEngine instance.""" + global _filter_engine + if _filter_engine is None: + _filter_engine = FilterEngine() + return _filter_engine diff --git a/filter_manager.py b/filter_manager.py new file mode 100644 index 0000000..23fe49f --- /dev/null +++ b/filter_manager.py @@ -0,0 +1,243 @@ +""" +Filter Manager Module + +This module handles loading, saving, and managing filter configurations +from the filters.json file. It provides a clean interface for the UI +to interact with filter definitions. +""" + +import json +import os +from typing import Dict, Any, Optional, Callable + +from filter_engine import get_filter_engine, FilterEngine + + +FILTERS_JSON_PATH = os.path.join(os.path.dirname(__file__), "filters.json") + + +class FilterManager: + """Manages filter configurations from JSON file.""" + + def __init__(self, json_path: str = FILTERS_JSON_PATH): + """ + Initialize the FilterManager. + + Args: + json_path: Path to the filters.json file + """ + self.json_path = json_path + self._filters_config: Dict[str, Dict[str, Any]] = {} + self._engine: FilterEngine = get_filter_engine() + self.load_filters() + + def load_filters(self) -> Dict[str, Dict[str, Any]]: + """ + Load filter configurations from the JSON file. + + Returns: + Dictionary containing all filter configurations + """ + try: + with open(self.json_path, 'r', encoding='utf-8') as f: + self._filters_config = json.load(f) + except FileNotFoundError: + self._filters_config = {} + except json.JSONDecodeError as e: + raise ValueError(f"Error parsing filters.json: {e}") + + return self._filters_config + + def save_filters(self) -> None: + """Save current filter configurations to the JSON file.""" + with open(self.json_path, 'w', encoding='utf-8') as f: + json.dump(self._filters_config, f, indent=4, ensure_ascii=False) + + def get_categories(self) -> list: + """ + Get all available filter categories. + + Returns: + List of category names + """ + return list(self._filters_config.keys()) + + def get_filters_in_category(self, category: str) -> Dict[str, Any]: + """ + Get all filters in a specific category. + + Args: + category: The category name + + Returns: + Dictionary of filter configurations in the category + """ + return self._filters_config.get(category, {}) + + def get_filter(self, category: str, filter_name: str) -> Optional[Dict[str, Any]]: + """ + Get a specific filter configuration. + + Args: + category: The category name + filter_name: The filter name + + Returns: + Filter configuration dictionary or None if not found + """ + return self._filters_config.get(category, {}).get(filter_name) + + def get_filter_info(self, category: str, filter_name: str) -> Optional[Dict[str, Any]]: + """ + Get filter info including the apply function. + + Args: + category: The category name + filter_name: The filter name + + Returns: + Filter info dictionary with 'apply' and 'parameters' keys + """ + filter_config = self.get_filter(category, filter_name) + if not filter_config: + return None + + operation = filter_config.get("operation") + if not operation: + return None + + # Create an apply function that uses the filter engine + def apply_func(image, **params): + return self._engine.apply_operation(image, operation, **params) + + return { + "apply": apply_func, + "parameters": filter_config.get("parameters", {}), + "description": filter_config.get("description", "") + } + + def get_available_operations(self) -> list: + """ + Get list of all available filter operations. + + Returns: + List of operation names + """ + return self._engine.get_available_operations() + + def add_filter(self, category: str, filter_name: str, + operation: str, description: str = "", + parameters: Optional[Dict[str, Any]] = None) -> bool: + """ + Add a new filter to the configuration. + + Args: + category: The category name (created if doesn't exist) + filter_name: The name of the new filter + operation: The operation type (must be a valid operation) + description: Description of the filter + parameters: Dictionary of parameter configurations + + Returns: + True if successful, False otherwise + """ + # Validate operation exists + if operation not in self._engine.get_available_operations(): + raise ValueError(f"Unknown operation: {operation}") + + if category not in self._filters_config: + self._filters_config[category] = {} + + self._filters_config[category][filter_name] = { + "operation": operation, + "description": description, + "parameters": parameters or {} + } + + self.save_filters() + return True + + def remove_filter(self, category: str, filter_name: str) -> bool: + """ + Remove a filter from the configuration. + + Args: + category: The category name + filter_name: The filter name + + Returns: + True if successful, False if filter not found + """ + if category in self._filters_config and filter_name in self._filters_config[category]: + del self._filters_config[category][filter_name] + if not self._filters_config[category]: + del self._filters_config[category] + self.save_filters() + return True + return False + + def validate_parameters(self, parameters: Dict[str, Any]) -> tuple: + """ + Validate parameter configurations. + + Args: + parameters: Dictionary of parameter configurations + + Returns: + Tuple of (is_valid: bool, error_message: str) + """ + if not isinstance(parameters, dict): + return False, "Parameters must be a dictionary" + + for param_name, param_config in parameters.items(): + if not isinstance(param_name, str) or not param_name: + return False, "Parameter name must be a non-empty string" + + if not isinstance(param_config, dict): + return False, f"Parameter '{param_name}' config must be a dictionary" + + if "options" in param_config: + if not isinstance(param_config["options"], list) or len(param_config["options"]) == 0: + return False, f"Parameter '{param_name}': 'options' must be a non-empty list" + if "init" not in param_config: + return False, f"Parameter '{param_name}': missing 'init' value" + if param_config["init"] not in param_config["options"]: + return False, f"Parameter '{param_name}': 'init' must be one of the options" + else: + required_keys = ["min", "max", "init", "interval"] + for key in required_keys: + if key not in param_config: + return False, f"Parameter '{param_name}': missing required key '{key}'" + + try: + min_val = float(param_config["min"]) + max_val = float(param_config["max"]) + init_val = float(param_config["init"]) + interval = float(param_config["interval"]) + except (TypeError, ValueError): + return False, f"Parameter '{param_name}': values must be numeric" + + if min_val > max_val: + return False, f"Parameter '{param_name}': 'min' cannot be greater than 'max'" + if init_val < min_val or init_val > max_val: + return False, f"Parameter '{param_name}': 'init' must be between 'min' and 'max'" + if interval <= 0: + return False, f"Parameter '{param_name}': 'interval' must be positive" + + return True, "" + + +_filter_manager: Optional[FilterManager] = None + + +def get_filter_manager() -> FilterManager: + """ + Get the singleton FilterManager instance. + + Returns: + The FilterManager instance + """ + global _filter_manager + if _filter_manager is None: + _filter_manager = FilterManager() + return _filter_manager diff --git a/filters.json b/filters.json new file mode 100644 index 0000000..3083c8e --- /dev/null +++ b/filters.json @@ -0,0 +1,181 @@ +{ + "Color": { + "escala_grises": { + "operation": "grayscale", + "description": "Convierte la imagen a escala de grises", + "parameters": {} + }, + "indie": { + "operation": "indie_effect", + "description": "Efecto retro/indie con bordes y ruido", + "parameters": { + "blur_kernel_size": {"min": 3, "max": 15, "init": 5, "interval": 2}, + "canny_threshold1": {"min": 50, "max": 150, "init": 100, "interval": 10}, + "canny_threshold2": {"min": 100, "max": 250, "init": 200, "interval": 10}, + "color_adjustment": {"min": 0.5, "max": 2.0, "init": 1.5, "interval": 0.1}, + "noise_level": {"min": 10, "max": 30, "init": 20, "interval": 5} + } + }, + "matiz": { + "operation": "hue_shift", + "description": "Cambia el matiz/tono de los colores", + "parameters": { + "hue_shift": {"min": -180, "max": 180, "init": 0, "interval": 1} + } + }, + "saturador": { + "operation": "saturate_color", + "description": "Aumenta la saturación de un color específico", + "parameters": { + "color": {"options": ["red", "green", "blue", "yellow", "cyan", "magenta"], "init": "red"}, + "saturation_scale": {"min": 0, "max": 100, "init": 50, "interval": 1}, + "brightness_boost": {"min": 0, "max": 100, "init": 50, "interval": 1} + } + } + }, + "Desenfoque": { + "bilateral": { + "operation": "bilateral_filter", + "description": "Filtro bilateral que preserva bordes", + "parameters": { + "d": {"min": 1, "max": 10, "init": 3, "interval": 1}, + "sigmaColor": {"min": 1, "max": 100, "init": 20, "interval": 1}, + "sigmaSpace": {"min": 1, "max": 100, "init": 15, "interval": 1} + } + }, + "box": { + "operation": "box_blur", + "description": "Desenfoque de caja", + "parameters": { + "ksize_x": {"min": 3, "max": 15, "init": 5, "interval": 2}, + "ksize_y": {"min": 3, "max": 15, "init": 5, "interval": 2} + } + }, + "desenfoque_gausiano": { + "operation": "gaussian_blur", + "description": "Desenfoque gaussiano suave", + "parameters": { + "kernel_size": {"min": 1, "max": 31, "init": 15, "interval": 2}, + "sigma": {"min": 1, "max": 10, "init": 5, "interval": 1} + } + }, + "desenfoque_horizontal": { + "operation": "horizontal_blur", + "description": "Desenfoque de movimiento horizontal", + "parameters": { + "kernel_size": {"min": 1, "max": 101, "init": 3, "interval": 2} + } + }, + "desenfoque_vertical": { + "operation": "vertical_blur", + "description": "Desenfoque de movimiento vertical", + "parameters": { + "multi": {"min": 1, "max": 100, "init": 30, "interval": 1} + } + }, + "mediana": { + "operation": "median_blur", + "description": "Desenfoque de mediana para eliminar ruido", + "parameters": { + "kernel_size": {"min": 1, "max": 101, "init": 3, "interval": 2} + } + } + }, + "Glitch": { + "glitch": { + "operation": "glitch", + "description": "Efecto glitch con líneas desplazadas", + "parameters": { + "glitch_intensity": {"min": 1, "max": 50, "init": 10, "interval": 1}, + "glitch_frequency": {"min": 1, "max": 50, "init": 5, "interval": 1} + } + }, + "shift": { + "operation": "channel_shift", + "description": "Desplazamiento de canal de color", + "parameters": { + "shift_value": {"min": -50, "max": 50, "init": 5, "interval": 1} + } + }, + "shift_pro": { + "operation": "channel_shift_pro", + "description": "Desplazamiento avanzado de canales RGB", + "parameters": { + "shift_value_b": {"min": -50, "max": 50, "init": 5, "interval": 1}, + "shift_value_g": {"min": -50, "max": 50, "init": -5, "interval": 1}, + "shift_value_r": {"min": -50, "max": 50, "init": 2, "interval": 1}, + "direction_b": {"options": ["horizontal", "vertical"], "init": "horizontal"}, + "direction_g": {"options": ["horizontal", "vertical"], "init": "horizontal"}, + "direction_r": {"options": ["horizontal", "vertical"], "init": "vertical"} + } + } + }, + "Pixel": { + "pixelar": { + "operation": "pixelate", + "description": "Efecto de pixelado básico", + "parameters": { + "block_size": {"min": 2, "max": 50, "init": 5, "interval": 1} + } + }, + "pixelar_pro": { + "operation": "pixelate_pro", + "description": "Pixelado avanzado con múltiples métodos", + "parameters": { + "block_size": {"min": 2, "max": 50, "init": 5, "interval": 1}, + "method": {"options": ["mean", "median", "mode"], "init": "mean"}, + "intensity": {"min": 0.1, "max": 2.0, "init": 1.0, "interval": 0.1}, + "block_shape": {"options": ["square", "rectangular"], "init": "square"} + } + } + }, + "Ruido": { + "ruido_gausiano": { + "operation": "gaussian_noise", + "description": "Añade ruido gaussiano a la imagen", + "parameters": { + "mean": {"min": -100, "max": 100, "init": 0, "interval": 10}, + "std_dev": {"min": 1, "max": 10, "init": 5, "interval": 1} + } + } + }, + "Morfología": { + "dilatar": { + "operation": "dilate", + "description": "Operación de dilatación morfológica", + "parameters": { + "kernel_size": {"min": 2, "max": 50, "init": 5, "interval": 1} + } + }, + "dilatar_pro": { + "operation": "dilate_pro", + "description": "Dilatación avanzada con más control", + "parameters": { + "kernel_size": {"min": 2, "max": 50, "init": 5, "interval": 1}, + "anchor": {"min": -1, "max": 10, "init": -1, "interval": 1}, + "iterations": {"min": 1, "max": 10, "init": 1, "interval": 1}, + "border_value": {"min": 0, "max": 255, "init": 0, "interval": 1} + } + }, + "erosionar": { + "operation": "erode", + "description": "Operación de erosión morfológica", + "parameters": { + "kernel_size": {"min": 2, "max": 50, "init": 5, "interval": 1}, + "anchor": {"min": -1, "max": 10, "init": -1, "interval": 1}, + "iterations": {"min": 1, "max": 10, "init": 1, "interval": 1}, + "border_type": {"min": 0, "max": 4, "init": 0, "interval": 1}, + "border_value": {"min": 0, "max": 255, "init": 0, "interval": 1} + } + }, + "super_efecto": { + "operation": "super_effect", + "description": "Combinación de blur, morfología y pirámide", + "parameters": { + "blur_radius": {"min": 1, "max": 10, "init": 5, "interval": 1}, + "morphology_iterations": {"min": 1, "max": 5, "init": 2, "interval": 1}, + "pyramid_levels": {"min": 1, "max": 5, "init": 3, "interval": 1} + } + } + } +} diff --git a/filters/Color/__init__.py b/filters/Color/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/filters/Color/__pycache__/__init__.cpython-312.pyc b/filters/Color/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 05b96971d6852c1979cb41687f2d04cf0d2cf17d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmX@j%ge<81fA->X(0MBh(HIQS%4zb87dhx8U0o=6fpsLpFwJVxg=Y~gche36~~lS z#JJ>_=jG%lrWD8c7o_Gnhw0`fX6D7XhkEMf=BI?jq-ExmfK(>u=j0c~#K&jmWtPOp j>lIY~;;;cKD9uT=D`Ev2$_T{8AjU^#Mn=XWW*`dyB^D`* diff --git a/filters/Color/__pycache__/color.cpython-312.pyc b/filters/Color/__pycache__/color.cpython-312.pyc deleted file mode 100644 index 54376b6bd1b6a22ffc9a14e06fa053efbfa7ac2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmZ8dzfZzY5WYvjAQez2b-}d*Ce+Ok6U2mQ6cQ8@NfSc)gOn87yjRgC4h$|#jyU)a zpnr;s#z9lvoZO6TOx~;17{27byLb1!d++WcnM?rUZtc(*p#R{95RqvxUcq1p8fe6V zI%ttAs1r>l_pjCtdtyGsCAeV-ZGLkG_tQH^Fd}71 z@CCIe`*F4B7H!L>eyUhGs8ky}rK()27EUGN$IMQ%r8EBM`lO(4DK(~3rt~h9UHiOi z*-ecp6-VzDPqLk+*;Puly==$UjupeST&`4k<6quzJb$6-IF{GY4MSI5%J;=*W2;Dg zco$RG`9c0BcFSHexu;C-S(G26@)(Pta{7;8Cw#;qctCkD0-8ieMNacb{!&YK8_Y5_ zoiiy`&lRZItZNuWFt{3y`Ow3@{8IM$h^Z__mD_&(S&(w9I8d4t4v Nl#tEw93hKAyC1?ZXodg) diff --git a/filters/Color/__pycache__/escala_grises.cpython-312.pyc b/filters/Color/__pycache__/escala_grises.cpython-312.pyc deleted file mode 100644 index b6989fc69f6e7f402e6cdbc1a5bf01728f5b5829..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 530 zcmZ8dK}!Nr5T56*Vs2`5s*_zk7-+YUpf(Vr6fA>eVOe&cqO7jGw^>354?%S5XovoQ z^iw)ibf~s&ow`MP^VGaeBm7{#nK$#jd2i+}9*+Uy^?t8@f&N1rrU*>}e+h#jSYQzw zYG8%Vp++nTIn*a<`BTVBCYh*(lhGtJe+Ov@?q97n?TPs*&cL09&=NOia67rP4hs0PjlTDu0>34&U(SRQ55IeNu7{a?Zyhn1cQz*o_!<2z_887y*l;V~U_T zBrn;ZuKxQY-7;K*B`_O7<6)!$yeP>)otBtL1pf&%GmXa4JPvnk(+!qHo!AOrMEa5l SA#V`-mI+z&BZMpj?S23g6l;qB diff --git a/filters/Color/__pycache__/inidie.cpython-312.pyc b/filters/Color/__pycache__/inidie.cpython-312.pyc deleted file mode 100644 index 330e32eaeded29e27586aa4e8c2ef0dd11a9bc18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1747 zcmb_cO>7%Q6yEjzcx`Wfy7^U7QuhZEw@OhHNLo~pA5(?2Q9^!Pl*Gc?dN$6w>$N+( z4z;zBd_Y2+pci6L4;*?(l><43Tal26#AO#FIht~a#K{*F2nUo4%sAfIf&&+xwBP&Y z?R)dy%)FT&nwtC+7V+cRwPzef{Xzz=cxvQ0h{+ZOC_rn}%BiMTJizl2wZZ@fBb%c` zY=yYb#(AP@f@h={{2hyUt}V)<0iDPHf#=}>ML`aanS2vPZPF&~h9uprt^sQcM_Tk6 z&bUKn!?MXYM6jo}Mzna#V=-sm-Ujc+`f4GT*E|bO3TO0;?9^)76wH8YOo}e6Ym2q` zJYF+kEH;n#)e^{vFyH~KTTQa19^o>xdJ1HeY;|pQy;IwrO<#kbRR8a?WQWV@8h9+u zqLQ6;i_89pdi?*fA3n>U$LShZzv;7li&Ti4t&mNH1S({CmA^2AM8w25hLAWVXAMJ@ z^+_!Y5f_$qJ%@z&h8a$4X^5J`b2D=bv8jcL<)O)`1?15)h>dG%2Jy0C<}wQ6vZ`+0 zL_CyrkWL|9Ps5a~AA_SQEJfE;%_N@H zZHcP7if6x`$stkBWVBpNSzA-$CcJ<%kUL`hilIOqdb^4j9dB>((U0EXfzVx?aD-QE zq5BV>YLUv#eNJ;Cja+P9|d*Y565l$vJ?Q}}07>!;J7On;WJFT8Hwm?(*pg*(`K zu6;{u6yb+uJ&}`k(aMJy*BicY5B~qwU)>&dnM7y=7-~+38uf zSEKd^a!GnnmQ+Vle@bfQBy^InBpHQ!Bv}hdRu<1Y;`!qISAp%w*Q?K0OX97vIPQq! z-$~zhf7@LW?-uU-EVYwN#cSJ~(=)jJX!mkS8Y$d640PIT=P_O}6(`^dxoFiV>0M0l zqekQAWB>~|z&~Yy7YM)y{8jD)0U*|R6KDo4ptWlEgL6Q_5yZdkDW}r7o!e)TKO;jq zrI7QekOUQS%`4;ugk3Ng@~|Kmt%pLNQ~f zO%wyNDZ^oG?Zuz5hyk~X(0(uw+P$w*cSeKVk%tKcWPVuja-jdP#Op&!#dtNFLH^k5;q) sqr?;8HtzJSro0W`#D3(ZFv!x57@DSEQvR2e^gG)@cO75%(WkTj4VM{~f&c&j diff --git a/filters/Color/__pycache__/matiz.cpython-312.pyc b/filters/Color/__pycache__/matiz.cpython-312.pyc deleted file mode 100644 index 7ae3e641c97b70ccb0e08bd1350bad0ece411200..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1395 zcmb7EO=uKJ6t4c6F^&`eNQ@INCFGC+6EL7GK@#G)#6UJ#65}B>47FV~V@Y@QSY6#Z zAq#U*5Kp?7?7>4;_K;=wwtMjIMG%%9JSgo&@M3Pm#*>%$s(U8Jcv$v9y?#~o>U&?k z_qty6_ZJYz?f2i+rfr1Y=}Aw_uCar`*gzCf!$Ygt*H|^Fc^9o()B6 zDLu=wY)?PY%8aS8VRBfhyWK42V_z4+F;wT#MkbRMo#z#tXk|G)`m9`=diYk2f(#LAd5 z90`KCPwGtINO6y^5kUj&5v(K;VJ(n8arv`91-`m`eY}uj&B%Hp$)^7zHpTqlptj69in za%=1C=4^ZT+*Y$a{LRx!yENUJdq93V@o?sTy=|ZRkVmIV8_pwVv;4>MtFfu}*wo9x z>DI#ABc*@r(hh_`u2wxc1V;K(odyGbOM{x!qBhOZJU#T-<_0&QJ^P}^u$`v-3X3D= z#SD8vh#N#c`^9HUW_H@O2fHEb#IMRtxvM7eiojsFJP<@e3*CwshH@*;#QzAO>yi|J z3jr#SZ>e7)KEJ!WD|KSC9?`Fb?h&60P~=WxbHP>82VKl$OuXNa9MaCmwCV?y9IG=` z%}Q9ML=mY|Gz=pg0o6jC*CPnz?S>;{PVNz(tO77fK6rT0|`Y(23hhjr1KPlk0DJ6mkB1B693RE*?#8omHrQ)3yABxE zk}Fjek*Z>(N=c;JID=nX*(8VLQUE07FR$6i5 z5Va^4rJ{^E>`J*XpW?6pU4$+{m!T`mQc+2vqT5z%E&?pa(Z_=b#UWbJQ}lMlQraQf zz!q(f?OkDOgjV#~?m2H0cT3ynWW=E__6f-HJF~XinZfQ$#}OiZUGaJxtB$taUm`=W z-Ppr2E&98xgB>de*^=$D{n&dA)^bfmXfEW;bhH(A@Z2T`b+nURd+()JM3im*e~u_y z{Z2k}n;q2AwuC*na{|&UtmPN1<*wC4JDd)J)0J#F#XGxD?CG-qw;SBkb%VD$EFIn6 z#ef|+hhn~on2Ru)HCBT$*II?)driEH;i)FZ41dwY?A!^fEAV9(lDM0gQ4ly|bVs5* z(J&QLMAHl^OsDf2l?qxqoxMVZWI;8koKaJnVX14ZPMR6~%~%4IPtFffB{^@6Wz$(g z{bOfNpP5XIj!zDqJa>VLW-hH;RMw42)kp!Wn#*YhrhFO#d8T#iif(EN@Z(jCVb-(? zIgPrE92N7rVI6|-X&pW?8UgPXIS>y)fLM@8)!o;BwaO)qYiawg^GsjuOlAS^=@VyWgOP_TH7KpK!Rp#mGEo~B^~ z_i4tgY9uwB=rH!Q#~Kk#R$$v-YR^w3N8(eaM$Gv9)%fx36(gNhu^B%D`!aT6Fr(^5 ze0=KEU?z*t$7l4k#Vi|p8*eTTXUV&{0`%CjbzItd&`K@|ywCt^ix0R1P@ylVUbb39E{DIPmdUSUsUyTkvMJ{p6LeGuaP+It;Rl_0Z3UYN69L<)hNE zM~e49S~Av_ZFzq=SyQ6S+tc)JkAPM5^=~3yM=HlEmv8T=9IizMSA|V5yeYC6zZGBI zdEj31!SHx>c)ZqsvNZA78(9@18?fIf$%NBbb+Lzi<2?}Y`{PmwyT0WxkA-Cy7O{k7 ztYG)D@B;QM^EwB`?VX^WoJ!P;#?GAhfP=7SKjxxxHpgzYNd?`|tw#Mds(5XP8L>Ay zCQv@38z9uHWCRAN0`;Sjc{Ls5$(zh 1 else masks[0] - - s = cv2.add(s, (saturation_scale * (color_mask // 255)).astype(np.uint8)) - v = cv2.add(v, (brightness_boost * (color_mask // 255)).astype(np.uint8)) - - s = np.clip(s, 0, 255) - v = np.clip(v, 0, 255) - - enhanced_hsv_image = cv2.merge([h, s, v]) - - enhanced_image = cv2.cvtColor(enhanced_hsv_image, cv2.COLOR_HSV2BGR) - - return enhanced_image - -def get_filter_data(): - return { - "parameters": { - "color": {"options": ["red", "green", "blue", "yellow", "cyan", "magenta"], "init": "red"}, - "saturation_scale": {"min": 0, "max": 100, "init": 50, "interval": 1}, - "brightness_boost": {"min": 0, "max": 100, "init": 50, "interval": 1} - } - } \ No newline at end of file diff --git a/filters/Desenfoque/__init__.py b/filters/Desenfoque/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/filters/Desenfoque/__pycache__/__init__.cpython-312.pyc b/filters/Desenfoque/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 039ad32b859f497fe49bdcea79f945b7e4b53600..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170 zcmX@j%ge<81fA->X(0MBh(HIQS%4zb87dhx8U0o=6fpsLpFwJVc_v%Mgche36~~lS z#JJ>_=jG%lrWD8c7o_Gnhw0`fX6D7XhkEMf=BI?jq-ExmfK;ZW7N_Q=B^G o=4F<|$LkeT{^GC!sw&M%wJTx;n#>5q#URE diff --git a/filters/Desenfoque/__pycache__/bilateral.cpython-312.pyc b/filters/Desenfoque/__pycache__/bilateral.cpython-312.pyc deleted file mode 100644 index 74e7c23d5b2c3b91fecab60f2027d04c17c04648..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 732 zcmZ8fzi-n(7`?L{Cv_5ql%mpzN(iP5txzUZAzCR2L=i!#lp(Sd9lmp9s7M_a`Of z0x#yJ&B(X{i&%v9*J}auLi@??)3qStuWWasuWT-q z2;FFJU)cY=*W+$lU~%ksTf%jPlgK<+pp9AqI3&OF#UJ_AL4NhUIDOf-K5c)`Z+k3uX~Beh~CSs)+^HN;ehcz{Ec zglP1@3jx?cYIvcSq&Y8y!4C6>9H*8LN;s;^EHsI`kqZDHh{O+(1q4(~OKp*~rkS)j zOPIvJ0AEt~0S?LU+|v0<<+Rq{J7a_L*7=>g{rcJBm-3+U>?;40HI}WBZWhcDF*D}G z0Nl%9h zdb29BB=ZW8G2)109~~i1UZW$-DIghO+n8!uqeYi$y=36g`}rM!F^a)?QO{SNs{>ev z;5)=a;*t+bk5_a^pCFqwroW+VlF(M~cCvOH_4hqL7Sc8n(+gNzB-t0rI~C3TiVTv% zvEP+a@MbDd1~k})x~lEnbqygzKzzBUSmB;nB^>;TW#Yy}8-V1&L~rOU1DN%8#PG5 diff --git a/filters/Desenfoque/__pycache__/desenfoque.cpython-312.pyc b/filters/Desenfoque/__pycache__/desenfoque.cpython-312.pyc deleted file mode 100644 index 04fdc0365241168e35f5a83c6272f0e17a781387..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 726 zcmZ9K&ubGw6vt=wN4m|X7Ng>iP@B>oT(F9QJ%~S08;b@bsP#}5!jPE_Yj-#4%qBJ= zLJ$5A_0U7~RPmByj~)dt4IT`u2f@?cqPg|dH~9hLgZ<2#;rrg3eQ&tH-YZ&DyE#=cV^K)dWg;+|tgr!7kseV05t&y4#E(z{? zHEyYq?)s$6B6E}RfO$pXHd&+#x9k%k6J@j6qkQS6y)GCRc75Akt-KDr3ZcS&T4lk5 zm&-nJ1AA@#(XwBmFKoy4LaC%ou)wKo)!0Aom1-kOk!saz6q)0&QpimxA`5cQey9Gw*Wkd?$0bc>AFC&CDHYxdDWrlElQOz>(Po zO8}SvrV3T5Ms=Fl)4-pi22lFbdy0#pL&-vvtP)OqCT|_f7RU1vvjDCEEG<%fHvo|B zNOJ=>j0`sbb)9&zd5_pvF7&e}(WdHyG diff --git a/filters/Desenfoque/__pycache__/desenfoque_gausiano.cpython-312.pyc b/filters/Desenfoque/__pycache__/desenfoque_gausiano.cpython-312.pyc deleted file mode 100644 index b212be1e81c66149dfc264ff9a7aee094201a1f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 735 zcmZ9K&ubGw6vt=wN4m+T7Ng>iP@B>oT(F9Q9z+FgW6@v)Z9SAg7&5bA?d~R>*~BJ9 z$ie@i9(srusd&k;N5PBWrNM(?^&oiKTQs+xe3Ks_KG@H^8Qy!}+4ttl>}&#o#Xmi^ z-(iG)$<5T1i8DM0&K{zOVi#>8s=P*9n5saffh{ePVfm6eiI$5FpWF{00rpS}w=l)c z7=qmGO9(aL^c4<3Z)`87O49>aTca#nX(=Bjm7gI?%||knOIS*%mg?8TW)XsEHt+n_nBK1PLqYYaLOJLvQQ?IIo?>kRoE1ai$Z<3uvU5LyA?u3;YpSG ztItDpV#|>m8WrF#3Wv9meaf@Zp2I-7D;2OZv zLe+D80ND*S$9ICzaD0fa6E~8tB9ddY6mH0jIN&|v@+t77gp3I1$}A|3TU4YZApAU> z%FBw&fHwMRobE3zA3QxweLmSs=lb&(JG^`C<6`g3+HYMwn;58iGB!ZE9vc<4R4z3~ r%^Q{aPw&{c4MVB9>@L3maq_3cDZoI*7=K5JKbnFc;Gq%2<9dGqmF=%z diff --git a/filters/Desenfoque/__pycache__/desenfoque_horizontal.cpython-312.pyc b/filters/Desenfoque/__pycache__/desenfoque_horizontal.cpython-312.pyc deleted file mode 100644 index f5f74fefe8606218924dfa90ec643d3f1da024cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 903 zcmZ8fO=uHA6rP!#O=EtnDTOM}(<3b2Db zsL->Ov#kD|k9}Mu`+d$!a)wQVxPQBk1Ex>Bf#BZ94AX*%0ht+iF>?LT1YkR*0l>NPGLS5n93uGCE1 zlUiY8T!xFL<#91SnbxFkmW(AXqpMul+{(LVjmvP~S$0*4R4R44F!3Na?{eYhHY&ME z=Y?%K26J;y%iNw=7%3U1otv6}JW_Jlvs^E^n`7MNcF|c|=Vy-jWk;Ab$My_sv|N=j zqg=MCd0s5?f+rGCO;vQC1Ju#+U}CGXS=miCRu6{9TZ8xN)5n?7Pg8qSpJ!T`NA>A9 zbK7?s`0d@sSTk|66~Fax3ZhrU42(H?zf_u1DfeTZd6bZ zn3|9(+X3m-U&4$qjZvnv5F2<$O>AP&I6NyOWkVPx4$`^eJaC1YeVZ##@j?ekQ>m3q z8^EYYYTBkJ!yr5t8-}%qL{jmF6kGt1gG=1Y_XNu`!!yKHxK*=vZvfQM&+x^=*pnG=m%j+|&T7CJ#x4|P+ebpam=N!?yAm8(KMrz>SQ*?$#J!&w;8y2T%g eYrs>##=Qh^qG61WP~->={U#KTbYeR0W&aDbhSRtJ diff --git a/filters/Desenfoque/__pycache__/desenfoque_vertical.cpython-312.pyc b/filters/Desenfoque/__pycache__/desenfoque_vertical.cpython-312.pyc deleted file mode 100644 index 02b6ed67da448dd007828df1eff7e1141d025d6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 943 zcmZ8g&ubGw7@gUjZDN|(+BWD#2^GXZ>!FCK1VLN09$G0jf;AA<%xu`W*{rjhn1mH_ z&_lhng+g=i5b>a;1^pAmtCyB~DAOW>r`&?M^`hT2KP-MQZ{F;@dEd-8v)>|-5Q0d4 zoio-kLcipo7fP43V?Z_#MHE|Tsng>ng(|Pml1f!TQo(W3l=AsZZKL9q6 z2N^n%JE`h^^RbT&#p_8>j`V4tY~!u&%9qLsw}HN(Pkg-9&E_kxbM#vAPW2{zOqD8} zo2u18gnBwdNr1ptd|W+;5ctDfCvxE)``7!35?|Spr-OKi;@W&13ypdehc#v9rrc=r z4@5j56x$J6mGOcr0)|!4y=zwmDcH;vs%d*d&6K7@&@e5J@u``(Dl{{%XPMCQMawfq z@EPMav(hD3ri;a5M~l-pQ%RR`H&rU9W(qHC@T6|)fy3(Pc* zKfV3*Q*JZ&edtDQ_VCi>57~FwkL!se}svaU#^bX#?nGK}_59L=ei&cuBW55kD_ij7b;)5JOq!r8~t+ zQ{B_Kw2*&?I}T7oKZ9fY;nDg+!~J~c%kACh-Il7H4YgDf9%vz=4IEp=H6D>>nqAC0 w6&?hBT*zay|1us06L3LAi{0We(8)*RMgdwX#`pk*4$#PNLhxietl^IPU+Bi#`Tzg` diff --git a/filters/Desenfoque/__pycache__/mediana.cpython-312.pyc b/filters/Desenfoque/__pycache__/mediana.cpython-312.pyc deleted file mode 100644 index e75707527e0817535e0d00ee203293124366557e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 704 zcmZ8e&ubGw6n?WaNn?H}25XfdwnD)@q+mf0B7tf_B^VF29?C)(GLtfPcazR;ViQux z!HXxowFeLJR`KG|KSA(fC`eg72nv#0Ft?t3lcW^-!TWf#eD9l`?|sc=l7LwG{-pYs z0Q@$G6Ijz`bOoC|5Fm&P8z8J#ut5Yxwpzqy10yR{PSobauqf8ZLE3{5ohJ~aLq1NG zyV!S#u-b6|LAP!Kv{AH6LNhtW3*&PJ9XNGZ1Dllw#(ZErwIQ2E)Z;r5tu&XTq$h>q z{(ZNhZ7Yf?k8jE-wI#JL-EyGXGB6|a^T&@Wcb)Y>>cDC4IIFdnzFXrWaGuts|KNGv zcY^r|Dwv3gxR`mz6j20INtB{woont%^QYRZSeq;d(vX?gF(jK|R?6470En3K zRTw3dk0VX)egHjdbQwb_Be<|B!}8?uWx+$PFXPv9SiudXF8oSdI!a&d-8xwNyzu2> zKUW-5Yc4sYEFB*Li^a!QHr3_{<6y=HXKp!8+{2I?t}N&|)S2A|S)?H)g#3WyKgP(? JC>RcVrzS zKx9KAgWakPk!-7mZixbBbrbW34lOLCRNcOVx%k{o>`5BPd}?UMR3?}fomfzl@)X3Z zVPfwLL^{-BmiipV(kI;vqWfV-`4{Xnkr9WM1(jGS-=bZ&mgVR{?_Fukg2aPDfO%C{?E*bl z!AF4-xIniQdCVnMoq(K1eiFbg!Vie%WUWA%Eb^$4DCD8wr9h8KqLh%>{V*IE9*Bj9 zgCX2lwtI)dBSCzo?DR)^`UZoc(RF~DXzp2>J4`j^w)|+gC<*CLixk&g9A4r0sPC*3jo0->>B_51p{v+Shuv zX1~b!mVrMF!Ugvs=kz!Zfm3jlnh=F@?}DC6ni)iIqC}-q8>o%CwO)^0_Xsmn8tg(H eWPx6Y)lA@!V;E*1xc5QRU!G$w72PhTT>T#z3kVhf diff --git a/filters/Desenfoque/__pycache__/shift.cpython-312.pyc b/filters/Desenfoque/__pycache__/shift.cpython-312.pyc deleted file mode 100644 index deb9c311e25a19f460f267816e88b5ba67d5496d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 695 zcmZ8e&ubGw6rS1HP2-xRAd0_=2(6X{E0}`^5ih9~g>KPW4`m??JF{_QGrP`gVgnI! z@Zf*YOAj6^dhpnz$AYJTXjm_TUUG}(){}4Y_{K_qYe=o?1p zmuco<&B*uykbT4u!!g=L%zA@%F(ZIh2YdF=jQJs%O`GCCntps6U>{|mJdGD~&G`Y; zF6!d0^$~xVDZD0KvW+~;_pIE~DWn2M!ge)3mv+=9r*iwbh_Ve4XIu%{@a)_XP1@jj zMW-|qG_EVkL`&zTFo_d2?+DYGwqfH{h?9-lonS|ErGxffu%5h@;$f!BAaCxWF9Y<@Z@2v2 zUGBTf@9rPU&r4rjzgHWauJj&_;V}vg%wqw_#IqG3;5KF!Bg_VL4h#}jnDK=JOJH!Q z=;v-q6>W0xK@W||LuSDIQZR)yg#@5`xh;qPZ{IPRrDEv5y2S$K<;0g;Ki|9z*D(@ DX}+pL diff --git a/filters/Desenfoque/__pycache__/stack.cpython-312.pyc b/filters/Desenfoque/__pycache__/stack.cpython-312.pyc deleted file mode 100644 index d6a3a7021e614851aec2cf61f666f8841a38103d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 660 zcmZ8fF>ljA6u$G>iJLa6R0SnvAp;DSC`cVERH?Ko>X0B*%1~KYhwogO+K#z%(xeJf z2KWJ;vUFf9V&NySvw&Etg8_*tTOeB}-Z_a>c+!3P?%n72-hJoK<#Gu@?7wXW&oM&3 z^kyE$j7(}khKM1CBXo$E@dh1YMgWZ-cFd8MHT}6-?@U+()?^)Eh%%@=jhEL|vs@qb z@rPOdwb3{Bk%OC#p@`pER|_(u{_(@8EgYh(Fs4UbS;sOw<*NJwvVt=&&r02|Uaw#6 zZg02;k_+i}y6#TvB#By-N%wKelkI0~F%1*<(ZSwY++t7NAdE7t+~JZZLF-kUyLrER zX-_TDG>v*54+8FILc63pNbSV{{isxjjkCtPI~O;;RqhWOmy5M8W^Dpz$k#BBB9PpO z+d#lV%nU}D$t+elH~-lb?c~OT5{ouJrd8z;UUD*%7?RLaB#ZA!ZpFyqwj0wj0 QJ1YG#4ZJ$B3wT=YFLH05ZU6uP diff --git a/filters/Desenfoque/__pycache__/super_queee.cpython-312.pyc b/filters/Desenfoque/__pycache__/super_queee.cpython-312.pyc deleted file mode 100644 index c8b7c309da23296ef650f6705f9994adf9f2d417..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1473 zcmZ`(U2NM_6uypsl7^&|{$&GER&Aq1s1ut6YgvUftzB2vq>wI~)JF5%`lfT@*xt2M znk_|<;DsmXQ$eZ(PpGJq=p-aM?SbxzOg!Ldl~&#D0SR96Rwz$+8qW=)Rn&TnYYF;n61d4%46b;0y|3O53Rrc%&gD6LX znuCerXr^IT*)CSuqa_UBqF=QgTf2>;LCVk0pP?-?=E6lyhYIR5EGn1=wyBuc3}=9b zawf5Enz}hx7+IjLv5CY-m*mmJ`QfpV_!a863S`*)#Gs;mQ?W1=VV$(1EmYKU${eOI z&FFa|6GheXHtpJzlK0x9tx%)nG*#B|Jl1U*W;Fz>a$Q2oVq##Ow(YO3iL*jk1@}f~ zh^_NhePy@6y&h}E^YIJsr;;`%c4~eh#X8naMYU5C7B((i?awKiks3{o_2*3WN-AA@ z6n1JD+t^5(xAHh;=PgWRU>KjW3N)lxmR^u?I*n%?(gRLx*6bevO6af7$Z~8c_T9|Y7Bwtac+i)&vcSI&Gp@Xf&E@QKpJXOUN*MEWX`zSZzg_LH+? zm9t~z_uT9DR>X0IPN?ZVx;(u!?e?DD82RP${mW(Ue!P6XG88N4f4}qSjys)t^rmj_&2brNOVSuj8My_p+P4_l!;aYxZH*O-w#C+;@{(9al@^I}!>V zafQe(w2WpMG=sgDwQs;XK;SFFsXmog1we6yp`c3pxWB;zE4;?RG`7TP&{AMOF-((9 z{Gd@~Z*YAs14AaZ@&-;?(kAb+Ch>i)f}QCVGUE!$md|7*yp9T!!?fsjXVzw!NlZs`~i6z e^4N=Jv)*@jj^m!9;8WE8j}YdL?DqJ%y88bysbPlz diff --git a/filters/Desenfoque/bilateral.py b/filters/Desenfoque/bilateral.py deleted file mode 100644 index fc05a28..0000000 --- a/filters/Desenfoque/bilateral.py +++ /dev/null @@ -1,13 +0,0 @@ -import cv2 as cv - -def apply_effect(image, d=3, sigmaColor=20, sigmaSpace=15): - return cv.bilateralFilter(image, d, sigmaColor, sigmaSpace) - -def get_filter_data(): - return { - "parameters": { - "d": {"min": 1, "max": 10, "init": 3, "interval": 1}, - "sigmaColor": {"min": 1, "max": 100, "init": 20, "interval": 1}, - "sigmaSpace": {"min": 1, "max": 100, "init": 15, "interval": 1} - } - } diff --git a/filters/Desenfoque/box.py b/filters/Desenfoque/box.py deleted file mode 100644 index 094c5a8..0000000 --- a/filters/Desenfoque/box.py +++ /dev/null @@ -1,13 +0,0 @@ -import cv2 - -def apply_effect(image, ksize_x, ksize_y): - blurred_image = cv2.boxFilter(image, -1, (ksize_x, ksize_y)) - return blurred_image - -def get_filter_data(): - return { - "parameters": { - "ksize_x": {"min": 3, "max": 15, "init": 5, "interval": 2}, - "ksize_y": {"min": 3, "max": 15, "init": 5, "interval": 2} - } - } diff --git a/filters/Desenfoque/desenfoque_gausiano.py b/filters/Desenfoque/desenfoque_gausiano.py deleted file mode 100644 index 31cdcdf..0000000 --- a/filters/Desenfoque/desenfoque_gausiano.py +++ /dev/null @@ -1,14 +0,0 @@ -import cv2 - -def apply_effect(image, kernel_size, sigma): - kernel_size = kernel_size + (kernel_size % 2 == 0) - return cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma) - -def get_filter_data(): - return { - "parameters": { - "kernel_size": {"min": 1, "max": 31, "init": 15, "interval": 2}, - "sigma": {"min": 1, "max": 10, "init": 5, "interval": 1} - } - } - \ No newline at end of file diff --git a/filters/Desenfoque/desenfoque_horizontal.py b/filters/Desenfoque/desenfoque_horizontal.py deleted file mode 100644 index 983a5b6..0000000 --- a/filters/Desenfoque/desenfoque_horizontal.py +++ /dev/null @@ -1,14 +0,0 @@ -import cv2 -import numpy as np -def apply_effect(image, kernel_size): - kernel_h = np.zeros((kernel_size, kernel_size)) - kernel_h[int((kernel_size - 1)/2), :] = np.ones(kernel_size) - kernel_h /= kernel_size - return cv2.filter2D(image, -1, kernel_h) - -def get_filter_data(): - return { - "parameters": { - "kernel_size": {"min": 1, "max": 101, "init": 3, "interval": 2} - } - } \ No newline at end of file diff --git a/filters/Desenfoque/desenfoque_vertical.py b/filters/Desenfoque/desenfoque_vertical.py deleted file mode 100644 index 74fb315..0000000 --- a/filters/Desenfoque/desenfoque_vertical.py +++ /dev/null @@ -1,15 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, multi): - kernel_v = np.zeros((multi, multi), dtype=np.float64) - kernel_v[:, int((multi - 1)/2)] = np.ones(multi) - kernel_v /= multi - return cv2.filter2D(image, -1, kernel_v) - -def get_filter_data(): - return { - "parameters": { - "multi": {"min": 1, "max": 100, "init": 30, "interval": 1} - } - } diff --git a/filters/Desenfoque/mediana.py b/filters/Desenfoque/mediana.py deleted file mode 100644 index 7fcae1f..0000000 --- a/filters/Desenfoque/mediana.py +++ /dev/null @@ -1,13 +0,0 @@ -import cv2 - -def apply_effect(image, kernel_size): - kernel_size = max(1, kernel_size) - kernel_size = kernel_size + (kernel_size % 2 == 0) - return cv2.medianBlur(image, kernel_size) - -def get_filter_data(): - return { - "parameters": { - "kernel_size": {"min": 1, "max": 101, "init": 3, "interval": 2} - } - } diff --git a/filters/Ruido/__init__.py b/filters/Ruido/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/filters/Ruido/__pycache__/__init__.cpython-312.pyc b/filters/Ruido/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 156ffe8d0bf8babf7deb1cb467229d89a2096763..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmX@j%ge<81fA->X(0MBh(HIQS%4zb87dhx8U0o=6fpsLpFwJVxg=Y~gche36~~lS z#JJ>_=jG%lrWD8c7o_Gnhw0`fX6D7XhkEMf=BI?jq-ExmfK(QhW~SuF#K&jmWtPOp j>lIY~;;;cKD9uT=D`Ev2$_T{8AjU^#Mn=XWW*`dyCJ!l( diff --git a/filters/Ruido/__pycache__/ruido.cpython-312.pyc b/filters/Ruido/__pycache__/ruido.cpython-312.pyc deleted file mode 100644 index 32628bfe6c3bdb748e394a00fb9927a317641e9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 973 zcmZWnO=uHA6rS1t{Iu9@t%?<_SV|5pda)u!sI8PBRDDJmnV6t(W*FO^T!s_Iqz;zxU0X_vTw)Ulf5wrk`ei zV1$0NMc24a(7X!44kCzP6D?!Hy+g|!;Q{#qP6%~ITgdPoHght3Y~4Ho*g+x6f$y~x zFp7LpD2hS%B$N|+SqSBrT91V|&KXX+1sc~} z*DR#cQ?d^PNru6##=t;bPJGqrSijUOdBfCrGsmk*z zuHLOA599Yrk5AO0o$(LjyJO|Cj}w)F%Iu+f=SZEZsZ(FLugYiTP@O3~IgSl~7lxal zjxr3ocK&E?!wx(*7!%=%Ktv+#2}B030J1o;#~B!!A6=kPSMxMWXV)_JTCG-{K=Rp0 z*uzc;l(!5UK-;8(VH*Kepz*r5u9!dw!F~ije-faCeknr@30=EUUaXEDsI^OT$Ja;7FRB9vxHkCck1Rx_hA73N z4J3)twlI_>+Ih<@coGP-Ti({}Ir>zYhPmZU{h`Nt!s_)^fCi5-{)yDTf`G3y<1#)| F{RcVu-v0mq diff --git a/filters/Ruido/__pycache__/ruido_gausiano.cpython-312.pyc b/filters/Ruido/__pycache__/ruido_gausiano.cpython-312.pyc deleted file mode 100644 index b905fa4e481a49e100070889aee3ab71618aefb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 980 zcmZWnO-vI(6rSz=(iW5jQ6nZ|B%}w3Uc?w)piKpJG+_=;?r6s1kWWM)i=6gT$-hAusjv|nw@6R$n zIfQ<*O@3b-(q~NTEtkU5C+Yp7)8_)z^(Dup3 z&h!4OCVu{Q2fD$A7rXvxWw z$@|(%U-x`%WlfuMR&CS4#Mhp?x;^=7)WU|XO}|WzS`L}lGKLv2=4{R&j^;5;E#jPS zVA~mU^E8fK*UYE%Oh!)!-X%!G8tN|q6wyIX-^TL#^4sa+MDg|ep~HC3Mshv5A0Mp7 z2e;-+u5zcG+>75WK0Z|YH%C5!&KaQ4r%aeQR?R|B;s*Zn&d{sUxd+J2- z=|ODZhcIvq(NUT~Th4yZEjWRH2lFC45r{~nZGp(36+mWpZAT0arVyQ>Q5SpI(%Jir z?M96TMxh|g9b6kM%~pDMxoY2|KeEsz z)kP^5ts_Z{Hp4og#)2ROdYvAJx diff --git a/filters/Ruido/__pycache__/sal_y_pimienta.cpython-312.pyc b/filters/Ruido/__pycache__/sal_y_pimienta.cpython-312.pyc deleted file mode 100644 index 72c264c73afa2f3c47b5423a805dfaf97e41c965..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1465 zcmcgsO>7fK6rS0gU7L`#n>31xkP-^1s6`W$7Np8T%^_}~iR2_r=>fU27Vp}OwccH3 z*95z^GkPI2TT*h(oOwdwU z__||4Q^Y%cps|E}+@2}4HQNKwSJ7%~z9#Qsl0B3btl}yvsIQM#6!fu15nWR$09OtRW+mvu>ELzwo2OjUu~V?qI7+f zpjxN~+^X#NbeN$w^9&6AuWMA=ncuKR{10oCXW_Ikw3<4%2gT(S6Q%G>+GPh+y!4f;>7+Af zTaIpc=_%K=$G;ga>eNo3J^$r!(J{VG=cwfi=2sjXvo4xIVF^qH%l<-#f8>;*pkGo4I~*fzH#%k-=tUuz?$+ zjmyiYe>%2uX?6Hvh!sQ~h!Rc2yp*Hn-7|OvUzysxgy89wq&JS-9n0@vO)L& z7NS6{xn%~g6bf!WPz=%rj3DjH@ENG1l4X9zjsj2o7oJGEC1Z^LK)u^U!?B$K74NFP E0tN*~6aWAK diff --git a/filters/Ruido/ruido_gausiano.py b/filters/Ruido/ruido_gausiano.py deleted file mode 100644 index f8f71d2..0000000 --- a/filters/Ruido/ruido_gausiano.py +++ /dev/null @@ -1,17 +0,0 @@ -import numpy as np - -def apply_effect(image, mean, std_dev): - h, w, c = image.shape - noise = np.random.normal(mean, std_dev, (h, w, c)) - noisy_image = np.clip(image + noise, 0, 255).astype(np.uint8) - return noisy_image - -def get_filter_data(): - return{ - "parameters": - { - - "mean": {"min": -100, "max": 100, "init": 0, "interval": 10}, - "std_dev": {"min": 1, "max": 10, "init": 5, "interval": 1} - } } - diff --git a/filters/__init__.py b/filters/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/filters/__pycache__/__init__.cpython-312.pyc b/filters/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0d24a98e023bcd84677c153e9a94b7431dfb367d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmX@j%ge<81fA->X(0MBh(HIQS%4zb87dhx8U0o=6fpsLpFwJVIVD@ggche36~~lS z#JJ>_=jG%lrWD8c7o_Gnhw0`fX6D7XhkEMf=BFqm=NDAQq-ExmfKGAV5&cP(`ZD*@=uUzN>u~8Yx4E z{tc9&L%RhbBqWyZS&&$$7^rUTlr1Psop`=nN@yo6-RF1b=kM9?`|jJwh=pLN=96-0 zB6O->`Xdns-B}3s5Jl8*(1t;iG_h-JB&fNIHq5$_PqlU8Q#%>w>nRNn@S*M^z#j4q zn45Byljo4#B)i zDoiX`QF6f}l2m)&3V1bI!jxeqd`T!-7)+uC{DM&K>UA3yvDf;v%^dw&V>TC*P`nku zUe&8isQb@;#Vb{^;ySJ@;388sPHf)#A|T&y9l~x2RWa}}<-tm?jLyDHvIYt4KS*3j znSc|km`XA~kmCHlQJ>Cln@@xZyB3a}x9tJ6k;!~AOhv5=4H{a1?-Iw+k(3rhYjCz? z7fS1jNmZ(Cmx}IN;kbmV(ld{Vl~?ms!bR!f`s#eur7ugmyv{r?xZu^p9PvD-zRAjE zX8SS=yqa5G2iQTsCsA(nXLjy5JNKR)Ja1Z`AN|NKeNQfRVHw#P`Xd8DWUSi|fGG_M zM%0$1seO%UIy4Yx;&^!9Ar~<$a>Wre{YbP_a2aCB2U%; diff --git a/filters/__pycache__/color.cpython-312.pyc b/filters/__pycache__/color.cpython-312.pyc deleted file mode 100644 index 2c28bb76e8ac4b8476056afd91ffb14b64413813..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 522 zcmZ8dF;Bu!5WYtXK@danMvZCpRM-llK~G3}15J-MjnVy?6JRNW=hfzkaL_(SM3VfY29r8hg_1Czl5wp$xJPrj3%K?4v|LS{M9-^Ps|6n0w)NeBW}*&VRq*P#-x&! zJc;%TUX=Eol4V-VOO~of)mn3}Tq~4oyJuPAg^g~jt#R>aX1YVSlseZKSNfOAzID+v zt(MA_s;%`(r@3y+=qcs;L9T158EV)V#oO>5e@*9~)A^Tp;Ylt`un1JpfA|{_1CGE2CW0}r1UeQMw1DI-w>789 zO+(dKlUhC9XtN~dBWOH~G=R5w%GYNlCKCRSe9c^|X|#w#otxS=OQB9|gRdZcON5Yj Qi2cZfY)&GCEcxwz0dpy5ng9R* diff --git a/filters/__pycache__/desenfoque.cpython-312.pyc b/filters/__pycache__/desenfoque.cpython-312.pyc deleted file mode 100644 index feda40dbe34d78022e3b3d9da48fbe5395a1cdaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 721 zcmZ9Kzi-n(6vyxUBX#4HiU8tARGO+Cutcc<(E;&8(54kc1PN4z%2IU7og-7*anDX@ zq9A49e<(wTz*NMNu_GfAOCts%!+^x(Es(8K-{r@`ll-~o)AxPf`JKPb%p?)S!_{Zj zM~u)P@iGZ%oD42OvX3aD*g@NfO7GA%rZP~fV?&9BtWc82YO$#Bi1)!$z&>i?CZ@QN zKv27L4WS0?zQrN*8^!BMr^yDa&0&=#HKosE=a&dcPq1jT!uV-(YmhFU~U!g2uo>kecvKxLqN)g|8>P2Q*tQ2w`+(n0A z1<*ph^jtf0xcgzg^QgOUr<-1F<$vjCe(0I5o_U{Z=R29>#rsFq?|SZ7$qk?cl>{a+ z1&MGQECFEnm`YTp3RP+1K!JRUYCw@sA4oQa;Uo)D(kGm_OdK^9(~ai^W&vCQ7)m6& zwg(`4kz#vx7-_Z#>Kbuk@gKzE;tYvvqGB5G32|}xJS89$(QKK8#nCv5l!Sy|hFx*k z;5wj%erxA@i%W+uj?!Pxb~E|j+|@Sk-2Af8y|DgQl`kgyvYJZtk*X$!riRQ#&*AWf oR{!-o(eA-DR2}w!Ujd!?sNfu+FJp{z9l_K`=&v3 zFP(Ahtg+N@=c(9ZJ!YpFJ&B&&h8h8v_Nz-;^tCfix18y| z=HV9P;hpA9^P(H@J?XHoow0|!Ko^*vsN#>O-95nzjKk@x2@ZL}LnN`YHZ3S&!pZBp zBLxWWEX$TgI3;W42ye^gl13QQA_AytHbVlGb<-uBoiz+8NO(OXLriqZ&|PIY2`IJY)d+RCJ3Bnu2d^wp=;)w-Evz^51dSWuW zW*S*pbrN%yW==dF$;i5yxVvy~B$HJiB~rTK(#S^PCS8Mzgk{Sza!DMjxKT^`b@P`Cs@T9-68+4z)6 zRd!{&A5LkgGYn8bKg9OKA?Z`!?$bSaKlY%Z%X{L}sOm5i0M>AKe^P%rym?-Owx2DxHrx9qDBN3Z43cdD`&;vP~M} zK~BUI96aDj3?6#qPcSht5;&N_1BqUGvaz1H;F~QijTc@rZ+D`&x4Bl<(VmYskG|`mDT+m@D0@6YU~_5%9nZelP!yFGX84%VK)&b;SokDv^3#A7 z)f4!VFSJ3uEsD?y3K=p;OHdbrP)4XLdC1=vCsYRJWaz@r3Mn&_ zG1DGB2MJ^t?kjL5^lL!-K;DzGvNLdPTkbDSyeX_{e^nH3Ur8<}*R-A3#nN3^cbr}g zy$!8J*2l_^cN5o36F(B2M>`MJ$IHfUVx)C%tlYcZKfKkun;0)mfc#X)%JlLy$VWHG zx2}<$_-N_gep^Q=S%XucGKPmht0v54^WvpNohu+CVAOhTJc#=gOgBm%L z*Gx|VmwY`uJgf`>s_=JCd6k;Xq#Uuq8H-FNHr#TU*dT%H`-o^C=XG<}1&+06O)ph% zV+tFdLHl9%EDVRNsg&iBr9qVN>eB=fhqQU z&MD9c;GpS2u>Ue1gva6P<}LFky#ze|N?o3>DhYzHhoXC^^N-vvbk_P5p&tDokRJp* diff --git a/filters/glitch/__pycache__/shift_pro.cpython-312.pyc b/filters/glitch/__pycache__/shift_pro.cpython-312.pyc deleted file mode 100644 index c03ef8aea71deecf779f20ac68d8b2a7e7b0147c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1509 zcmah}&2JM&6rc5edDq_fBOs*Gv_WlYRw5t}stTe=sBNko5|kDYM3$_@v))Wt@2+My zZj9EF4;;t|^+XOi&=alb!Ro1hLZnI+Za73k4@m9Br>0F$a0qYKcH*Rpxe>Dspf&BdUOtmc|^e0yalIoM~aUc&7Mp$ssd~X-#Q#f@G&5KwBlp2C5b-DX& zMde#xAR`iCZQm4lh6u1Hi-X()1MD$+(*L~#F-^6ZZfX(FkjSDkV0(M44kDU*EMei! z1VR+*@&qh4GqHTYl8u!E7Go%XdN1#Q#cC|XnV+O5{0tz?kwg5`aVpLZ`R5Mt*J3d? zhWztG9ZK${_uT=;BXKUyJL2xC3vuBaWXiKlU+^ip>wA&qc6k#i@2@E^$yz3%sW7op zKgZ-Sa7n~cUcmIbHub}@OYYcAqQ2`gxn@(xHj7LqHOsM?5iXEwRIXd@l3l*NUv>7Y zlw~lXb|oUd2d+bkGptai-7Z%aEYGuDra60*?ov4^iz&Qn$kf}N{+`4qRoR%w?^3!; zmE;gOEW+u>|0ge}Di=%FLz{-B`f}+D|69-XEgY7v2DbP4jmertywX?KrzdMZzE-Ld zH{zZSw5+mF>UATa{^_8>vQ`kdjj~;>!lCInd`yBBJ_cx^zfPKu$@io!Z=C$3BYn`C zzTa3i|5K2WdoZ&yvubW+KWu%urJ&LG9=-c}adKs@eYQ2VSsZ(-2C;?Fhw69gs`lu! zcKuoYa%=jJeDP2V#Eldl+*-K>aTk6dPmg`NVO(fUy~q?=Guu!qs&E*H1|)f^*MPud z7jO!TSi&+^)}(~1LpRtyRW!EA8X2+&C+$7MxbFVaUgKbmM4 zf0OfHlgZvvEoe}F7s*&9l^iJdW%?2f=#pz+rf0#AzxMDbz?LWo!gHiQN5y}oBSLZe JlrHqLzXo#1h@=1j diff --git a/filters/glitch/glitch.py b/filters/glitch/glitch.py deleted file mode 100644 index 20c2779..0000000 --- a/filters/glitch/glitch.py +++ /dev/null @@ -1,20 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, glitch_intensity, glitch_frequency): - rows, cols, _ = image.shape - glitch_image = image.copy() - - for i in range(0, rows, glitch_frequency): - shift = np.random.randint(-glitch_intensity, glitch_intensity) - glitch_image[i] = np.roll(image[i], shift, axis=0) - - return glitch_image - -def get_filter_data(): - return { - "parameters": { - "glitch_intensity": {"min": 1, "max": 50, "init": 10, "interval": 1}, - "glitch_frequency": {"min": 1, "max": 50, "init": 5, "interval": 1} - } - } diff --git a/filters/glitch/shift.py b/filters/glitch/shift.py deleted file mode 100644 index b5bebda..0000000 --- a/filters/glitch/shift.py +++ /dev/null @@ -1,22 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, shift_value): - b_channel, g_channel, r_channel = cv2.split(image) - - zeros = np.zeros_like(b_channel) - - b_shifted = np.roll(b_channel, shift_value, axis=1) - g_shifted = np.roll(g_channel, -shift_value, axis=1) - r_shifted = np.roll(r_channel, shift_value // 2, axis=0) - - merged = cv2.merge([b_shifted, g_shifted, r_shifted]) - - return merged - -def get_filter_data(): - return { - "parameters": { - "shift_value": {"min": -50, "max": 50, "init": 5, "interval": 1} - } - } diff --git a/filters/glitch/shift_pro.py b/filters/glitch/shift_pro.py deleted file mode 100644 index 8f83911..0000000 --- a/filters/glitch/shift_pro.py +++ /dev/null @@ -1,32 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, shift_value_b, shift_value_g, shift_value_r, direction_b, direction_g, direction_r): - - b_channel, g_channel, r_channel = cv2.split(image) - - zeros = np.zeros_like(b_channel) - - axis_b = 1 if direction_b == 'horizontal' else 0 - axis_g = 1 if direction_g == 'horizontal' else 0 - axis_r = 1 if direction_r == 'horizontal' else 0 - - b_shifted = np.roll(b_channel, shift_value_b, axis=axis_b) - g_shifted = np.roll(g_channel, shift_value_g, axis=axis_g) - r_shifted = np.roll(r_channel, shift_value_r, axis=axis_r) - - merged = cv2.merge([b_shifted, g_shifted, r_shifted]) - - return merged - -def get_filter_data(): - return { - "parameters": { - "shift_value_b": {"min": -50, "max": 50, "init": 5, "interval": 1}, - "shift_value_g": {"min": -50, "max": 50, "init": -5, "interval": 1}, - "shift_value_r": {"min": -50, "max": 50, "init": 2, "interval": 1}, - "direction_b": {"options": ["horizontal", "vertical"], "init": "horizontal"}, - "direction_g": {"options": ["horizontal", "vertical"], "init": "horizontal"}, - "direction_r": {"options": ["horizontal", "vertical"], "init": "vertical"} - } - } \ No newline at end of file diff --git a/filters/pixel/__pycache__/pixelar.cpython-312.pyc b/filters/pixel/__pycache__/pixelar.cpython-312.pyc deleted file mode 100644 index da43dc37b60ebf38c507e03d00e288b51a7ab55b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1378 zcmah}O>7%Q6rR~XJI@bxBt&hrwW+D{PJ{q|%&=3vZ7qZ@7YyGr8AMMq!FNyp}>E)!-;dM!XJ(!ZvgLw&2 z=>|%KW_S1z+k96${E`xw$;7Q#dP4A=Ha`z=N?O@)5+NoUrpsg}r&&4^OpD1?-7Yyy zUIzaKCfk~MTW3;H*UW^HC4J}7D^g%QgfDW zUcEU{)C@BNY^SR(VOtWj_|(yPGdn*Fv_VX9_Xg-Tk_X7!9~ABP;gk(|GPt)kzD zV(ZHrck6eX@6_+LBAxi;*3i4FA3uM&`%eYMM>cZxTvKgTJL4BOlTXfeKA3Oc{H*=j z=TDbzJzL6kmU2&H#_AkEUm03YZbirUP)MG~Yxwyy16#J zeKgh>s4q0~o#8WU*Z#{rJkmJbe4{bZia$QuI?*0I*BL$!=;Y-eX1`BB@i0(QsiyGgF`m+Cj0#KfXu0@KP&GEBo|VQ4Ph zUeOBQA^SAPo#1Q?7&~%XchmmurHKZOat6HoD{xK%TSdQyhqu*Z&DUF3Ht~;Nbw;o6 zia5@9B&y0T5@p4Um5`a3S(#)qbf;CZ`ETgmwHNJQDPM$>&@#&f{X_dL!13qf#DPIz WjJpcL{lB2WJ!ue+b>Eb*AOCO2I8h7$ diff --git a/filters/pixel/__pycache__/pixelar_pro.cpython-312.pyc b/filters/pixel/__pycache__/pixelar_pro.cpython-312.pyc deleted file mode 100644 index 54825ec7e402062d5c169638a31a94e907ca2671..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2076 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 diff --git a/filters/varios/__pycache__/piramide.cpython-312.pyc b/filters/varios/__pycache__/piramide.cpython-312.pyc deleted file mode 100644 index a90ff77ffa0491c521f2f9515b25005765ef6bb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1207 zcmZ8gy>HV%6u*n@#7fgpQfNzyDkw;hp&$?wAW)h{X`v)lD5$Lr))70Ak>f;XClJY% zjKlyE3k!5$$OtM!2mSyCHm0aTK?g%6Sh7Wwi3Q#{NeMj3?|$$5`Q5vZ!^2?&^kyhi zPy~d&aiLFv9@sku;3d+KhE25IJ@I-#3p_^af+heL>o_j9xqBwxkK&RHA7n*)mod-a z0PoWML9d?9MK0dz={EvyK*LACvLOsai9OeW!xfx;>3(n@?t}e|;y@nW*MH=S19|+J z{Od4ajr?oj(iZ`5a1socv8twWFa#wIeMXUuhK@+#6t_7oK(;@;3 zpER06F1m^0Ql|YIi1PWw1^;OO2HZuyR%SxJcAiDHu2WJnG$!Y3PO@y4DT^kv zOWB+{m(HC}=MrlyQZOtnS+<;VmD+4Lms-iGsnwNKE|XYdqsfKDVn$6YFXytW>dnO) z%S$O1(I|P)kBG}mGD@VVv!JQhbkk;`O0QZ2eo3sdP+#B| za;!(cQu&L@EnBCyQhTV(mmgSWnP|3>t>{+r_H2n5mXf};I9n=fD@wsI9p95sqiib` z!~bplOr_2uq*5{Ks$MAQd52Cw1-?G^3t%4l9zEEKPB){|o7F~PCpzn;J3$mXvKf0m z(->5xBi0qmfB3_4K&6 zusby3E$u;PD9>4+LIC{t!@GwU*xM9Mf_(|T5)Eu%*o{zzg)0PpT<4>pBjDTSFRjfN zs?Fo$A`?o61&lmoqG1^hlMM^JYs7qm=m~J4{ISt-VC+Cqchv4+RgE}=o&+uLz@7o- zptCRx=ITt>jE_*dZP`mf+G m`7+#P)zq)hQxL*iw+{n@DPjBtg}5)h?wyu!H~Al+QWpsT diff --git a/filters/varios/__pycache__/super_queee.cpython-312.pyc b/filters/varios/__pycache__/super_queee.cpython-312.pyc deleted file mode 100644 index c662f6f7608fd69a13a3394ba9c893b62c0b748d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1469 zcmZ`(O>7%Q6rT0|*fDkp`Ev^*G@>O~C>5;)O=yHvwP_MsJK)rj$;`dz`A9cxtkO4EJFSwTxs7rive2_9I?(2%CKAqO!YaQS|)Jd zB@EXaDobLEa_xJc_bm#JH<1Z>)FVZ)=)23WH?)?dmTc{wlShhD5oJ2Op+{8VC~#H0 z4OJcDA?8O{(5 znFuF+FW0Q#w$K|oaxeF7c@hjAC703niV}pwJO~t}ggmKb}woy^bDf5`V zFstW@OcYhi+q7ptmAqdbZHE{or>U}z7qD*AFpD8rmFp>_EG7olX-8{xEjbI6RZwqu zhS)f7)kk(4eCx5MJsUs&UMgv0Vy6}sQ>7VSQ%*08m1Ibw^fW%du}ytaYws zH_y6*V~@JWOR;|?)ZX#Mwa>48nOr^n&Cu6FkHW`G7oSF6ejFL7LftA@e4{dX z!#%5P_so{!PrG_ohL?xGy1s#b%HGRv_1`nL@UPhiSvN8Dz;NG5Zg*WRUEY;Y=!h#s z_Ml`m$DnEKOxDi-+d$wC!l^!$R|PsutNildMy%yz+T4+E6YAA#t z++XRt{I_3-1b2BUB<&(elxhl5k+6cPXykLyOzhWE+u@qVfkK`IMxa~rI)0zL1%B+1 b*{t?mp5wSDDEI_*{v(9BBYS;*uAcpW`R`x| diff --git a/filters/varios/dilatar.py b/filters/varios/dilatar.py deleted file mode 100644 index c8936b2..0000000 --- a/filters/varios/dilatar.py +++ /dev/null @@ -1,17 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, kernel_size): - - gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - kernel = np.ones((kernel_size, kernel_size), np.uint8) - dilated = cv2.dilate(gray, kernel, iterations=1) - - return cv2.cvtColor(dilated, cv2.COLOR_GRAY2BGR) - -def get_filter_data(): - return { - "parameters": { - "kernel_size": {"min": 2, "max": 50, "init": 5, "interval": 1}, - } - } \ No newline at end of file diff --git a/filters/varios/dilatar_pro.py b/filters/varios/dilatar_pro.py deleted file mode 100644 index d3a19c0..0000000 --- a/filters/varios/dilatar_pro.py +++ /dev/null @@ -1,19 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, kernel_size, anchor, iterations, border_value): - - gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - kernel = np.ones((kernel_size, kernel_size), np.uint8) - - return cv2.dilate(gray, kernel, anchor=(-1, -1), iterations=iterations, borderType=cv2.BORDER_CONSTANT, borderValue=border_value) - -def get_filter_data(): - return { - "parameters": { - "kernel_size": {"min": 2, "max": 50, "init": 5, "interval": 1}, - "anchor": {"min": -1, "max": -1, "init": -1, "interval": 1}, - "iterations": {"min": 1, "max": 10, "init": 1, "interval": 1}, - "border_value": {"min": 0, "max": 255, "init": 0, "interval": 1}, - } - } \ No newline at end of file diff --git a/filters/varios/erode.py b/filters/varios/erode.py deleted file mode 100644 index 06f4824..0000000 --- a/filters/varios/erode.py +++ /dev/null @@ -1,19 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, kernel_size, anchor, iterations, border_type, border_value): - - gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - kernel = np.ones((kernel_size, kernel_size), np.uint8) - - return cv2.erode(gray, kernel, anchor=(-1, -1), iterations=iterations, borderType=border_type, borderValue=border_value) -def get_filter_data(): - return { - "parameters": { - "kernel_size": {"min": 2, "max": 50, "init": 5, "interval": 1}, - "anchor": {"min": -1, "max": -1, "init": -1, "interval": 1}, - "iterations": {"min": 1, "max": 10, "init": 1, "interval": 1}, - "border_type": {"min": 0, "max": 4, "init": 0, "interval": 1}, - "border_value": {"min": 0, "max": 255, "init": 0, "interval": 1}, - } - } \ No newline at end of file diff --git a/filters/varios/super_queee.py b/filters/varios/super_queee.py deleted file mode 100644 index 0022565..0000000 --- a/filters/varios/super_queee.py +++ /dev/null @@ -1,22 +0,0 @@ -import cv2 -import numpy as np - -def apply_effect(image, blur_radius, morphology_iterations, pyramid_levels): - blurred_image = cv2.boxFilter(image, -1, (blur_radius, blur_radius)) - - kernel = np.ones((5,5),np.uint8) - morphology_image = cv2.morphologyEx(blurred_image, cv2.MORPH_GRADIENT, kernel, iterations=morphology_iterations) - - pyr_down_image = cv2.pyrDown(morphology_image, dstsize=(image.shape[1]//2, image.shape[0]//2)) - pyr_up_image = cv2.pyrUp(pyr_down_image, dstsize=(image.shape[1], image.shape[0])) - - return pyr_up_image - -def get_filter_data(): - return { - "parameters": { - "blur_radius": {"min": 1, "max": 10, "init": 5, "interval": 1}, - "morphology_iterations": {"min": 1, "max": 5, "init": 2, "interval": 1}, - "pyramid_levels": {"min": 1, "max": 5, "init": 3, "interval": 1} - } - } \ No newline at end of file diff --git a/filters_config_generator.py b/filters_config_generator.py deleted file mode 100644 index 33e085b..0000000 --- a/filters_config_generator.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import importlib.util -import inspect - -def load_module(filepath): - spec = importlib.util.spec_from_file_location("module", filepath) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - -def get_filter_info(filepath): - module = load_module(filepath) - filter_data_func = getattr(module, "get_filter_data", None) - if filter_data_func and inspect.isfunction(filter_data_func): - return filter_data_func() - else: - return None - - -def generate_py_code(directory): - py_code = "from filters import *\n\nFILTERS = {}\n\n" - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith(".py") and file != "__init__.py": - filepath = os.path.join(root, file) - folder_name = os.path.basename(os.path.dirname(filepath)) - module_name = os.path.splitext(os.path.basename(filepath))[0] - py_code += f"from filters.{folder_name} import {module_name}\n" - - py_code += "\nFILTERS = {\n" - - for root, dirs, files in os.walk(directory): - for dir_name in dirs: - if dir_name != "__pycache__": - py_code += f' "{dir_name}": {{\n' - for file in os.listdir(os.path.join(directory, dir_name)): - if file.endswith(".py") and file != "__init__.py": - module_name = os.path.splitext(file)[0] - py_code += f' "{module_name}": {{\n' - py_code += f' "apply": {module_name}.apply_effect,\n' - filter_info = get_filter_info(os.path.join(directory, dir_name, file)) - if filter_info: - py_code += ' "parameters": {\n' - for param, value in filter_info["parameters"].items(): - py_code += f' "{param}": {value},\n' - py_code += ' }\n' - py_code += ' },\n' - py_code += ' },\n' - - py_code += "}\n" - - return py_code - -directory_path = "filters" -generated_py_code = generate_py_code(directory_path) - -with open("effects_settings.py", "w") as py_file: - py_file.write(generated_py_code) - -print("Archivo 'effects_settings.py' creado con éxito.") diff --git a/main.py b/main.py index 1d52a7c..7416e03 100644 --- a/main.py +++ b/main.py @@ -1,193 +1,277 @@ +""" +MODT - Photo Editor Application + +A modular photo editor with filter effects that can be configured via JSON. +""" + import sys +import json import cv2 from PyQt5.QtWidgets import ( - QApplication, QComboBox, QMainWindow, QSizePolicy, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog, QSlider, QMessageBox + QApplication, QComboBox, QMainWindow, QSizePolicy, QLabel, QPushButton, + QVBoxLayout, QHBoxLayout, QWidget, QFileDialog, QSlider, QMessageBox, + QTabWidget, QLineEdit, QFormLayout, QScrollArea, QGroupBox, QSpinBox, + QDoubleSpinBox, QTextEdit, QFrame ) from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap, QImage, QIcon -from effects_settings import FILTERS - -class PhotoEditorApp(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("MODT - Photo Editor") - self.setGeometry(100, 100, 600, 400) - self.setWindowIcon(QIcon("assets/calavera.gif")) +from filter_manager import get_filter_manager, FilterManager + + +class Styles: + """Centralized styles for the application.""" + + MAIN_WINDOW = """ + QMainWindow { + background-color: #000; + } + QWidget { + background-color: #222; + color: #fff; + } + QLabel { + color: #fff; + } + QComboBox, QSlider, QPushButton { + background-color: #333; + color: #fff; + border: 1px solid #666; + border-radius: 5px; + } + QPushButton:hover { + background-color: #555; + } + """ + + SLIDER = """ + QSlider::groove:horizontal { + height: 6px; + background: #444; + border: 1px solid #666; + border-radius: 3px; + } + QSlider::handle:horizontal { + background: #4CAF50; + border: 1px solid #4CAF50; + width: 12px; + margin: -6px 0; + border-radius: 6px; + } + """ + + COMBOBOX = """ + QComboBox { + max-width: 200px; + padding: 3px; + border: 1px solid #666; + border-radius: 2px; + } + QComboBox::drop-down { + width: 20px; + } + QComboBox::down-arrow { + width: 10px; + height: 10px; + } + """ + + FILTER_BUTTON = """ + QPushButton { + padding: 8px 16px; + border: 2px solid #4CAF50; + border-radius: 25px; + background-color: #4CAF50; + color: #fff; + font-weight: bold; + } + QPushButton:hover { + background-color: #45a049; + } + """ + + FILE_BUTTON = """ + QPushButton { + padding: 8px 16px; + border: 2px solid #008CBA; + border-radius: 25px; + background-color: #008CBA; + color: #fff; + font-weight: bold; + } + QPushButton:hover { + background-color: #005f78; + } + """ + + SLIDER_LABEL = """ + QLabel { + color: #fff; + font-weight: bold; + } + """ + + TAB_WIDGET = """ + QTabWidget::pane { + border: 1px solid #444; + background-color: #222; + } + QTabBar::tab { + background-color: #333; + color: #fff; + padding: 10px 20px; + border: 1px solid #444; + border-bottom: none; + } + QTabBar::tab:selected { + background-color: #4CAF50; + } + QTabBar::tab:hover { + background-color: #555; + } + """ + + INPUT_FIELD = """ + QLineEdit, QTextEdit, QSpinBox, QDoubleSpinBox { + background-color: #333; + color: #fff; + border: 1px solid #666; + border-radius: 3px; + padding: 5px; + } + QLineEdit:focus, QTextEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus { + border: 1px solid #4CAF50; + } + """ + + GROUP_BOX = """ + QGroupBox { + border: 1px solid #666; + border-radius: 5px; + margin-top: 10px; + padding-top: 10px; + color: #fff; + font-weight: bold; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + } + """ + + @classmethod + def get_combined_style(cls): + """Get all styles combined.""" + return (cls.COMBOBOX + cls.SLIDER + cls.SLIDER_LABEL + + cls.TAB_WIDGET + cls.INPUT_FIELD + cls.GROUP_BOX) + + +class PhotoEditorTab(QWidget): + """Tab for photo editing with filters.""" + + def __init__(self, filter_manager: FilterManager, parent=None): + super().__init__(parent) + self.filter_manager = filter_manager self.image = None self.original_image = None self.active_filter = None self.active_category = None self.sliders = [] self.comboboxes = [] - self.initUI() - - def initUI(self): - self.setStyleSheet(""" - QMainWindow { - background-color: #000; - } - QWidget { - background-color: #222; - color: #fff; - } - QLabel { - color: #fff; - } - QComboBox, QSlider, QPushButton { - background-color: #333; - color: #fff; - border: 1px solid #666; - border-radius: 5px; - } - QPushButton:hover { - background-color: #555; - } - """) - - slider_style = """ - QSlider::groove:horizontal { - height: 6px; - background: #444; - border: 1px solid #666; - border-radius: 3px; - } - QSlider::handle:horizontal { - background: #4CAF50; - border: 1px solid #4CAF50; - width: 12px; - margin: -6px 0; - border-radius: 6px; - } - """ - - combobox_style = """ - QComboBox { - max-width: 200px; - padding: 3px; - border: 1px solid #666; - border-radius: 2px; - } - QComboBox::drop-down { - width: 20px; - } - QComboBox::down-arrow { - width: 10px; - height: 10px; - } - """ - - button_style = """ - QPushButton { - padding: 8px 16px; - border: 2px solid #4CAF50; - border-radius: 25px; - background-color: #4CAF50; - color: #fff; - font-weight: bold; - } - QPushButton:hover { - background-color: #45a049; - } - """ - - self.filter_button_style = button_style - - file_button_style = """ - QPushButton { - padding: 8px 16px; - border: 2px solid #008CBA; - border-radius: 25px; - background-color: #008CBA; - color: #fff; - font-weight: bold; - } - QPushButton:hover { - background-color: #005f78; - } - """ - - slider_label_style = """ - QLabel { - color: #fff; - font-weight: bold; - } - """ - - central_widget = QWidget(self) - self.setCentralWidget(central_widget) - main_layout = QHBoxLayout(central_widget) - + self.init_ui() + + def init_ui(self): + """Initialize the photo editor UI.""" + main_layout = QHBoxLayout(self) + + # Left panel - filter selection self.filter_buttons_layout = QVBoxLayout() main_layout.addLayout(self.filter_buttons_layout) - + self.category_combo = QComboBox(self) - self.category_combo.addItems(FILTERS.keys()) + self.category_combo.addItems(self.filter_manager.get_categories()) self.category_combo.currentTextChanged.connect(self.update_filter_buttons) self.filter_buttons_layout.addWidget(self.category_combo) - + self.filter_buttons_row_layout = QVBoxLayout() self.filter_buttons_layout.addLayout(self.filter_buttons_row_layout) - + + # Right panel - image display and controls right_layout = QVBoxLayout() right_layout.setSizeConstraint(QVBoxLayout.SetMinimumSize) + self.image_label = QLabel(self) self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) right_layout.addWidget(self.image_label) - + load_btn = QPushButton("Cargar Imagen", self) load_btn.clicked.connect(self.load_image) - load_btn.setStyleSheet(file_button_style) + load_btn.setStyleSheet(Styles.FILE_BUTTON) right_layout.addWidget(load_btn) - + self.slider_layout = QVBoxLayout() self.slider_layout.setSizeConstraint(QVBoxLayout.SetMinimumSize) right_layout.addLayout(self.slider_layout) - + save_btn = QPushButton("Guardar Imagen", self) save_btn.clicked.connect(self.save_image) - save_btn.setStyleSheet(file_button_style) + save_btn.setStyleSheet(Styles.FILE_BUTTON) right_layout.addWidget(save_btn) - - combined_style = combobox_style + slider_style + slider_label_style - app.setStyleSheet(combined_style) - + main_layout.addLayout(right_layout) - central_widget.setLayout(main_layout) - self.update_filter_buttons() - + + def refresh_filters(self): + """Refresh the filter list from the filter manager.""" + current_category = self.category_combo.currentText() + self.category_combo.clear() + self.category_combo.addItems(self.filter_manager.get_categories()) + + # Try to restore the previous category + index = self.category_combo.findText(current_category) + if index >= 0: + self.category_combo.setCurrentIndex(index) + + self.update_filter_buttons() + def update_filter_buttons(self): + """Update the filter buttons based on selected category.""" for i in reversed(range(self.filter_buttons_row_layout.count())): widget = self.filter_buttons_row_layout.itemAt(i).widget() - widget.deleteLater() - + if widget: + widget.deleteLater() + category = self.category_combo.currentText() - if category in FILTERS: - for filter_name in FILTERS[category].keys(): - btn = QPushButton(filter_name, self) - btn.clicked.connect(lambda _, c=category, f=filter_name: self.apply_filter(c, f)) - btn.setStyleSheet(self.filter_button_style) - self.filter_buttons_row_layout.addWidget(btn) - + filters_in_category = self.filter_manager.get_filters_in_category(category) + + for filter_name in filters_in_category.keys(): + btn = QPushButton(filter_name, self) + btn.clicked.connect(lambda _, c=category, f=filter_name: self.apply_filter(c, f)) + btn.setStyleSheet(Styles.FILTER_BUTTON) + self.filter_buttons_row_layout.addWidget(btn) + def load_image(self): - file_path, _ = QFileDialog.getOpenFileName(self, "Cargar Imagen", "", "Images (*.png *.jpg *.jpeg *.bmp)") + """Load an image from file.""" + file_path, _ = QFileDialog.getOpenFileName( + self, "Cargar Imagen", "", "Images (*.png *.jpg *.jpeg *.bmp)" + ) if file_path: self.original_image = cv2.imread(file_path) self.image = self.original_image.copy() self.display_image(self.image) - + def display_image(self, img): + """Display an image in the image label.""" widget_width = self.width() widget_height = self.height() - + max_width = int(widget_width * 0.7) max_height = int(widget_height * 0.7) - + img_height, img_width = img.shape[:2] - aspect_ratio = img_width / img_height - + if img_width > max_width or img_height > max_height: if img_width / max_width > img_height / max_height: new_width = max_width @@ -198,60 +282,75 @@ def display_image(self, img): else: new_width = img_width new_height = img_height - + resized_img = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_AREA) - + if len(resized_img.shape) == 2: - q_img = QImage(resized_img.data, resized_img.shape[1], resized_img.shape[0], resized_img.strides[0], QImage.Format_Grayscale8) + q_img = QImage( + resized_img.data, resized_img.shape[1], resized_img.shape[0], + resized_img.strides[0], QImage.Format_Grayscale8 + ) else: - q_img = QImage(resized_img.data, resized_img.shape[1], resized_img.shape[0], resized_img.strides[0], QImage.Format_RGB888).rgbSwapped() - + q_img = QImage( + resized_img.data, resized_img.shape[1], resized_img.shape[0], + resized_img.strides[0], QImage.Format_RGB888 + ).rgbSwapped() + pixmap = QPixmap.fromImage(q_img) self.image_label.setPixmap(pixmap) - - def resizeEvent(self, event): - if self.image is not None: - self.display_image(self.image) - super().resizeEvent(event) - + def apply_filter(self, category, filter_name): + """Apply a filter to the image.""" if self.original_image is None: self.show_message("Advertencia", "Primero carga una imagen.") return self.active_category = category self.active_filter = filter_name self.update_effect() - + def update_effect(self): + """Update the effect on the image.""" if self.original_image is None: self.show_message("Advertencia", "Primero carga una imagen.") return + if self.active_filter is not None: - filter_info = FILTERS[self.active_category][self.active_filter] - if "parameters" in filter_info and filter_info["parameters"]: - self.update_sliders() - else: - self.image = filter_info["apply"](self.original_image) - self.display_image(self.image) - + filter_info = self.filter_manager.get_filter_info( + self.active_category, self.active_filter + ) + if filter_info: + if "parameters" in filter_info and filter_info["parameters"]: + self.update_sliders() + else: + self.image = filter_info["apply"](self.original_image) + self.display_image(self.image) + def save_image(self): + """Save the current image to file.""" if self.image is not None: - file_path, _ = QFileDialog.getSaveFileName(self, "Guardar Imagen", "", "Images (*.png *.jpg *.jpeg *.bmp)") + file_path, _ = QFileDialog.getSaveFileName( + self, "Guardar Imagen", "", "Images (*.png *.jpg *.jpeg *.bmp)" + ) if file_path: cv2.imwrite(file_path, self.image) QMessageBox.information(self, "Éxito", "La imagen se ha guardado correctamente.") else: QMessageBox.warning(self, "Advertencia", "No hay ninguna imagen para guardar.") - + def update_sliders(self): + """Update the parameter sliders for the current filter.""" if self.original_image is None: self.show_message("Advertencia", "Primero carga una imagen.") return - + self.clear_sliders() - - filter_info = FILTERS[self.active_category][self.active_filter] - + + filter_info = self.filter_manager.get_filter_info( + self.active_category, self.active_filter + ) + if not filter_info: + return + for param_name, param_info in filter_info["parameters"].items(): if "options" in param_info: combobox = QComboBox(self) @@ -264,9 +363,13 @@ def update_sliders(self): self.slider_layout.addWidget(QLabel(param_name)) self.slider_layout.addWidget(combobox) else: - slider = QSlider(Qt.Horizontal, minimum=int(param_info["min"]), maximum=int(param_info["max"]), value=int(param_info["init"]), singleStep - -=int(param_info["interval"])) + slider = QSlider( + Qt.Horizontal, + minimum=int(param_info["min"]), + maximum=int(param_info["max"]), + value=int(param_info["init"]), + singleStep=int(param_info["interval"]) + ) slider.valueChanged.connect(self.update_slider_value) slider.setFixedHeight(20) slider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -276,21 +379,28 @@ def update_sliders(self): self.image = filter_info["apply"](self.original_image, **self.get_slider_values()) self.display_image(self.image) - + def show_message(self, title, message): + """Show a warning message dialog.""" QMessageBox.warning(self, title, message) - + def get_slider_values(self): + """Get the current values from all sliders and comboboxes.""" values = {param_name: slider.value() for param_name, slider in self.sliders} values.update({param_name: combobox.currentText() for param_name, combobox in self.comboboxes}) return values - + def update_slider_value(self): - filter_info = FILTERS[self.active_category][self.active_filter] - self.image = filter_info["apply"](self.original_image, **self.get_slider_values()) - self.display_image(self.image) - + """Update the image when a slider value changes.""" + filter_info = self.filter_manager.get_filter_info( + self.active_category, self.active_filter + ) + if filter_info: + self.image = filter_info["apply"](self.original_image, **self.get_slider_values()) + self.display_image(self.image) + def clear_sliders(self): + """Clear all parameter sliders and comboboxes.""" for param_name, slider in self.sliders: slider.deleteLater() for param_name, combobox in self.comboboxes: @@ -303,6 +413,386 @@ def clear_sliders(self): self.sliders = [] self.comboboxes = [] + +class ParameterWidget(QWidget): + """Widget for configuring a single filter parameter.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.init_ui() + + def init_ui(self): + """Initialize the parameter widget UI.""" + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + # Parameter name + self.name_edit = QLineEdit() + self.name_edit.setPlaceholderText("Nombre del parámetro") + layout.addWidget(self.name_edit) + + # Parameter type + self.type_combo = QComboBox() + self.type_combo.addItems(["slider", "options"]) + self.type_combo.currentTextChanged.connect(self.on_type_changed) + layout.addWidget(self.type_combo) + + # Slider configuration + self.slider_widget = QWidget() + slider_layout = QHBoxLayout(self.slider_widget) + slider_layout.setContentsMargins(0, 0, 0, 0) + + self.min_spin = QDoubleSpinBox() + self.min_spin.setRange(-10000, 10000) + self.min_spin.setPrefix("Min: ") + slider_layout.addWidget(self.min_spin) + + self.max_spin = QDoubleSpinBox() + self.max_spin.setRange(-10000, 10000) + self.max_spin.setValue(100) + self.max_spin.setPrefix("Max: ") + slider_layout.addWidget(self.max_spin) + + self.init_spin = QDoubleSpinBox() + self.init_spin.setRange(-10000, 10000) + self.init_spin.setValue(50) + self.init_spin.setPrefix("Init: ") + slider_layout.addWidget(self.init_spin) + + self.interval_spin = QDoubleSpinBox() + self.interval_spin.setRange(0.001, 1000) + self.interval_spin.setValue(1) + self.interval_spin.setPrefix("Intervalo: ") + slider_layout.addWidget(self.interval_spin) + + layout.addWidget(self.slider_widget) + + # Options configuration + self.options_widget = QWidget() + options_layout = QHBoxLayout(self.options_widget) + options_layout.setContentsMargins(0, 0, 0, 0) + + self.options_edit = QLineEdit() + self.options_edit.setPlaceholderText("Opciones (separadas por coma)") + options_layout.addWidget(self.options_edit) + + self.init_option_edit = QLineEdit() + self.init_option_edit.setPlaceholderText("Valor inicial") + options_layout.addWidget(self.init_option_edit) + + layout.addWidget(self.options_widget) + self.options_widget.hide() + + # Remove button + self.remove_btn = QPushButton("X") + self.remove_btn.setFixedWidth(30) + self.remove_btn.setStyleSheet("background-color: #f44336; color: white;") + layout.addWidget(self.remove_btn) + + def on_type_changed(self, param_type): + """Handle parameter type change.""" + if param_type == "slider": + self.slider_widget.show() + self.options_widget.hide() + else: + self.slider_widget.hide() + self.options_widget.show() + + def get_config(self): + """Get the parameter configuration.""" + name = self.name_edit.text().strip() + if not name: + return None, None + + if self.type_combo.currentText() == "slider": + config = { + "min": self.min_spin.value(), + "max": self.max_spin.value(), + "init": self.init_spin.value(), + "interval": self.interval_spin.value() + } + else: + options_text = self.options_edit.text().strip() + options = [opt.strip() for opt in options_text.split(",") if opt.strip()] + config = { + "options": options, + "init": self.init_option_edit.text().strip() + } + + return name, config + + +class FilterEditorTab(QWidget): + """Tab for creating and editing filters.""" + + def __init__(self, filter_manager: FilterManager, on_filter_added=None, parent=None): + super().__init__(parent) + self.filter_manager = filter_manager + self.on_filter_added = on_filter_added + self.parameter_widgets = [] + self.init_ui() + + def init_ui(self): + """Initialize the filter editor UI.""" + main_layout = QVBoxLayout(self) + + # Scroll area for the form + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll_content = QWidget() + form_layout = QVBoxLayout(scroll_content) + + # Basic info group + basic_group = QGroupBox("Información básica del filtro") + basic_layout = QFormLayout() + + self.filter_name_edit = QLineEdit() + self.filter_name_edit.setPlaceholderText("Nombre del filtro") + basic_layout.addRow("Nombre:", self.filter_name_edit) + + self.category_combo = QComboBox() + self.category_combo.setEditable(True) + self.category_combo.addItems(self.filter_manager.get_categories()) + basic_layout.addRow("Categoría:", self.category_combo) + + self.operation_combo = QComboBox() + self.operation_combo.addItems(self.filter_manager.get_available_operations()) + basic_layout.addRow("Operación:", self.operation_combo) + + self.description_edit = QLineEdit() + self.description_edit.setPlaceholderText("Descripción del filtro") + basic_layout.addRow("Descripción:", self.description_edit) + + basic_group.setLayout(basic_layout) + form_layout.addWidget(basic_group) + + # Parameters group + params_group = QGroupBox("Parámetros del filtro") + self.params_layout = QVBoxLayout() + + add_param_btn = QPushButton("+ Añadir parámetro") + add_param_btn.clicked.connect(self.add_parameter) + add_param_btn.setStyleSheet(Styles.FILTER_BUTTON) + self.params_layout.addWidget(add_param_btn) + + self.params_container = QVBoxLayout() + self.params_layout.addLayout(self.params_container) + + params_group.setLayout(self.params_layout) + form_layout.addWidget(params_group) + + # JSON preview group + preview_group = QGroupBox("Vista previa JSON") + preview_layout = QVBoxLayout() + + self.json_preview = QTextEdit() + self.json_preview.setReadOnly(True) + self.json_preview.setMaximumHeight(150) + preview_layout.addWidget(self.json_preview) + + preview_btn = QPushButton("Actualizar vista previa") + preview_btn.clicked.connect(self.update_preview) + preview_layout.addWidget(preview_btn) + + preview_group.setLayout(preview_layout) + form_layout.addWidget(preview_group) + + # Spacer + form_layout.addStretch() + + scroll.setWidget(scroll_content) + main_layout.addWidget(scroll) + + # Action buttons + buttons_layout = QHBoxLayout() + + save_btn = QPushButton("Guardar filtro") + save_btn.clicked.connect(self.save_filter) + save_btn.setStyleSheet(Styles.FILTER_BUTTON) + buttons_layout.addWidget(save_btn) + + clear_btn = QPushButton("Limpiar formulario") + clear_btn.clicked.connect(self.clear_form) + clear_btn.setStyleSheet(Styles.FILE_BUTTON) + buttons_layout.addWidget(clear_btn) + + main_layout.addLayout(buttons_layout) + + def add_parameter(self): + """Add a new parameter widget.""" + param_widget = ParameterWidget() + param_widget.remove_btn.clicked.connect(lambda: self.remove_parameter(param_widget)) + self.parameter_widgets.append(param_widget) + self.params_container.addWidget(param_widget) + + def remove_parameter(self, widget): + """Remove a parameter widget.""" + if widget in self.parameter_widgets: + self.parameter_widgets.remove(widget) + widget.deleteLater() + + def get_parameters_config(self): + """Get the configuration for all parameters.""" + parameters = {} + for widget in self.parameter_widgets: + name, config = widget.get_config() + if name and config: + parameters[name] = config + return parameters + + def update_preview(self): + """Update the JSON preview.""" + filter_config = { + "operation": self.operation_combo.currentText(), + "description": self.description_edit.text().strip(), + "parameters": self.get_parameters_config() + } + + preview_text = json.dumps(filter_config, indent=4, ensure_ascii=False) + self.json_preview.setPlainText(preview_text) + + def validate_form(self): + """Validate the form data.""" + filter_name = self.filter_name_edit.text().strip() + if not filter_name: + return False, "El nombre del filtro es obligatorio." + + category = self.category_combo.currentText().strip() + if not category: + return False, "La categoría es obligatoria." + + operation = self.operation_combo.currentText().strip() + if not operation: + return False, "La operación es obligatoria." + + parameters = self.get_parameters_config() + is_valid, error_msg = self.filter_manager.validate_parameters(parameters) + if not is_valid: + return False, error_msg + + return True, "" + + def save_filter(self): + """Save the filter to the JSON file.""" + is_valid, error_msg = self.validate_form() + if not is_valid: + QMessageBox.warning(self, "Error de validación", error_msg) + return + + filter_name = self.filter_name_edit.text().strip() + category = self.category_combo.currentText().strip() + operation = self.operation_combo.currentText() + description = self.description_edit.text().strip() + parameters = self.get_parameters_config() + + # Check if filter already exists + existing = self.filter_manager.get_filter(category, filter_name) + if existing: + reply = QMessageBox.question( + self, "Filtro existente", + f"El filtro '{filter_name}' ya existe en la categoría '{category}'. ¿Desea sobrescribirlo?", + QMessageBox.Yes | QMessageBox.No + ) + if reply != QMessageBox.Yes: + return + + try: + self.filter_manager.add_filter( + category=category, + filter_name=filter_name, + operation=operation, + description=description, + parameters=parameters + ) + + QMessageBox.information( + self, "Éxito", + f"El filtro '{filter_name}' se ha guardado correctamente en la categoría '{category}'." + ) + + # Refresh category list + current_categories = [self.category_combo.itemText(i) for i in range(self.category_combo.count())] + if category not in current_categories: + self.category_combo.addItem(category) + + # Notify parent to refresh filters + if self.on_filter_added: + self.on_filter_added() + + self.clear_form() + + except Exception as e: + QMessageBox.critical(self, "Error", f"Error al guardar el filtro: {str(e)}") + + def clear_form(self): + """Clear all form fields.""" + self.filter_name_edit.clear() + self.description_edit.clear() + self.operation_combo.setCurrentIndex(0) + self.json_preview.clear() + + # Remove all parameter widgets + for widget in self.parameter_widgets[:]: + self.remove_parameter(widget) + + +class PhotoEditorApp(QMainWindow): + """Main application window.""" + + def __init__(self): + super().__init__() + self.setWindowTitle("MODT - Photo Editor") + self.setGeometry(100, 100, 900, 600) + self.setWindowIcon(QIcon("assets/calavera.gif")) + + # Initialize filter manager + self.filter_manager = get_filter_manager() + + self.init_ui() + + def init_ui(self): + """Initialize the main UI.""" + self.setStyleSheet(Styles.MAIN_WINDOW) + + # Create central widget with tabs + central_widget = QWidget(self) + self.setCentralWidget(central_widget) + main_layout = QVBoxLayout(central_widget) + + # Create tab widget + self.tab_widget = QTabWidget() + self.tab_widget.setStyleSheet(Styles.TAB_WIDGET) + + # Photo Editor tab + self.photo_editor_tab = PhotoEditorTab(self.filter_manager) + self.tab_widget.addTab(self.photo_editor_tab, "Editor de Fotos") + + # Filter Editor tab + self.filter_editor_tab = FilterEditorTab( + self.filter_manager, + on_filter_added=self.on_filter_added + ) + self.tab_widget.addTab(self.filter_editor_tab, "Editor de Filtros") + + main_layout.addWidget(self.tab_widget) + + # Apply combined styles + QApplication.instance().setStyleSheet(Styles.get_combined_style()) + + def on_filter_added(self): + """Callback when a new filter is added.""" + # Reload filters from JSON + self.filter_manager.load_filters() + # Refresh the photo editor tab + self.photo_editor_tab.refresh_filters() + + def resizeEvent(self, event): + """Handle window resize.""" + if self.photo_editor_tab.image is not None: + self.photo_editor_tab.display_image(self.photo_editor_tab.image) + super().resizeEvent(event) + + if __name__ == '__main__': app = QApplication(sys.argv) editor_app = PhotoEditorApp() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..858469fa8090b5402a1cce8f42ee0e7e3db97324 GIT binary patch literal 142 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 SKKS*6-#GNpAvl}p^!-005<33? literal 0 HcmV?d00001 diff --git a/tests/test_filter_manager.py b/tests/test_filter_manager.py new file mode 100644 index 0000000..9448b04 --- /dev/null +++ b/tests/test_filter_manager.py @@ -0,0 +1,328 @@ +""" +Tests for the FilterManager module. +""" + +import json +import os +import tempfile +import unittest + +import sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +class TestFilterManager(unittest.TestCase): + """Test cases for FilterManager class.""" + + def setUp(self): + """Set up test fixtures.""" + # Import here to avoid circular imports + from filter_manager import FilterManager + + self.test_filters = { + "TestCategory": { + "test_filter": { + "operation": "gaussian_blur", + "description": "Test gaussian blur filter", + "parameters": { + "kernel_size": {"min": 1, "max": 31, "init": 15, "interval": 2}, + "sigma": {"min": 1, "max": 10, "init": 5, "interval": 1} + } + }, + "test_filter_options": { + "operation": "saturate_color", + "description": "Test color saturation filter", + "parameters": { + "color": {"options": ["red", "green", "blue"], "init": "red"} + } + } + }, + "AnotherCategory": { + "another_filter": { + "operation": "grayscale", + "description": "Test grayscale filter", + "parameters": {} + } + } + } + + # Create a temporary JSON file + self.temp_file = tempfile.NamedTemporaryFile( + mode='w', suffix='.json', delete=False + ) + json.dump(self.test_filters, self.temp_file) + self.temp_file.close() + + self.manager = FilterManager(self.temp_file.name) + + def tearDown(self): + """Clean up test fixtures.""" + os.unlink(self.temp_file.name) + + def test_load_filters(self): + """Test loading filters from JSON file.""" + filters = self.manager.load_filters() + self.assertEqual(len(filters), 2) + self.assertIn("TestCategory", filters) + self.assertIn("AnotherCategory", filters) + + def test_get_categories(self): + """Test getting all categories.""" + categories = self.manager.get_categories() + self.assertEqual(len(categories), 2) + self.assertIn("TestCategory", categories) + self.assertIn("AnotherCategory", categories) + + def test_get_filters_in_category(self): + """Test getting filters in a specific category.""" + filters = self.manager.get_filters_in_category("TestCategory") + self.assertEqual(len(filters), 2) + self.assertIn("test_filter", filters) + self.assertIn("test_filter_options", filters) + + def test_get_filters_in_nonexistent_category(self): + """Test getting filters from non-existent category.""" + filters = self.manager.get_filters_in_category("NonExistent") + self.assertEqual(filters, {}) + + def test_get_filter(self): + """Test getting a specific filter.""" + filter_config = self.manager.get_filter("TestCategory", "test_filter") + self.assertIsNotNone(filter_config) + self.assertEqual(filter_config["operation"], "gaussian_blur") + self.assertEqual(filter_config["description"], "Test gaussian blur filter") + + def test_get_nonexistent_filter(self): + """Test getting a non-existent filter.""" + filter_config = self.manager.get_filter("TestCategory", "nonexistent") + self.assertIsNone(filter_config) + + def test_add_filter(self): + """Test adding a new filter.""" + result = self.manager.add_filter( + category="NewCategory", + filter_name="new_filter", + operation="pixelate", + description="A new pixelation filter", + parameters={"block_size": {"min": 2, "max": 50, "init": 5, "interval": 1}} + ) + + self.assertTrue(result) + self.assertIn("NewCategory", self.manager.get_categories()) + + new_filter = self.manager.get_filter("NewCategory", "new_filter") + self.assertIsNotNone(new_filter) + self.assertEqual(new_filter["operation"], "pixelate") + + def test_add_filter_to_existing_category(self): + """Test adding a filter to an existing category.""" + result = self.manager.add_filter( + category="TestCategory", + filter_name="new_test_filter", + operation="median_blur", + description="A new median blur filter", + parameters={} + ) + + self.assertTrue(result) + filters = self.manager.get_filters_in_category("TestCategory") + self.assertEqual(len(filters), 3) + self.assertIn("new_test_filter", filters) + + def test_remove_filter(self): + """Test removing a filter.""" + result = self.manager.remove_filter("TestCategory", "test_filter") + self.assertTrue(result) + + filter_config = self.manager.get_filter("TestCategory", "test_filter") + self.assertIsNone(filter_config) + + def test_remove_last_filter_in_category(self): + """Test removing the last filter in a category removes the category.""" + self.manager.remove_filter("AnotherCategory", "another_filter") + self.assertNotIn("AnotherCategory", self.manager.get_categories()) + + def test_remove_nonexistent_filter(self): + """Test removing a non-existent filter.""" + result = self.manager.remove_filter("TestCategory", "nonexistent") + self.assertFalse(result) + + def test_validate_parameters_valid_slider(self): + """Test validation of valid slider parameters.""" + params = { + "intensity": {"min": 0, "max": 100, "init": 50, "interval": 1} + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertTrue(is_valid) + self.assertEqual(error_msg, "") + + def test_validate_parameters_valid_options(self): + """Test validation of valid options parameters.""" + params = { + "color": {"options": ["red", "green", "blue"], "init": "red"} + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertTrue(is_valid) + self.assertEqual(error_msg, "") + + def test_validate_parameters_invalid_min_max(self): + """Test validation with min > max.""" + params = { + "intensity": {"min": 100, "max": 0, "init": 50, "interval": 1} + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertFalse(is_valid) + self.assertIn("min", error_msg.lower()) + + def test_validate_parameters_invalid_init(self): + """Test validation with init out of range.""" + params = { + "intensity": {"min": 0, "max": 100, "init": 150, "interval": 1} + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertFalse(is_valid) + self.assertIn("init", error_msg.lower()) + + def test_validate_parameters_invalid_interval(self): + """Test validation with non-positive interval.""" + params = { + "intensity": {"min": 0, "max": 100, "init": 50, "interval": 0} + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertFalse(is_valid) + self.assertIn("interval", error_msg.lower()) + + def test_validate_parameters_missing_key(self): + """Test validation with missing required key.""" + params = { + "intensity": {"min": 0, "max": 100, "init": 50} # missing interval + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertFalse(is_valid) + self.assertIn("interval", error_msg.lower()) + + def test_validate_parameters_empty_options(self): + """Test validation with empty options list.""" + params = { + "color": {"options": [], "init": "red"} + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertFalse(is_valid) + self.assertIn("options", error_msg.lower()) + + def test_validate_parameters_init_not_in_options(self): + """Test validation with init not in options.""" + params = { + "color": {"options": ["red", "green", "blue"], "init": "yellow"} + } + is_valid, error_msg = self.manager.validate_parameters(params) + self.assertFalse(is_valid) + self.assertIn("init", error_msg.lower()) + + def test_save_filters(self): + """Test saving filters to JSON file.""" + from filter_manager import FilterManager + + self.manager.add_filter( + category="SaveTest", + filter_name="save_test_filter", + operation="glitch", + description="Test save filter", + parameters={} + ) + + # Create a new manager to reload from file + new_manager = FilterManager(self.temp_file.name) + self.assertIn("SaveTest", new_manager.get_categories()) + self.assertIsNotNone(new_manager.get_filter("SaveTest", "save_test_filter")) + + def test_load_nonexistent_file(self): + """Test loading from non-existent file creates empty config.""" + from filter_manager import FilterManager + manager = FilterManager("/nonexistent/path/filters.json") + self.assertEqual(manager.get_categories(), []) + + def test_get_available_operations(self): + """Test getting available operations.""" + operations = self.manager.get_available_operations() + self.assertIsInstance(operations, list) + self.assertIn("grayscale", operations) + self.assertIn("gaussian_blur", operations) + self.assertIn("pixelate", operations) + + def test_get_filter_info(self): + """Test getting filter info with apply function.""" + filter_info = self.manager.get_filter_info("TestCategory", "test_filter") + + self.assertIsNotNone(filter_info) + self.assertIn("apply", filter_info) + self.assertIn("parameters", filter_info) + self.assertTrue(callable(filter_info["apply"])) + + +class TestFilterEngine(unittest.TestCase): + """Test cases for FilterEngine class.""" + + def setUp(self): + """Set up test fixtures.""" + from filter_engine import FilterEngine + import numpy as np + + self.engine = FilterEngine() + # Create a simple test image (100x100 BGR) + self.test_image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + + def test_get_available_operations(self): + """Test getting list of available operations.""" + operations = self.engine.get_available_operations() + self.assertIsInstance(operations, list) + self.assertGreater(len(operations), 0) + self.assertIn("grayscale", operations) + self.assertIn("gaussian_blur", operations) + + def test_grayscale_operation(self): + """Test grayscale operation.""" + result = self.engine.apply_operation(self.test_image, "grayscale") + self.assertEqual(len(result.shape), 2) # Grayscale has 2 dimensions + + def test_gaussian_blur_operation(self): + """Test Gaussian blur operation.""" + result = self.engine.apply_operation( + self.test_image, "gaussian_blur", + kernel_size=5, sigma=1 + ) + self.assertEqual(result.shape, self.test_image.shape) + + def test_pixelate_operation(self): + """Test pixelate operation.""" + result = self.engine.apply_operation( + self.test_image, "pixelate", + block_size=10 + ) + # Result should be smaller due to block alignment + self.assertIsNotNone(result) + + def test_unknown_operation_raises_error(self): + """Test that unknown operation raises ValueError.""" + with self.assertRaises(ValueError): + self.engine.apply_operation(self.test_image, "unknown_operation") + + def test_hue_shift_operation(self): + """Test hue shift operation.""" + result = self.engine.apply_operation( + self.test_image, "hue_shift", + hue_shift=30 + ) + self.assertEqual(result.shape, self.test_image.shape) + + def test_glitch_operation(self): + """Test glitch operation.""" + result = self.engine.apply_operation( + self.test_image, "glitch", + glitch_intensity=5, glitch_frequency=10 + ) + self.assertEqual(result.shape, self.test_image.shape) + + +if __name__ == '__main__': + unittest.main() From 1b8cd6753f6afc60333bb40f7dc14b0fddf354c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:24:37 +0000 Subject: [PATCH 3/3] Add .gitignore and optimize glitch effect - Add .gitignore to exclude __pycache__ and other artifacts - Optimize glitch effect to compute step outside loop - Remove accidentally committed __pycache__ directories Co-authored-by: CorsoCoder <45120484+CorsoCoder@users.noreply.github.com> --- .gitignore | 110 ++++++++++++++++++ __pycache__/filter_engine.cpython-312.pyc | Bin 22605 -> 0 bytes __pycache__/filter_manager.cpython-312.pyc | Bin 10497 -> 0 bytes __pycache__/main.cpython-312.pyc | Bin 39620 -> 0 bytes filter_engine.py | 3 +- tests/__pycache__/__init__.cpython-312.pyc | Bin 142 -> 0 bytes .../test_filter_manager.cpython-312.pyc | Bin 18563 -> 0 bytes 7 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 .gitignore delete mode 100644 __pycache__/filter_engine.cpython-312.pyc delete mode 100644 __pycache__/filter_manager.cpython-312.pyc delete mode 100644 __pycache__/main.cpython-312.pyc delete mode 100644 tests/__pycache__/__init__.cpython-312.pyc delete mode 100644 tests/__pycache__/test_filter_manager.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3000f81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/__pycache__/filter_engine.cpython-312.pyc b/__pycache__/filter_engine.cpython-312.pyc deleted file mode 100644 index 87b8355492233998b3f3d73e4383b5d8aebe95bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22605 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 L`X88XAoteTL diff --git a/__pycache__/filter_manager.cpython-312.pyc b/__pycache__/filter_manager.cpython-312.pyc deleted file mode 100644 index 2b149bf682b539c920272c8d431d14d805f954dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10497 zcmd5iTWlLwc6Ue)-x4K}vSd-R#*%D`en?L2*p}j#Y(YSXJdD@I2nt9s6uIA^(Oi`ok6)n-8I}K*k79 z#tF~xrZ_Xs(AqR^g4!H6Pg=$;lk7MNyoTj(mC#Ah+vx7p$}ZF zCw3Owdg}?0e!ag~3)S}+?Tu&enY7lp$3zARZ+)BaHsf0jZwl86R-p!`6Y6;T38;C8 zP!l!r&bKY&-dR)7mHr3kKrEgTMD9X35x$0XlAn$XPUqFc*mn;RF{;z-pr52p}Xyia2)OnM!Kk!;us>6H8s!=i-HEED@sl2XlmiCNYD(iHZF`@bPK`(h0 z=1-?)8IFp~3nMQLYht20P-H?=;nej=LEtG9ux*CF%~zpVAQ>h@LbL;m z7=gEfq7ahO)tcNZKtM0z(kDYE9D2K0>yQkYWrC*kn@+CqoJWZZ$75-Mif7r@x(q0z z(I;M$dW|<6!`MzQcM&$lfpk|?5CdsA$ciplnd&J>trXHoCfe|;%@AT#P#py1+#(6_ zD8-bJSRD8GVE6Upq|hx+ClZ3#J(Cn~bYFO>e^e_pp$}1o7R0V8P@hl;^(z!g?g%#E{E`fqzeed&7L0%rNaFMV$ekfb3H zkgwA^l*6Lt8E8i1Wy-cK@{HydjjTA!fmo)(ATr<%z}!}z$rwy!#K-J(DthqvT}HJF ziAWN?%W%5+IZ*`LKNS|GqF>Q$aW5CNtCr-HkkAyug1V$yQ0~Plpov-waw-fbQv=EA z1W(6NJ-DHM0Y(?J56Q@`|OFw}E zMc?Y^V;YE z;b~Vq?Xtc7E6=u1zkTE)HM_qgjAL)Eea~F~XKpTUFU5IeGu{virmt(3Tc2I&TRAMZ zoL;LtBfHQ1HgCqB$5Ib0^R31{j{Ld5&td*+JG4G=uzfAIPpZw>ZlNVtbGFU=$;mn> zf99z^+hYD%3k&VxGA%`mL$&lnXe@m9I$BZtG9cAF`?2+2K{p`Ovf;m?*3R0t>gTMj zApiFCk&*CCfs0IwqL4^c7E0X+7lmJCX2b^Aqu2<=uW;f4Fsas9f`>s=w~&ygMIjWH zBC#0S=OzS2BQ0Xkrds&v$tfz5K@$}*k%P|#GfV|cbwd;-=+=6xq?Ps^M$G4+xJ`ap zRXg9Y(DiQDQhV06UUeK~%yEHP>3iU=`N8=+=jZv29s86W`LjB zeL>5|5-#$+jy|jT6D!;2v6XQbw@JDCJT$(UyAy_A1@OY7`^|d=TRTF6OjUoAev#Y+z@7^ zyA0)pgiF)ZT&>1furLD5oo+7I`10Q4fG6#O;x>7N8a&^>;puY1jN zNVXpmy#S)x^>vj+Ee3^q0S2NLG6D70XQJTKO222A7}4dO()6FV(KJdUa0y~%F3d?& zLL?TAMGW#(AnW$nL|=0+Hwvymf1{K&rUMhn87~z%Q{oYTKiaa;HSEQ6L1}87UK>t%tR^+td%_*P%%OK4l>Q0fd+QQ1|`kgYlWapYZ z6lO$H7>oL5X&9(*Fy#1R&;Y)EIGGT*Sd>d7!9JtwHij1u0d?F9WRyb;O6?-lKhSUiK@O(#Qo^N=16i?5}{x#2lY#*TPFXOlop=M#2@00Jd z3*-)&DA+mpWIS*dG3mN`%bYPw%q){JSK>8q@cNI~J1oM@v2$=xgd?Q=mL+3(0+yv{ z3!VVM@>YEXyzNJ3kR*H91xqyui^5dkXDkuL19l`njR6={%kb1xe3pw&CunH*|5&Q% z+Dn9qrUwFa`2h~|69I@LL0%!NAO$#d0tN+!RR=_pTB4%N3!+y;FTQyOia#|i5EHp$ zu4tz#oJum}jwxz_^C$Bm^T!No@GzJ$%p0g4x~z~fDdZ~H*d(M~R4ZMiB%*`STohUt zj=O!kAmJJ&@+1A!lO!ArVKmuwBA$$d)Ryf?VdlfaN^w)8{Tfk z+nr^zvx@h{`vXwO?iV$a=PZ&;bwP3_BF1PMMJ3=3fSCHY(Ir-lR>m>JKrp?7%QXcyn)WMA`R}ABdYb?VtL{GyoUb)oll)!aPPJ_U|CcqwVLPCw_jQ;YOOH5EN z*I%?p`sCWm$|f#aFu*7YHKLeS(A+_J;8b)AxL0GWis9|3I8CD!X*v=SBq=%_Kfn!u z%>`-zEyS6aR1A;O-m%1uL~K4L7H_xur(My)SQt3}DW`H`mm$+O%)k1m^RY zbI!71Z&vKhi-saMlTSE;Y4}prldvfsu5rtwn;uLX!wri${hJb5noL3-BCIR3aW(Rt zsqZ-tC)DZ8Ypx;J<5s*0qw_I6b1A9+5|1QBGe`4qeGXa;>IAsp(j{q||gR zzbV%|3!Z<&8&JG(zFYM^mGd=fC->z>+4pqLU9;hCQQR%-?pA=yd3>5kmPU7&6Q&}W zg|B6q5hi88=@B=L_nEKDj`Wjx=1LtQuK?~X(=BsJuG^F{{D?lDXv&z-)6AMm;LH`~ zb@FE3f*E0+ecv24MF3|6E(SAZP=wzr#pSK&mr}SZ`WQ3Srd;~Bj9G)aSBQIf+YPWs zVsQ?8$s7)tgDL5A7W-fv2lax5@oq8_^UiH!rLH36HNv}p!r-~5pIoU2%CH$0#|3!1 z0biA2VSm-{!$lkJPursfuCh{SGU)>|dT>U4<+rS5ee_Dy=(Cme(Ulr~_Oia(vN0VQ zYsPk?zlH70y=z=K#7w_E0j-Gw-;JUDg*NEM#^E+$W%apic@@ z*7S**;69)VvY|TH>oly(1*#Z#01fG1IME*91KIBw5B~m%p73`HC=53${a-lP>8Usk zEjnXTh=vBe+-u1sxLEj@T7>>dNX;%?8kZuVSVuliw;?SxsNl!_Vi1r}8C(z3U3SR;x2*o)t z+*d7l!Io|S;K`Uo?>hqcC4dppw2Gb}&^^h`I{H!#UDC8B0JBenT9B$yGbO zMu#ENJ4;3uh1aKJA_RJH4CVrlnSh8%5pfucmvGGpOy?~UFEKqSh_Q&a9hd}25Kds4 z2Ew*$G;YLOdbqd`z~q5+aQk{+6H+sRfB=Y1kK`0|mj((4bC|M7p{b+<_p3MIo_Pzk;EpFTvD8ZW(ftO^f?JbKYkkc0Rq)d0go{ zzSh~hNp?H_fKl8Xb1y8qA9!09Cq8ap_jdi|#ODp?e+jSgkNelby)`s1xt3p8J|Q0) zm3NKF4P&b{V-G!zi$jX1^FhPzr31^eN?XrQkE}PG`T4m|-SXJ0%JZSM{;M)Cto92z zKezN^*14RKpS>!#g=K$u)fdircP=$6-h(BbFUxHsvVUaNH(HC-256>iRe5hnZVjzBhVm>z=k3y?gUYrBrm4{m*J?O1vxyI0xMv)0-> z|NH~`x=-12a;^2${PS?;_xTrpf7RPjvbfR>`WLUQdiN|hW(QY;&z8XOa+@Igg;k%B z^ZFNG{}V_Q7zDa3w~fpG@m1e={v3n6$qz83rt#O0>dDTw4}0Z?F1Vh9g_od=%57t^ ze{9`12KRH&^;m*Hfc&lNcz=)S=RFP2)w6%+W1xiS$p;;nJ~~6|2E35RQ&%XYIzu7t z*E!I3heEGUhvS7FTPVaQBk;L);QY|&b1#pC@KP&uv2XNwaTLZ8aW)ZS8PS3TUUbrf z2Gu0-Aih401*XBY+|D^{;o)Dkpcxhi@%=o$vne#yBL48=E?iCHcY1q@iXmcwpQO_u z)^C%~Eyo^q_IyE{Z1Yn4CRX`I58JWGFCAXCERST{moMF|mfMdiyN+!V=-6yCvlp1W z)5bovWZ5K8;Q#@_P@ljmv!Y@QOKm{B9yo;Prm}Bdj#qB1Mf~f#|=~pi&Zn9^*j= zM1xgK#6hrXbli5P@H>6Zx#JplzfmwO@kKpVWvL7poTh$&qTANP=s% z@i7tg3pFqbKQRb}L>_0La*mq-u7s##)PDB{{kYmKyLxOtp(*VtQG#BKtECteUHqSX0svbq>{ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 0343891e9e5823cbaed40c57351639d72fcb7023..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39620 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

a4ZC zsh8eRP(b}Xh&q_Q>7FlLZrQm5ejXH_v%5PU#2jvS--AMzyOCTrR6p33@9uq2UgEBN z(AMpK+5Y2cd%jy~Hx)REvb2k)i>` z!i;h5_fcD}4j=-c`3g#mzm*xO4G0RMsX<2~CHD3hc zMi0Ymp;{aWbp6M$ruU^1ksxDf|6-TgB4`6?wP6{Pklb{sDRJ0>O8981PQ#gN<6(@5(!CVWz$j0(q zG#QI9(e%+soD3tcFbg{Qe%HR4E>iIv$I*1h$ZhWUMvgqKLbgrDaPyZ+ zn`$@_1uK^d{eWHaGS+e!LO3#V=M~p@BqEm~gO4V794sSYh&Np_T>04-z~t8n;XDu? zbAbi8K7`_#J&khiqmyXP2SN!DpmC)D9V@pGolfjTMkWx+he5Nfu0e*+P=PRze>94> zxjpjGDM*X;hlY&}-S6RsC=8$m_98d1(qH)Yo=wtPP$8S7G`2*k(13(A!v7lir2_q+ zZ!pT>mnl}KGVR36p8>*XyXB#M3T4VZ#fUt}DxV1&zKBJru1GgNstD{bg!jrqRJMCU zZm=#RG$11z8h^P0RzMkzJxv;ReD(!VzT~g<;*f}h9o)9}v zWc(-Za0S=$$ZzTJZLW#U3TT*J)Lav_JV0@T6lB%}waS_v9;Y`H{6`FGf=3QU4>vs` zXJ-$}YuqR74+h-s&Yyq|ZoY3rz^Y|gvfRW3`;7KPa1V3S21W=;SX(eI;|oM)Z(HhQGrM=?*I zz4`hpk8Jq4FW7A^|8nYcXVnkAN4T*Ik9*IsNU^8CTi5 h%YXIocaDAg*z(!8UcUD7uf1~HRfVsaxT;xq|37Fh_xb<; diff --git a/filter_engine.py b/filter_engine.py index fe06481..ee90426 100644 --- a/filter_engine.py +++ b/filter_engine.py @@ -249,7 +249,8 @@ def _glitch(self, image: np.ndarray, glitch_intensity: int = 10, rows, cols = image.shape[:2] glitch_image = image.copy() - for i in range(0, rows, max(1, glitch_frequency)): + step = max(1, glitch_frequency) + for i in range(0, rows, step): shift = np.random.randint(-glitch_intensity, glitch_intensity + 1) glitch_image[i] = np.roll(image[i], shift, axis=0) diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 858469fa8090b5402a1cce8f42ee0e7e3db97324..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 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 SKKS*6-#GNpAvl}p^!-005<33?