<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Código en Llamas | Programación, Algoritmos y Software explicado paso a paso]]></title><description><![CDATA[Espacio dedicado a explorar temas de programación, algoritmos, visión por computadora.]]></description><link>https://codigoenllamas.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1770237596105/963b6c67-ad0f-43bf-804d-bbb23091784f.png</url><title>Código en Llamas | Programación, Algoritmos y Software explicado paso a paso</title><link>https://codigoenllamas.com</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 08 Apr 2026 23:50:57 GMT</lastBuildDate><atom:link href="https://codigoenllamas.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Transferencia de color y sus aplicaciones en procesamiento de imágenes]]></title><description><![CDATA[La transferencia de color (color transfer). Es una técnica del procesamiento de imágenes cuyo objetivo es modificar las propiedades de color de una imagen a partir de otra como referencia, preservando la estructura visual de la escena original.
Exist...]]></description><link>https://codigoenllamas.com/transferencia-de-color-y-sus-aplicaciones-en-procesamiento-de-imagenes</link><guid isPermaLink="true">https://codigoenllamas.com/transferencia-de-color-y-sus-aplicaciones-en-procesamiento-de-imagenes</guid><category><![CDATA[image processing]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Fri, 30 Jan 2026 17:20:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769665417213/81f028da-a853-4dbd-ba4b-37e89a448947.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>La transferencia de color (<em>color transfer</em>).</strong> Es una técnica del procesamiento de imágenes cuyo objetivo es modificar las propiedades de color de una imagen a partir de otra como referencia, preservando la estructura visual de la escena original.</p>
<p>Existen diversas estrategias para la transferencia de color entre imágenes, incluyendo modelos basados en mezclas gaussianas, enfoques estadísticos, métodos apoyados en machine learning, así como combinaciones de varios enfoques.</p>
<p>Muchos métodos de transferencia de color presentan una limitación importante cuando no existe correspondencia semántica entre la imagen fuente y la imagen objetivo. En estos casos, los enfoques basados en transferencias globales tienden a producir resultados inconsistentes, al no considerar la semántica de la escena.</p>
<p>Una estrategia habitual para abordar esta limitación consiste en incorporar información semántica mediante técnicas de segmentación. Sin embargo, la segmentación precisa de una imagen sigue siendo una tarea compleja, especialmente cuando se busca una solución completamente automatizada. Por esta razón, cuando se requiere un mayor nivel de control y coherencia visual, resulta común recurrir a métodos de transferencia de color asistidos por el usuario.</p>
<p>En la práctica, la transferencia de color suele implementarse mediante transformaciones estadísticas aplicadas sobre los canales de color. Estas transformaciones difieren principalmente en qué estadísticas de la imagen se igualan y en cuánta información de la distribución cromática se tiene en cuenta, lo que da lugar a métodos con distintos niveles de expresividad y complejidad.</p>
<p>A continuación se presentan tres métodos estadísticos clásicos para la transferencia global de color.</p>
<h2 id="heading-normalizacion-de-media-y-desviacion-estandar-reinhard">Normalización de media y desviación estándar (Reinhard)</h2>
<p>El método propuesto por Reinhard constituye un enfoque fundamental al formular la transferencia de color como un problema de igualación de estadísticas de primer y segundo orden. El procedimiento se realiza en el espacio de color Lab, elegido por su mayor aproximación a la percepción humana y su separación explícita entre luminancia (L) y cromaticidad (a, b).</p>
<p>Para cada canal c ∈ {L, a, b}, el método ajusta la imagen fuente para que su media y desviación estándar coincidan con las de la imagen objetivo:</p>
<p>$$I_c' = \frac{\sigma_c^{(t)}}{\sigma_c^{(s)}} (I_c - \mu_c^{(s)}) + \mu_c^{(t)}$$</p><p>Este proceso puede interpretarse como una normalización seguida de una reescalación, aplicada de forma independiente a cada canal. El resultado es una transferencia estable, computacionalmente eficiente y robusta frente al ruido.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769704496299/f753ef79-aed3-4618-9bfa-80dd0e23a753.jpeg" alt="Triptych illustrating source images and a chromatically rich target image used for color transfer." /></p>
<p>Sin embargo, al limitarse a estadísticas de primer y segundo orden, este método no puede alterar la forma de la distribución (asimetría, multimodalidad), ni modelar correlaciones entre canales.</p>
<p><strong>(Reinhard):</strong></p>
<pre><code class="lang-text">Input: source_image, target_image
Convert source_image and target_image to Lab space

for each channel c in {L, a, b}:
    mu_s  = mean(source_image[c])
    sigma_s = std(source_image[c])

    mu_t  = mean(target_image[c])
    sigma_t = std(target_image[c])

    for each pixel p in source_image[c]:
        p' = (p - mu_s) * (sigma_t / sigma_s) + mu_t
        assign p' to output_image[c]

Convert output_image back to RGB
</code></pre>
<hr />
<h2 id="heading-ajuste-de-histogramas-y-cdf-matching">Ajuste de histogramas y CDF matching</h2>
<p>Para superar las limitaciones asociadas al uso de estadísticas de bajo orden, los métodos basados en histogram matching o ajuste de la función de distribución acumulada (CDF) extienden el control estadístico al considerar la distribución marginal completa de cada canal.</p>
<p>La idea central consiste en encontrar una transformación monótona <em>Tc</em> tal que la CDF del canal fuente coincida con la del canal objetivo:</p>
<p>$$T_c(x) = F_c^{(t),-1}(F_c^{(s)}(x))$$</p><p>Donde</p>
<p>$$\quad( F_c^{(s)}) \quad y \quad (F_c^{(t)} )$$</p><p>son las CDF del canal ( c ) de la imagen fuente y objetivo, respectivamente.</p>
<p>Este enfoque permite redistribuir los valores de intensidad de manera no lineal, ajustando contraste, rango dinámico y densidad de probabilidad, lo que resulta especialmente útil en el canal de luminancia. No obstante, al operar de forma independiente sobre cada canal, sigue sin capturar dependencias estadísticas entre componentes cromáticas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769704545792/1ff9b1bd-9dcc-49d0-a9de-e23d9d5f0357.jpeg" alt="Triptych illustrating source images and a chromatically rich target image used for color transfer (The Crimean Landscape, Konstantin Bogaevsky)." class="image--center mx-auto" /></p>
<p><strong>Pseudocódigo (Histogram Matching):</strong></p>
<pre><code class="lang-text">Input: source_image, target_image
Convert images to chosen color space (e.g., Lab)

for each channel c:
    hist_s = histogram(source_image[c])
    hist_t = histogram(target_image[c])

    cdf_s = cumulative_sum(hist_s)
    cdf_t = cumulative_sum(hist_t)

    mapping = inverse_cdf(cdf_t) ∘ cdf_s

    for each pixel p in source_image[c]:
        p' = mapping(p)
        assign p' to output_image[c]

Convert output_image back to RGB
</code></pre>
<hr />
<h2 id="heading-transformacion-de-blanqueamiento-y-recoloreo-wct">Transformación de blanqueamiento y recoloreo (WCT)</h2>
<p>A diferencia de los enfoques anteriores, la <strong>Whitening and Coloring Transform (WCT)</strong> formula la transferencia de color como un problema estadístico multivariado, modelando explícitamente la matriz de covarianza completa de la distribución cromática.</p>
<p>Sea</p>
<p>$$X_s \in \mathbb{R}^{3 \times N}$$</p><p>la imagen fuente representada como un conjunto de vectores de color centrados. El método consta de dos etapas:</p>
<ol>
<li><p><strong>Blanqueamiento (Whitening)</strong>: elimina las correlaciones entre canales y normaliza la varianza, transformando la distribución para que tenga covarianza identidad.</p>
</li>
<li><p><strong>Recolorización (Coloring)</strong>: impone la covarianza de la imagen objetivo, reintroduciendo correlaciones y escalas cromáticas.</p>
</li>
</ol>
<p>Este procedimiento permite transferir no solo estadísticas marginales, sino también estructuras de dependencia entre canales, lo que se traduce en resultados visualmente más ricos y coherentes.</p>
<p>Es importante destacar que WCT no genera nueva información cromática: la distribución final replica la del objetivo. Por tanto, la calidad del resultado depende directamente de la diversidad cromática presente en la imagen de referencia.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769704452214/2ba48ba5-c78e-4f60-9c8c-b3d5151365fc.jpeg" alt="Source image (photograph of Scarlett Johansson) and target image (La Raie verte, original French title) used for color transfer." class="image--center mx-auto" /></p>
<p><strong>(WCT):</strong></p>
<pre><code class="lang-text">Input: source_image, target_image
Reshape images to matrices X_s, X_t of size (3, N)

Compute mean vectors mu_s, mu_t
Center data:
    X_s = X_s - mu_s
    X_t = X_t - mu_t

Compute covariance matrices:
    C_s = cov(X_s)
    C_t = cov(X_t)

Eigen decomposition:
    C_s = E_s * D_s * E_s^T
    C_t = E_t * D_t * E_t^T

Whitening:
    X_w = D_s^{-1/2} * E_s^T * X_s

Coloring:
    X_c = E_t * D_t^{1/2} * X_w

Add target mean:
    X_out = X_c + mu_t

Reshape X_out back to image
</code></pre>
<p>Cada uno de los métodos ofrece un nivel distinto de control sobre el color transferido. Sin embargo, en la práctica, los tres tienden a producir resultados visualmente similares cuando se aplican a imágenes con distribuciones cromáticas compatibles. Esto se debe a que todos parten de una misma idea fundamental: la redistribución estadística de los canales de color.</p>
<p>Las diferencias se vuelven más evidentes en escenarios de estilización, especialmente cuando la imagen objetivo presenta una estructura cromática muy distinta de la imagen fuente, caso en el cual WCT puede ofrecer resultados más expresivos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769735563490/02c3e305-e06a-429c-9f87-0d782bc3edf9.jpeg" alt="Source image (AI-generated) and target image (Portrait de Matisse) used for color transfer." class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-aplicaciones">Aplicaciones</h2>
<p>Aunque la transferencia global de color no resulta adecuada para muchas aplicaciones, existe un escenario en el que puede ser especialmente útil: <strong>el realce cromático de una imagen</strong>. Cuando se dispone de imágenes con una estructura de color similar, es posible emplear una imagen objetivo con mayor dispersión cromática o contraste para transferir estas características a la imagen fuente.</p>
<p>El objetivo, por tanto, <strong>no es cambiar la semántica de la imagen ni su contenido visual</strong>, sino redistribuir la información de color para mejorar contraste, saturación y riqueza cromática, preservando la estructura original. Desde un punto de vista estadístico, este problema puede interpretarse como una expansión y reorganización de la varianza de los canales de color, sin introducir nuevas estructuras ni dependencias espaciales.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769704408782/e60f8e75-2056-4862-a1b6-b81efa282f16.jpeg" alt="Color transfer results using Mona Lisa as source and Portrait of a Lady in an Empire Dress as target, with outputs from Reinhard, CDF, and WCT." class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769704369662/99a7948a-5d91-48a7-a8af-fbcd7a6c18b6.jpeg" alt="Source image (Janet and Iris Laing) and target image (picnic scene by Brebca) used for color transfer." class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769731854532/5750db34-b092-4822-ac2a-51255f734714.jpeg" alt="Color transfer results with an AI-generated source image and a Pop Art target, using Reinhard, CDF, and WCT." class="image--center mx-auto" /></p>
<p>Bajo estas condiciones, la transferencia global de color resulta adecuada siempre que la imagen objetivo comparta una <strong>estructura cromática global similar</strong> con la imagen fuente, pero presente una mayor dispersión en su distribución de color. En la práctica, esto implica elegir imágenes de referencia más contrastadas o con una paleta cromática más rica, pero sin diferencias extremas que puedan introducir artefactos visuales.</p>
<h2 id="heading-conclusion">Conclusión</h2>
<p>Muchos de los trabajos revisados para escribir este articulo abordan la transferencia de color desde una perspectiva orientada a la estilización visual, buscando efectos artísticos como transformar un amanecer en un atardecer o modificar radicalmente la atmósfera de una escena. Sin embargo, siendo crítico, gran parte de estos enfoques presenta limitaciones importantes para lograr este tipo de resultados de forma consistente y controlada.</p>
<p>Para obtener resultados visualmente satisfactorios en escenarios de estilización compleja suele requerir el uso de herramientas de edición manual, como Photoshop, que permiten realizar correcciones locales y ajustes finos. Aun así, los métodos de transferencia de color pueden desempeñar un papel valioso como punto de partida, proporcionando una base cromática mejorada que reduce significativamente el número de ajustes posteriores necesarios.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[Haciendo a Python más rápido con Numba]]></title><description><![CDATA[Python es un lenguaje muy querido por su facilidad de uso y por la rapidez con la que permite escribir código. Sin embargo, una de sus limitaciones más conocidas es el rendimiento en tareas de cómputo intensivo, pues al tratarse de un lenguaje interp...]]></description><link>https://codigoenllamas.com/haciendo-a-python-mas-rapido-con-numba</link><guid isPermaLink="true">https://codigoenllamas.com/haciendo-a-python-mas-rapido-con-numba</guid><category><![CDATA[Python]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[numpy]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sat, 10 Jan 2026 08:47:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768034359132/82515032-34f8-442d-81e2-e2364670d429.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Python es un lenguaje muy querido por su facilidad de uso y por la rapidez con la que permite escribir código. Sin embargo, una de sus limitaciones más conocidas es el rendimiento en tareas de cómputo intensivo, pues al tratarse de un lenguaje interpretado, suele ser más lento que uno compilado.</p>
<p>Esta limitación se hizo muy evidente mientras desarrollaba mi propia librería de algoritmos de procesamiento de imágenes que inicie justo en Python para aprovechar el amplio soporte de bibliotecas que tiene. En un inicio, la librería surgió únicamente como apoyo para los ejemplos de código de mis artículos, pero con el tiempo fue creciendo y comencé a utilizarla también en otros proyectos.</p>
<p>Aunque el rendimiento no siempre era el mejor, durante bastante tiempo no le di importancia, hasta que los tiempos de ejecución comenzaron a volverse un problema real, pero la cantidad de código y la complejidad hacían poco viable una reescritura completa en C++.</p>
<p>En lugar de optar por la reescritura, decidí explorar alternativas de optimización dentro del propio ecosistema de Python, comenzando por la simplificación de los algoritmos y la utilización de librerías para ciertas operaciones, pero la mejora más significativa que obtuve fue el uso de Numba.</p>
<h2 id="heading-que-es-numba">Qué es Numba</h2>
<p>Numba es un compilador <em>just-in-time</em> (JIT) para Python que transforma funciones numéricas en código máquina optimizado durante la ejecución. Su principal ventaja es acelerar secciones críticas del código sin abandonar Python ni reescribirlo en otro lenguaje.</p>
<p>El mayor beneficio se obtiene en funciones numéricas bien definidas, con control de flujo simple y uso intensivo de bucles, especialmente cuando operan sobre arreglos de NumPy con tipos homogéneos. En cambio, código fuertemente dependiente de objetos de Python, estructuras dinámicas o librerías ya optimizadas en C suele beneficiarse poco, e incluso puede verse afectado por la sobrecarga del proceso JIT.</p>
<p>Como ejemplo voy a mostrar el proceso de optimizacion de mi propia implementacion del algoritmo CLAHE. Al trabajar con cálculos locales y recorridos explícitos sobre los pixels de la imagen, resulta apropiado para mostrar cómo Numba puede acelerar este tipo de código en procesamiento de imágenes.</p>
<h2 id="heading-breve-explicacion-de-que-es-clahe">Breve explicación de qué es CLAHE</h2>
<p>CLAHE (<em>Contrast Limited Adaptive Histogram Equalization</em>) es una variante del ecualizado de histograma que mejora el contraste de forma local. En lugar de aplicar una transformación global, divide la imagen en bloques y calcula un histograma independiente por región, lo que permite realzar detalles locales incluso en presencia de variaciones de iluminación.</p>
<p>Para evitar la amplificación del ruido, introduce un límite de recorte (<em>clip limit</em>) sobre los histogramas locales y redistribuye el exceso de forma controlada. Posteriormente, los resultados de cada bloque se combinan mediante interpolación para evitar discontinuidades visibles entre regiones.</p>
<p>Por su estructura basada en cálculos locales repetitivos y recorridos explícitos sobre los pixels de la imagen, hacen de CLAHE un buen candidato para analizar el impacto de distintas estrategias de optimización en Python.</p>
<p>En una implementación escrita solo en python, el coste de los bucles domina el tiempo de ejecución, dando lugar a un rendimiento muy inferior al de implementaciones optimizadas como la de OpenCV(hasta 17,000 veces mas lento), lo que hace evidente la necesidad de refactorizacion.</p>
<h2 id="heading-identificacion-del-cuello-de-botella">Identificación del cuello de botella</h2>
<p>El primer paso en cualquier proceso de optimización consiste en identificar con claridad el cuello de botella, es decir, la operación o conjunto de operaciones que están dominando el tiempo total de ejecución. Para ello se pueden utilizar herramientas de análisis como <code>cProfile</code>, lo que permite obtener una visión global del comportamiento de la implementación del algoritmo.</p>
<pre><code class="lang-python">print(<span class="hljs-string">"=== cProfile: Custom CLAHE ==="</span>)
cProfile.run(<span class="hljs-string">"HistogramEqualizationClaheGrayscale(img)"</span>)
</code></pre>
<p>El análisis muestra que la mayor parte del tiempo de ejecución no se concentraba en el cálculo de histogramas ni en la generación de las tablas de búsqueda, sino en la etapa de interpolación final. En particular, la función encargada de aplicar la interpolación espacial entre bloques acumulaba más del 60% del tiempo total, con cientos de miles de llamadas a funciones auxiliares y operaciones elementales dentro de bucles en Python.</p>
<p>Este resultado dejaba claro que el principal problema de rendimiento no esta en el algoritmo en sí, sino en la ejecución repetitiva de bucles y operaciones a bajo nivel dentro del intérprete de Python.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768033843400/bf92f3b6-0939-40bb-9e09-aa52919d6766.png" alt /></p>
<h3 id="heading-la-interpolacion-como-principal-problema">La interpolación como principal problema</h3>
<p>Al inspeccionar el perfil, la función que más tiempo consume es la encargada de la interpolación bilineal:</p>
<pre><code class="lang-text">1.096 s  ApplyInterpolation
0.234 s  InterpolationIndices
</code></pre>
<p>Es decir, <strong>la mayor parte del tiempo se recorre píxel a píxel en la imagen</strong>, combinando los mapas de transformación generados.</p>
<h3 id="heading-implementacion-original-clara-pero-costosa">Implementación original: clara pero costosa</h3>
<p>La versión original priorizaba la legibilidad y la separación de responsabilidades. Conceptualmente es correcta, pero desde el punto de vista del rendimiento es muy cara:</p>
<pre><code class="lang-python"><span class="hljs-keyword">for</span> y <span class="hljs-keyword">in</span> range(height):
    i0, i1, y_weight = InterpolationIndices(y, cell_h, n_rows)

    <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> range(width):
        j0, j1, x_weight = InterpolationIndices(x, cell_w, n_cols)
        ...
</code></pre>
<p>Aquí ocurre lo peor para Python:</p>
<ul>
<li><p>Bucles anidados en Python puro</p>
</li>
<li><p>Llamadas repetidas a <code>InterpolationIndices</code></p>
</li>
<li><p>Uso de <code>np.floor</code> y <code>np.round</code> en contexto escalar</p>
</li>
</ul>
<p>Cada una de estas operaciones es pequeña, pero ejecutadas cientos de miles de veces se vuelven dominantes.</p>
<h2 id="heading-enfoque-optimizado-menos-abstraccion-mas-control">Enfoque optimizado: menos abstracción, más control</h2>
<p>La versión optimizada cambia completamente el enfoque. En lugar de llamar a funciones auxiliares, <strong>toda la lógica de interpolación se integra directamente dentro del bucle</strong>, y se compila con Numba:</p>
<pre><code class="lang-python"><span class="hljs-meta">@njit(cache=True, fastmath=True)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">apply_interpolation_numba</span>(<span class="hljs-params">...</span>):</span>
    <span class="hljs-keyword">for</span> y <span class="hljs-keyword">in</span> range(h):
        fy = (y + <span class="hljs-number">0.5</span>) / cell_h - <span class="hljs-number">0.5</span>
        ...
        <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> range(w):
            fx = (x + <span class="hljs-number">0.5</span>) / cell_w - <span class="hljs-number">0.5</span>
            ...
</code></pre>
<p>Las decisiones clave aquí son:</p>
<ul>
<li><p>Eliminar llamadas a funciones dentro del bucle crítico</p>
</li>
<li><p>Reemplazar operaciones de NumPy por aritmética simple</p>
</li>
<li><p>Usar tipos estáticos y bucles compilados con <code>@njit</code></p>
</li>
</ul>
<p>Esto permite que Numba genere código máquina eficiente, muy cercano a lo que escribirías en C o C++.</p>
<p>Este ejemplo deja una lección clara:<br />cuando el cuello de botella está en bucles intensivos y operaciones simples repetidas muchas veces, <strong>el problema no es Python como lenguaje, sino el coste del intérprete</strong>. En esos casos, Numba encaja de forma natural como una herramienta de optimización, especialmente en contextos de investigación donde la claridad del código sigue siendo importante. ahora las ordenes de magnitud estan solo en 5 y no en 170000.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768034036386/1b92e606-890b-461c-9755-609d6551fe39.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-evitar-comprobaciones-repetitivas-en-el-camino-critico">Evitar comprobaciones repetitivas en el camino crítico</h3>
<p>Una optimización menos evidente, pero igual de importante, es, <strong>eliminar validaciones y comprobaciones dentro de funciones que se ejecutan miles de veces</strong>. En Python solemos escribir código defensivo, validando tipos, dimensiones y rangos en cada llamada. Esto es correcto desde el punto de vista de la robustez, pero puede ser muy costoso cuando esas funciones forman parte del camino crítico del algoritmo.</p>
<p>Por ejemplo, en la versión original del cálculo del histograma se realizan varias comprobaciones en cada llamada:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">cal_histogram</span>(<span class="hljs-params">channel</span>):</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> isinstance(channel, np.ndarray):
        <span class="hljs-keyword">raise</span> TypeError(...)

    <span class="hljs-keyword">if</span> channel.ndim != <span class="hljs-number">2</span>:
        <span class="hljs-keyword">raise</span> ValueError(...)

    <span class="hljs-keyword">if</span> channel.dtype != np.uint8:
        <span class="hljs-keyword">raise</span> ValueError(...)
</code></pre>
<p>Estas validaciones son razonables si la función se usa de forma aislada o como API pública. El problema es que, en CLAHE, esta función se ejecuta <strong>una vez por bloque</strong>, y el número de bloques puede crecer rápidamente. El coste de estas comprobaciones termina siendo mayor que el propio cálculo del histograma.</p>
<h3 id="heading-validar-una-vez-no-cien-veces">Validar una vez, no cien veces</h3>
<p>La clave aquí es separar responsabilidades:</p>
<ul>
<li><p>Las <strong>validaciones</strong> deben hacerse una sola vez, al inicio del procesamiento.</p>
</li>
<li><p>Las <strong>funciones internas</strong> deben asumir que los datos ya son correctos.</p>
</li>
</ul>
<p>En el contexto de CLAHE, tiene mucho más sentido comprobar que la imagen es <code>uint8</code>, 2D y válidar <strong>antes de entrar al bucle principal</strong>, y no dentro de cada llamada al histograma o al recorte del histograma.</p>
<p>Este cambio reduce drásticamente el trabajo innecesario del intérprete y, siguiendo esa misma idea, las versiones especializadas para el núcleo del algoritmo —como las optimizadas con Numba— eliminan por completo las comprobaciones para centrarse exclusivamente en el cálculo.</p>
<pre><code class="lang-python"><span class="hljs-meta">@njit(cache=True, fastmath=True)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">cal_histogram_numba</span>(<span class="hljs-params">block</span>):</span>
    hist = np.zeros(<span class="hljs-number">256</span>, dtype=np.int32)
    h, w = block.shape

    <span class="hljs-keyword">for</span> y <span class="hljs-keyword">in</span> range(h):
        <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> range(w):
            hist[block[y, x]] += <span class="hljs-number">1</span>

    <span class="hljs-keyword">return</span> hist
</code></pre>
<p>Aquí se asume explícitamente que:</p>
<ul>
<li><p><code>block</code> es 2D</p>
</li>
<li><p>los valores están en <code>[0, 255]</code></p>
</li>
<li><p>el tipo de dato es el esperado</p>
</li>
</ul>
<p>No hay verificaciones, no hay conversiones, no hay llamadas a funciones externas. Solo bucles y operaciones simples, exactamente lo que Numba puede optimizar bien.</p>
<p>Lo mismo ocurre con el recorte del histograma:</p>
<pre><code class="lang-python"><span class="hljs-meta">@njit(cache=True, fastmath=True)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">clip_histogram_numba</span>(<span class="hljs-params">hist, clip_limit</span>):</span>
    excess = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">256</span>):
        <span class="hljs-keyword">if</span> hist[i] &gt; clip_limit:
            excess += hist[i] - clip_limit
            hist[i] = clip_limit

    incr = excess // <span class="hljs-number">256</span>
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">256</span>):
        hist[i] += incr

    <span class="hljs-keyword">return</span> hist
</code></pre>
<h2 id="heading-ejemplos">Ejemplos</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768034098414/f6ac3de1-e681-4d01-9c22-87857b60ea0a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768034082377/aa93dd14-ffcd-46b9-8fb8-f183e17df5eb.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768034064256/4b4da754-0055-4981-97ca-72b15a84ac00.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">conclusion</h2>
<p>La idea central es simple: el código rápido no es el más defensivo, sino el que ejecuta solo lo necesario, una vez y en el lugar correcto. En algoritmos intensivos, como los de procesamiento de imágenes, conviene validar al inicio, fallar rápido y mantener el núcleo del algoritmo lo más simple y predecible posible.</p>
<p>Numba refuerza esta mentalidad al separar claramente la lógica de alto nivel del código crítico de bajo nivel. Aunque Python tenga límites en cómputo intensivo, un algoritmo bien diseñado —con lógica simplificada y sin trabajo innecesario en el camino crítico— puede acercarse mucho al rendimiento de código compilado usando herramientas como NumPy y Numba.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[Algoritmo de Otsu para la Umbralización y Segmentación de Imágenes]]></title><description><![CDATA[La umbralización (thresholding) es una técnica básica de segmentación que permite separar una imagen en escala de grises en dos regiones: primer plano y fondo. Este proceso convierte la imagen en una representación binaria comparando la intensidad de...]]></description><link>https://codigoenllamas.com/algoritmo-de-otsu</link><guid isPermaLink="true">https://codigoenllamas.com/algoritmo-de-otsu</guid><category><![CDATA[Python]]></category><category><![CDATA[image processing]]></category><category><![CDATA[opencv]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Thu, 18 Dec 2025 08:56:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766047872898/bdbe8023-380e-40fa-b4db-fa2c021568a0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La umbralización (<em>thresholding</em>) es una técnica básica de segmentación que permite separar una imagen en escala de grises en dos regiones: primer plano y fondo. Este proceso convierte la imagen en una representación binaria comparando la intensidad de cada píxel con un valor de umbral previamente definido, los píxeles con intensidades superiores al umbral se clasifican como objeto, mientras que el resto se considera fondo.</p>
<p>Este enfoque requiere definir un umbral que permita separar correctamente ambas regiones. El análisis del histograma de intensidades resulta especialmente útil en este contexto, ya que, en muchos casos, el fondo y el objeto presentan distribuciones de intensidad diferenciadas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766044877064/562547f8-2b4b-45c8-822e-e0620480891f.png" alt class="image--center mx-auto" /></p>
<p>Los métodos de umbralización automática buscan explotar esta característica para determinar el umbral de manera simple y rapida. Uno de los más utilizados es el algoritmo de Otsu, que selecciona el umbral óptimo maximizando la separabilidad estadística entre las dos clases de píxeles.</p>
<h2 id="heading-como-funciona">¿Cómo funciona?</h2>
<p>El algoritmo de Otsu determina automáticamente un umbral global analizando la distribución de intensidades de la imagen. Su objetivo es encontrar el valor de umbral que separa los píxeles en dos clases de la forma más discriminativa posible, basándose únicamente en la información estadística del histograma.</p>
<p><strong>1. Cálculo del histograma</strong><br />El proceso comienza calculando el histograma de la imagen en escala de grises. Este histograma representa la frecuencia de cada nivel de intensidad y constituye la base para evaluar todos los posibles valores de umbral.</p>
<p><strong>2. Evaluación de umbrales candidatos</strong><br />Cada posible nivel de intensidad se considera como un umbral candidato. Para un umbral dado, la imagen se divide en dos clases: una correspondiente a los píxeles con intensidades menores o iguales al umbral y otra con intensidades mayores.</p>
<p><strong>3. Cálculo de estadísticas por clase</strong><br />Para cada una de las dos clases se calculan su peso (proporción de píxeles) y su media de intensidad. Estas magnitudes describen cómo se distribuyen los valores de gris en cada grupo.</p>
<p><strong>4. Varianza entre clases</strong><br />A partir de los pesos y las medias de ambas clases, se calcula la varianza entre clases, que mide qué tan separadas están estadísticamente. Cuanto mayor es esta varianza, mejor es la separación entre fondo y objeto para ese umbral.</p>
<p><strong>5. Selección del umbral óptimo</strong><br />El algoritmo recorre todos los umbrales posibles y selecciona aquel que maximiza la varianza entre clases. Este valor corresponde al umbral óptimo, ya que proporciona la mejor separabilidad estadística entre las dos regiones de la imagen.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">OtsuThreshold</span>(<span class="hljs-params">I</span>):</span>
    <span class="hljs-string">"""Implementación del algoritmo de Otsu para encontrar el umbral óptimo."""</span>
    <span class="hljs-comment"># 1. Calcular el histograma</span>
    hist = CalHistogram(I)
    total_pixels = I.size

    <span class="hljs-comment"># Inicialización de las medias m0 y m1</span>
    m0 = <span class="hljs-number">0</span>
    m1 = np.sum(np.arange(<span class="hljs-number">256</span>) * hist)
    w0 = <span class="hljs-number">0</span>
    w1 = np.sum(hist)

    <span class="hljs-comment"># 2. Calcular el umbral óptimo</span>
    max_between_class_variance = <span class="hljs-number">0</span>
    optimal_threshold = <span class="hljs-number">0</span>

    <span class="hljs-comment"># Calcular la varianza intra-clase para cada umbral</span>
    <span class="hljs-keyword">for</span> t <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, <span class="hljs-number">256</span>):  <span class="hljs-comment"># Iterar por todos los posibles umbrales (1 a 255)</span>
        w0 += hist[t - <span class="hljs-number">1</span>]  <span class="hljs-comment"># Proporción de píxeles en el grupo 0 (fondo)</span>
        w1 -= hist[t - <span class="hljs-number">1</span>]  <span class="hljs-comment"># Proporción de píxeles en el grupo 1 (objeto)</span>

        <span class="hljs-comment"># Calcular las medias de los grupos</span>
        m0 += (t - <span class="hljs-number">1</span>) * hist[t - <span class="hljs-number">1</span>]
        m1 -= (t - <span class="hljs-number">1</span>) * hist[t - <span class="hljs-number">1</span>]

        <span class="hljs-comment"># Si no hay píxeles en el fondo o en el objeto, continuar</span>
        <span class="hljs-keyword">if</span> w0 == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> w1 == <span class="hljs-number">0</span>:
            <span class="hljs-keyword">continue</span>

        <span class="hljs-comment"># Calcular la varianza entre clases</span>
        mean_diff = m0 / w0 - m1 / w1
        between_class_variance = w0 * w1 * mean_diff ** <span class="hljs-number">2</span>

        <span class="hljs-comment"># Comparar la varianza entre clases</span>
        <span class="hljs-keyword">if</span> between_class_variance &gt; max_between_class_variance:
            max_between_class_variance = between_class_variance
            optimal_threshold = t

    <span class="hljs-keyword">return</span> optimal_threshold
</code></pre>
<h2 id="heading-resultados">Resultados</h2>
<p>Una vez obtenido el umbral óptimo con el algoritmo de Otsu, el siguiente paso consiste en aplicar la binarización de la imagen utilizando ese valor:</p>
<pre><code class="lang-python">thr_ots = lip.OtsuThreshold(img) 
_, binary_img = cv.threshold(img, thr_ots, <span class="hljs-number">255</span>, cv.THRESH_BINARY)
</code></pre>
<p>En esta línea, <code>thr_ots</code> corresponde al umbral calculado automáticamente, y <code>binary_img</code> es la imagen binaria resultante, en la que los píxeles del objeto se asignan al valor máximo (255) y los del fondo a cero.</p>
<p>Este procedimiento es un método <strong>sencillo y eficiente de umbralización</strong>, que funciona particularmente bien cuando el fondo es claramente distinguible del objeto. La facilidad de cálculo y su efectividad en imágenes con histogramas bimodales lo convierten en una herramienta práctica para segmentación rápida y confiable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766044896641/65c1324f-d59a-43fd-b245-67983ce201d8.png" alt class="image--center mx-auto" /></p>
<p>El algoritmo de Otsu es un método <strong>robusto y eficiente</strong> para la umbralización de imágenes en escala de grises, especialmente cuando el fondo y el objeto están claramente diferenciados y el histograma presenta una distribución bimodal. Su simplicidad y capacidad para calcular automáticamente un umbral óptimo lo hacen ideal para tareas de segmentación rápida sin necesidad de calculos manuales.</p>
<h2 id="heading-limitaciones">Limitaciones</h2>
<p>Sin embargo, esta técnica presenta limitaciones importantes. En imágenes más complejas, donde el fondo y el objeto exhiben intensidades similares, o donde intervienen sombras, gradientes de iluminación o ruido, la segmentación mediante un único umbral global suele resultar ineficaz. Esto se debe a que dicho enfoque no considera variaciones locales de intensidad, lo que puede conducir a errores en la separación de regiones.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766046719872/8cca63bf-253c-4c4f-a82d-de6b5add6677.png" alt class="image--center mx-auto" /></p>
<p>Un ejemplo de esta situación se observa en la imagen de las monedas: aunque el fondo es visualmente distinguible de los objetos, el histograma de intensidades muestra una fuerte concentración en valores superiores a 200.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766046759571/6b882d80-fb76-4307-88d4-84c3d1374eb1.png" alt class="image--center mx-auto" /></p>
<p>Esto ocurre debido a la predominancia del fondo blanco y de los tonos grises claros presentes en la imagen, lo que provoca que el método de Otsu no logre una separación adecuada entre fondo y objetos.</p>
<p>Una solución simple consiste en eliminar explícitamente el rango de intensidades correspondiente al fondo. Dado que el fondo se concentra en valores altos de intensidad, se puede aplicar una umbralización por rango para suprimir dichos píxeles.</p>
<pre><code class="lang-c">img_rg, mask_rg = lip.RemoveIntensityRange(img, low=<span class="hljs-number">245</span>, high=<span class="hljs-number">255</span>, fill=<span class="hljs-number">0</span>)
</code></pre>
<p>En este caso, se descartan las intensidades comprendidas entre 0 y 245 , asignándoles un valor 255, lo que permite reducir la influencia del fondo y resaltar las regiones de interés antes de aplicar técnicas de segmentación posteriores.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766047337092/7fd830f0-23f0-4be8-9648-14e19ef4bf3a.png" alt class="image--center mx-auto" /></p>
<p>La umbralización y, en particular, el método de Otsu, constituyen herramientas simples y eficientes para la segmentación de imágenes en escala de grises cuando existe una separación clara entre fondo y objeto. No obstante, su carácter global limita su desempeño en escenas más complejas, donde la distribución de intensidades está dominada por el fondo o presenta variaciones locales significativas. En estos casos, resulta necesario complementar la umbralización automática con estrategias adicionales, como la supresión de rangos de intensidad o el uso de espacios de color más adecuados, con el fin de mejorar la separación de las regiones de interés.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[C++ Multithreading desde cero — Parte 3]]></title><description><![CDATA[La base del trabajo con hilos ya está establecida: crear un hilo, controlar su finalización y mantener su ejecución dentro de un marco seguro. A partir de aquí, la concurrencia deja de ser solo una cuestión de lanzar tareas en paralelo y empieza a de...]]></description><link>https://codigoenllamas.com/c-multithreading-desde-cero-pt-3</link><guid isPermaLink="true">https://codigoenllamas.com/c-multithreading-desde-cero-pt-3</guid><category><![CDATA[C++]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sat, 15 Nov 2025 03:06:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/pitSNmqUfio/upload/b47ca842b7edff6986e1c52ffe78029d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La base del trabajo con hilos ya está establecida: crear un hilo, controlar su finalización y mantener su ejecución dentro de un marco seguro. A partir de aquí, la concurrencia deja de ser solo una cuestión de lanzar tareas en paralelo y empieza a depender de cómo se comunican esas tareas, cómo comparten datos y cómo se transfiere la responsabilidad sobre cada hilo.</p>
<p>Esta sección se centra en esos aspectos prácticos. Se examina el paso de argumentos, la propiedad de los hilos y la organización de múltiples hilos en ejecución. Cada uno de estos elementos define la manera en que un programa concurrente distribuye su trabajo y aprovecha realmente los recursos del sistema.</p>
<h2 id="heading-pasar-argumentos-a-una-funcion-de-hilo">Pasar argumentos a una función de hilo</h2>
<p>Cuando creamos un hilo en C++ con <code>std::thread</code>, además de especificar la función que ejecutará, también podemos pasarle argumentos. Esto se realiza directamente en el constructor del hilo, como si estuviéramos llamando a la función:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-keyword">int</span> i, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span> <span class="hljs-keyword">const</span>&amp; s)</span></span>;

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(f, <span class="hljs-number">3</span>, <span class="hljs-string">"hello"</span>)</span></span>;
    t.join();
}
</code></pre>
<p>En este caso, el hilo ejecutará la llamada <code>f(3, "hello")</code>. Sin embargo, aunque parezca una llamada directa, internamente <strong>el constructor de</strong> <code>std::thread</code> copia los argumentos a un almacenamiento interno, y luego los pasa a la función <strong>como valores temporales (rvalues)</strong> dentro del nuevo hilo de ejecución.</p>
<p>Esto tiene varias consecuencias importantes.</p>
<h3 id="heading-copias-internas-y-conversiones-tardias">Copias internas y conversiones tardías</h3>
<p>Los argumentos se copian tal cual son proporcionados, <strong>antes</strong> de que ocurra cualquier conversión de tipo esperada por la función.<br />Por ejemplo, en el siguiente código:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-keyword">int</span> i, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span> <span class="hljs-keyword">const</span>&amp; s)</span></span>;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">oops</span><span class="hljs-params">(<span class="hljs-keyword">int</span> some_param)</span> </span>{
    <span class="hljs-keyword">char</span> buffer[<span class="hljs-number">1024</span>];
    <span class="hljs-built_in">sprintf</span>(buffer, <span class="hljs-string">"%i"</span>, some_param);
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(f, <span class="hljs-number">3</span>, buffer)</span></span>;  <span class="hljs-comment">// &lt;- Peligroso</span>
    t.detach();
}
</code></pre>
<p>Aquí <code>buffer</code> es un arreglo local, y lo que realmente se pasa al hilo es un puntero (<code>char*</code>). El constructor de <code>std::thread</code> copia ese puntero <strong>sin realizar la conversión a</strong> <code>std::string</code>, porque esa conversión ocurre más tarde, cuando el hilo comienza su ejecución.<br />El problema es que, para cuando el nuevo hilo intenta hacer la conversión, <code>buffer</code> podría haber dejado de existir, produciendo <strong>comportamiento indefinido</strong>.</p>
<p>La forma correcta es convertir explícitamente a <code>std::string</code> <strong>antes</strong> de pasar el argumento:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">not_oops</span><span class="hljs-params">(<span class="hljs-keyword">int</span> some_param)</span> </span>{
    <span class="hljs-keyword">char</span> buffer[<span class="hljs-number">1024</span>];
    <span class="hljs-built_in">sprintf</span>(buffer, <span class="hljs-string">"%i"</span>, some_param);
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(f, <span class="hljs-number">3</span>, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>(buffer))</span></span>;  <span class="hljs-comment">// &lt;- Correcto</span>
    t.detach();
}
</code></pre>
<p>En este caso, la conversión a <code>std::string</code> ocurre en el hilo principal, de modo que lo que se copia internamente es un objeto completamente válido e independiente.</p>
<h3 id="heading-paso-por-referencia-stdref-y-stdcref">Paso por referencia: <code>std::ref</code> y <code>std::cref</code></h3>
<p>De manera predeterminada, <code>std::thread</code> <strong>copia todos los argumentos</strong>, incluso si la función espera una referencia.<br />Esto significa que el siguiente código <strong>no compilará</strong>:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">update_data_for_widget</span><span class="hljs-params">(widget_id w, widget_data&amp; data)</span></span>;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">oops_again</span><span class="hljs-params">(widget_id w)</span> </span>{
    widget_data data;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(update_data_for_widget, w, data)</span></span>;  <span class="hljs-comment">// &lt;- Error</span>
    t.join();
}
</code></pre>
<p>Aquí, <code>update_data_for_widget</code> espera una referencia, pero <code>std::thread</code> intenta pasar una copia de <code>data</code> como si fuera un rvalue, lo cual no es válido para una referencia no constante.</p>
<p>Para indicar explícitamente que queremos pasar una <strong>referencia</strong>, debemos envolver el argumento con <code>std::ref</code> (o <code>std::cref</code> si es una referencia constante):</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(update_data_for_widget, w, <span class="hljs-built_in">std</span>::ref(data))</span></span>;
</code></pre>
<p>Ahora el hilo recibirá una referencia real a <code>data</code>, y la función podrá modificarla correctamente.</p>
<h3 id="heading-paso-de-objetos-no-copiables-uso-de-stdmove">Paso de objetos no copiables (uso de <code>std::move</code>)</h3>
<p>Existen tipos que no pueden copiarse, como <code>std::unique_ptr</code>, pero que sí pueden <strong>moverse</strong>. En estos casos, es necesario usar <code>std::move</code> para transferir la propiedad del objeto al hilo:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">process_big_object</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::<span class="hljs-built_in">unique_ptr</span>&lt;big_object&gt; ptr)</span></span>;

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function"><span class="hljs-built_in">std</span>::<span class="hljs-built_in">unique_ptr</span>&lt;big_object&gt; <span class="hljs-title">p</span><span class="hljs-params">(<span class="hljs-keyword">new</span> big_object)</span></span>;
    p-&gt;prepare_data(<span class="hljs-number">42</span>);

    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(process_big_object, <span class="hljs-built_in">std</span>::move(p))</span></span>;
    t.join();
}
</code></pre>
<p>Aquí, la propiedad del puntero se transfiere al hilo, y el objeto <code>p</code> en el hilo principal queda vacío. Este enfoque es muy útil cuando se desea pasar recursos dinámicos de forma segura y eficiente.</p>
<h3 id="heading-funciones-miembro-y-lambdas">Funciones miembro y lambdas</h3>
<p><code>std::thread</code> también puede ejecutar funciones miembro de una clase. En ese caso, el primer argumento debe ser el <strong>puntero al objeto</strong> sobre el cual se invocará el método:</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">X</span> {</span>
<span class="hljs-keyword">public</span>:
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">do_lengthy_work</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Ejecutando trabajo largo...\n"</span>;
    }
};

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    X x;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(&amp;X::do_lengthy_work, &amp;x)</span></span>;
    t.join();
}
</code></pre>
<p>Esto ejecutará <a target="_blank" href="http://x.do"><code>x.do</code></a><code>_lengthy_work()</code> en el nuevo hilo.</p>
<p>De manera análoga, es posible usar <strong>lambdas</strong> para encapsular tanto la función como los argumentos:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">int</span> value = <span class="hljs-number">10</span>;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">([value]() {
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Valor: "</span> &lt;&lt; value &lt;&lt; <span class="hljs-string">'\n'</span>;
    })</span></span>;
    t.join();
}
</code></pre>
<p>Las lambdas capturan los valores según sus reglas (<code>[value]</code>, <code>[&amp;value]</code>, <code>[=]</code>, <code>[&amp;]</code>), lo que permite controlar explícitamente si se copian o se referencian los datos.</p>
<h3 id="heading-consideraciones-sobre-el-tiempo-de-vida">Consideraciones sobre el tiempo de vida</h3>
<p>Es fundamental garantizar que los objetos referenciados por el hilo <strong>permanezcan válidos</strong> mientras el hilo los use.<br />Esto implica:</p>
<ul>
<li><p>Si pasas referencias (con <code>std::ref</code>), asegúrate de que el objeto exista al menos hasta que el hilo finalice.</p>
</li>
<li><p>Si pasas punteros, evita apuntar a variables automáticas que puedan salir de alcance.</p>
</li>
<li><p>Si usas <code>detach()</code>, ten cuidado: el hilo puede seguir ejecutándose después de que el contexto local haya terminado.</p>
</li>
</ul>
<p>En general, es más seguro pasar copias o usar <code>std::shared_ptr</code> si el objeto necesita sobrevivir más allá del alcance del hilo que lo creó.</p>
<h2 id="heading-transferir-la-propiedad-de-un-hilo">Transferir la propiedad de un hilo</h2>
<p>Cada objeto <code>std::thread</code> <strong>posee</strong> un hilo de ejecución: es el responsable de gestionar su ciclo de vida, ya sea esperando su finalización mediante <code>join()</code> o liberándolo con <code>detach()</code>.</p>
<p>Esta relación exclusiva implica que <strong>solo un objeto</strong> <code>std::thread</code> puede poseer un hilo determinado a la vez.</p>
<p>A diferencia de otros tipos copiables, los hilos <strong>no pueden duplicarse</strong>, porque esto implicaría que dos objetos intentaran controlar el mismo recurso del sistema operativo. Sin embargo, sí pueden <strong>transferirse</strong> entre objetos mediante <em>move semantics</em> —el mismo mecanismo que utilizan clases como <code>std::unique_ptr</code> para transferir propiedad de recursos únicos.</p>
<h3 id="heading-propiedad-y-movimiento-de-hilos">Propiedad y movimiento de hilos</h3>
<p>El siguiente ejemplo muestra cómo puede moverse la propiedad de un hilo entre distintos objetos <code>std::thread</code>:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">some_function</span><span class="hljs-params">()</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">some_other_function</span><span class="hljs-params">()</span></span>;

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t1</span><span class="hljs-params">(some_function)</span></span>;        <span class="hljs-comment">// t1 posee el hilo</span>
    <span class="hljs-built_in">std</span>::thread t2 = <span class="hljs-built_in">std</span>::move(t1);       <span class="hljs-comment">// t2 toma la propiedad</span>
    t1 = <span class="hljs-built_in">std</span>::thread(some_other_function); <span class="hljs-comment">// t1 crea y posee un nuevo hilo</span>
    <span class="hljs-built_in">std</span>::thread t3;                       <span class="hljs-comment">// hilo vacío</span>
    t3 = <span class="hljs-built_in">std</span>::move(t2);                   <span class="hljs-comment">// t3 toma el hilo original</span>
    t1 = <span class="hljs-built_in">std</span>::move(t3);                   <span class="hljs-comment">// ¡Error! std::terminate()</span>
}
</code></pre>
<p>El flujo de propiedad es el siguiente:</p>
<ol>
<li><p><code>t1</code> crea un hilo que ejecuta <code>some_function</code>.</p>
</li>
<li><p><code>t2 = std::move(t1)</code> transfiere la propiedad del hilo de <code>t1</code> a <code>t2</code>.<br /> Después de esto, <code>t1</code> queda sin hilo asociado.</p>
</li>
<li><p><code>t1 = std::thread(some_other_function)</code> inicia un nuevo hilo y se convierte en su dueño.</p>
</li>
<li><p><code>t3</code> toma el hilo de <code>t2</code> con <code>std::move(t2)</code>.</p>
</li>
<li><p>Finalmente, la reasignación <code>t1 = std::move(t3)</code> provoca la terminación del programa, ya que <code>t1</code> aún era dueño de un hilo no finalizado.</p>
</li>
</ol>
<p>Este último punto es importante: <strong>asignar un nuevo hilo a un objeto que ya posee uno activo invoca</strong> <code>std::terminate()</code>.<br />La norma impone esto para mantener la consistencia con el destructor de <code>std::thread</code>, que también requiere que el hilo se haya sincronizado o desacoplado antes de destruir el objeto.</p>
<h3 id="heading-transferencia-de-hilos-entre-funciones">Transferencia de hilos entre funciones</h3>
<p>El soporte de movimiento en <code>std::thread</code> permite <strong>devolver o recibir hilos por valor</strong> en funciones, algo muy útil para diseñar interfaces limpias y seguras.</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">create_thread</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">std</span>::thread([] {
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Ejecutando hilo...\n"</span>;
    });
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">consume_thread</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::thread t)</span> </span>{
    <span class="hljs-keyword">if</span> (t.joinable()) t.join();
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-built_in">std</span>::thread t = create_thread();   <span class="hljs-comment">// El hilo se transfiere por retorno</span>
    consume_thread(<span class="hljs-built_in">std</span>::move(t));      <span class="hljs-comment">// Se pasa la propiedad a la función</span>
}
</code></pre>
<p>En este ejemplo:</p>
<ul>
<li><p><code>create_thread()</code> devuelve un <code>std::thread</code> movido, que transfiere su propiedad al llamador.</p>
</li>
<li><p><code>consume_thread()</code> acepta el hilo <strong>por valor</strong>, y puede unirse a él sin preocuparse de interferir con otros dueños.</p>
</li>
</ul>
<p>La transferencia explícita mediante <code>std::move()</code> evita copias ilegales y garantiza que solo exista un dueño válido del hilo en cada momento.</p>
<h3 id="heading-clases-auxiliares-scopedthread-y-joiningthread">Clases auxiliares: <code>scoped_thread</code> y <code>joining_thread</code></h3>
<p>A menudo es conveniente encapsular la gestión del hilo dentro de un objeto que <strong>asegure la sincronización automática al salir del ámbito</strong>. Una forma sencilla de hacerlo es con una clase llamada <code>scoped_thread</code>, que toma la propiedad de un hilo en su constructor y lo une en su destructor:</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">scoped_thread</span> {</span>
    <span class="hljs-built_in">std</span>::thread t;
<span class="hljs-keyword">public</span>:
    <span class="hljs-function"><span class="hljs-keyword">explicit</span> <span class="hljs-title">scoped_thread</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::thread t_)</span> : <span class="hljs-title">t</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::move(t_))</span> </span>{
        <span class="hljs-keyword">if</span> (!t.joinable())
            <span class="hljs-keyword">throw</span> <span class="hljs-built_in">std</span>::logic_error(<span class="hljs-string">"No thread"</span>);
    }
    ~scoped_thread() {
        t.join();
    }

    scoped_thread(<span class="hljs-keyword">const</span> scoped_thread&amp;) = <span class="hljs-keyword">delete</span>;
    scoped_thread&amp; <span class="hljs-keyword">operator</span>=(<span class="hljs-keyword">const</span> scoped_thread&amp;) = <span class="hljs-keyword">delete</span>;
};
</code></pre>
<p>Su uso es simple y seguro:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">task</span><span class="hljs-params">()</span></span>;

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function">scoped_thread <span class="hljs-title">worker</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::thread(task))</span></span>;  <span class="hljs-comment">// El hilo se une automáticamente al salir</span>
}
</code></pre>
<p>Gracias a este patrón, se evita olvidar la llamada a <code>join()</code>, reduciendo el riesgo de errores y cierres abruptos del programa.</p>
<p>Una variante más flexible es la clase <code>joining_thread</code>, que se comporta como un <code>std::thread</code> estándar pero <strong>se une automáticamente en su destructor</strong>. Esto permite usarla de forma más natural en estructuras dinámicas o funciones que retornan hilos</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">joining_thread</span> {</span>
    <span class="hljs-built_in">std</span>::thread t;
<span class="hljs-keyword">public</span>:
    joining_thread() <span class="hljs-keyword">noexcept</span> = <span class="hljs-keyword">default</span>;

    <span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> Callable, <span class="hljs-keyword">typename</span>... Args&gt;
    <span class="hljs-function"><span class="hljs-keyword">explicit</span> <span class="hljs-title">joining_thread</span><span class="hljs-params">(Callable&amp;&amp; f, Args&amp;&amp;... args)</span>
        : <span class="hljs-title">t</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::forward&lt;Callable&gt;(f), <span class="hljs-built_in">std</span>::forward&lt;Args&gt;(args)...)</span> </span>{}

    joining_thread(joining_thread&amp;&amp; other) <span class="hljs-keyword">noexcept</span>
        : t(<span class="hljs-built_in">std</span>::move(other.t)) {}

    joining_thread&amp; <span class="hljs-keyword">operator</span>=(joining_thread&amp;&amp; other) <span class="hljs-keyword">noexcept</span> {
        <span class="hljs-keyword">if</span> (joinable()) join();
        t = <span class="hljs-built_in">std</span>::move(other.t);
        <span class="hljs-keyword">return</span> *<span class="hljs-keyword">this</span>;
    }

    ~joining_thread() <span class="hljs-keyword">noexcept</span> {
        <span class="hljs-keyword">if</span> (joinable()) join();
    }

    <span class="hljs-function"><span class="hljs-keyword">bool</span> <span class="hljs-title">joinable</span><span class="hljs-params">()</span> <span class="hljs-keyword">const</span> <span class="hljs-keyword">noexcept</span> </span>{ <span class="hljs-keyword">return</span> t.joinable(); }
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">join</span><span class="hljs-params">()</span> </span>{ t.join(); }
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">detach</span><span class="hljs-params">()</span> </span>{ t.detach(); }
};
</code></pre>
<p>De esta forma, <code>joining_thread</code> combina la seguridad de <code>scoped_thread</code> con la flexibilidad de <code>std::thread</code>.</p>
<h3 id="heading-contenedores-de-hilos">Contenedores de hilos</h3>
<p>El soporte de movimiento también permite almacenar hilos en <strong>contenedores dinámicos</strong> como <code>std::vector</code>.</p>
<p>Esto resulta útil para lanzar múltiples tareas y luego sincronizarlas en grupo:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">do_work</span><span class="hljs-params">(<span class="hljs-keyword">unsigned</span> id)</span> </span>{
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Trabajando en hilo "</span> &lt;&lt; id &lt;&lt; <span class="hljs-string">"\n"</span>;
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">vector</span>&lt;<span class="hljs-built_in">std</span>::thread&gt; threads;

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">unsigned</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">8</span>; ++i)
        threads.emplace_back(do_work, i);

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; t : threads)
        <span class="hljs-keyword">if</span> (t.joinable()) t.join();
}
</code></pre>
<p>Cada hilo se crea y se almacena dentro del vector mediante <strong>movimiento implícito</strong>, y luego todos son sincronizados al final del programa.<br />Esta técnica permite administrar un número variable de hilos sin declarar múltiples variables, facilitando la creación de <em>thread pools</em> o sistemas de tareas paralelas.</p>
<hr />
<h2 id="heading-elegir-el-numero-de-hilos-en-tiempo-de-ejecucion"><strong>Elegir el número de hilos en tiempo de ejecución</strong></h2>
<p>Determinar cuántos hilos crear en un programa concurrente es una de las decisiones más importantes al diseñar una aplicación paralela eficiente. Crear demasiados hilos puede saturar el sistema operativo con cambios de contexto innecesarios; crear muy pocos desaprovecha el potencial de paralelismo del hardware.</p>
<p>Por ello, el número de hilos óptimo debe elegirse <strong>en tiempo de ejecución</strong>, tomando en cuenta los recursos físicos disponibles y el tipo de carga de trabajo.</p>
<h4 id="heading-consultar-el-hardware-stdthreadhardwareconcurrency">Consultar el hardware: <code>std::thread::hardware_concurrency()</code></h4>
<p>La biblioteca estándar de C++ proporciona una función que sirve como guía inicial para decidir cuántos hilos pueden ejecutarse realmente de forma concurrente:</p>
<pre><code class="lang-cpp"><span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span> n = <span class="hljs-built_in">std</span>::thread::hardware_concurrency();
</code></pre>
<p>Esta función devuelve el número de <em>hardware threads</em> (generalmente, el número de núcleos o núcleos lógicos) disponibles para el programa.<br />Por ejemplo, en un CPU con cuatro núcleos físicos y <em>hyper-threading</em>, el valor devuelto podría ser <code>8</code>.</p>
<p>Sin embargo, es importante entender que este valor es solo <strong>una sugerencia</strong>.<br />La implementación puede devolver <code>0</code> si la información no está disponible, por lo que es buena práctica definir un valor por defecto:</p>
<pre><code class="lang-cpp"><span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span> num_threads = <span class="hljs-built_in">std</span>::thread::hardware_concurrency();
<span class="hljs-keyword">if</span> (num_threads == <span class="hljs-number">0</span>)
    num_threads = <span class="hljs-number">2</span>;  <span class="hljs-comment">// valor por defecto razonable</span>
</code></pre>
<p>Este número sirve como punto de partida para asignar trabajo entre hilos, pero no siempre representa la cantidad ideal: en tareas muy ligeras o con fuerte interacción entre hilos, crear un hilo por núcleo puede no ser lo más eficiente.</p>
<h3 id="heading-estrategia-basica-dividir-la-carga-de-trabajo">Estrategia básica: dividir la carga de trabajo</h3>
<p>Una forma sencilla de aplicar este principio es dividir un conjunto de datos entre varios hilos.</p>
<p>El ejemplo siguiente implementa una versión paralela del algoritmo <code>std::accumulate</code>, que suma los elementos de un rango, dividiéndolos entre varios hilos según la cantidad de núcleos disponibles.</p>
<p><strong>accumulate_CD_04</strong></p>
<pre><code class="lang-cpp"><span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> Iterator, <span class="hljs-keyword">typename</span> T&gt;
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">accumulate_block</span> {</span>
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">operator</span><span class="hljs-params">()</span><span class="hljs-params">(Iterator first, Iterator last, T&amp; result)</span> </span>{
        result = <span class="hljs-built_in">std</span>::accumulate(first, last, result);
    }
};

<span class="hljs-keyword">template</span> &lt;<span class="hljs-keyword">typename</span> Iterator, <span class="hljs-keyword">typename</span> T&gt;
<span class="hljs-function">T <span class="hljs-title">parallel_accumulate</span><span class="hljs-params">(Iterator first, Iterator last, T init)</span> </span>{
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> <span class="hljs-keyword">const</span> length = <span class="hljs-built_in">std</span>::distance(first, last);
    <span class="hljs-keyword">if</span> (!length)
        <span class="hljs-keyword">return</span> init;

    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> <span class="hljs-keyword">const</span> min_per_thread = <span class="hljs-number">25</span>;
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> <span class="hljs-keyword">const</span> max_threads =
        (length + min_per_thread - <span class="hljs-number">1</span>) / min_per_thread;

    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> <span class="hljs-keyword">const</span> hardware_threads =
        <span class="hljs-built_in">std</span>::thread::hardware_concurrency();

    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> <span class="hljs-keyword">const</span> num_threads =
        <span class="hljs-built_in">std</span>::min(hardware_threads != <span class="hljs-number">0</span> ? hardware_threads : <span class="hljs-number">2</span>, max_threads);

    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> <span class="hljs-keyword">const</span> block_size = length / num_threads;

    <span class="hljs-function"><span class="hljs-built_in">std</span>::<span class="hljs-built_in">vector</span>&lt;T&gt; <span class="hljs-title">results</span><span class="hljs-params">(num_threads)</span></span>;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::<span class="hljs-built_in">vector</span>&lt;<span class="hljs-built_in">std</span>::thread&gt; <span class="hljs-title">threads</span><span class="hljs-params">(num_threads - <span class="hljs-number">1</span>)</span></span>;

    Iterator block_start = first;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> i = <span class="hljs-number">0</span>; i &lt; (num_threads - <span class="hljs-number">1</span>); ++i) {
        Iterator block_end = block_start;
        <span class="hljs-built_in">std</span>::advance(block_end, block_size);
        threads[i] = <span class="hljs-built_in">std</span>::thread(
            accumulate_block&lt;Iterator, T&gt;(),
            block_start, block_end, <span class="hljs-built_in">std</span>::ref(results[i])
        );
        block_start = block_end;
    }

    accumulate_block&lt;Iterator, T&gt;()(
        block_start, last, results[num_threads - <span class="hljs-number">1</span>]
    );

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; t : threads)
        t.join();

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">std</span>::accumulate(results.begin(), results.end(), init);
}
</code></pre>
<hr />
<h4 id="heading-analisis-del-algoritmo">Análisis del algoritmo</h4>
<ol>
<li><p><strong>Verificación del tamaño de entrada</strong><br /> Si el rango está vacío, simplemente se devuelve el valor inicial <code>init</code>.<br /> Esto evita lanzar hilos innecesarios cuando no hay trabajo que hacer.</p>
</li>
<li><p><strong>Límite mínimo por hilo</strong><br /> Se define un número mínimo de elementos por hilo (<code>min_per_thread</code>), con el fin de evitar la sobrecarga que supondría crear muchos hilos para tareas pequeñas.</p>
</li>
<li><p><strong>Cálculo del número máximo de hilos</strong><br /> A partir del tamaño del rango y del mínimo por hilo, se obtiene el número máximo de hilos que tendría sentido crear.</p>
</li>
<li><p><strong>Número real de hilos a usar</strong><br /> Se elige el menor valor entre el número máximo calculado y el número de hilos de hardware disponibles. Además, si la consulta al hardware falla, se usa un valor por defecto (en este caso, 2).</p>
</li>
<li><p><strong>División del trabajo</strong><br /> El tamaño del bloque asignado a cada hilo se calcula dividiendo la longitud total entre el número de hilos (<code>block_size</code>).</p>
</li>
<li><p><strong>Creación y ejecución de hilos</strong><br /> Se lanzan <code>num_threads - 1</code> hilos, cada uno procesando una parte del rango.<br /> El hilo principal procesa el último bloque para evitar crear un hilo adicional.</p>
</li>
<li><p><strong>Sincronización final</strong><br /> Todos los hilos creados se sincronizan mediante <code>join()</code>, y luego se combinan los resultados parciales con un último <code>std::accumulate</code>.</p>
</li>
</ol>
<hr />
<h3 id="heading-consideraciones-sobre-rendimiento">Consideraciones sobre rendimiento</h3>
<p>El objetivo de este enfoque es <strong>maximizar la utilización del hardware</strong> evitando el fenómeno de <em>oversubscription</em>, que ocurre cuando hay más hilos activos que núcleos disponibles. El exceso de hilos genera cambios de contexto constantes, lo que degrada el rendimiento en lugar de mejorarlo.</p>
<p>Algunos puntos clave:</p>
<ul>
<li><p><strong>Sobrecarga de creación:</strong> lanzar un hilo tiene un costo no trivial; conviene hacerlo solo cuando el trabajo lo justifique.</p>
</li>
<li><p><strong>Equilibrio de carga:</strong> si los hilos procesan bloques de distinto tamaño o complejidad, algunos núcleos quedarán inactivos antes que otros.</p>
</li>
<li><p><strong>Afinidad de CPU:</strong> en casos de alta demanda puede ser beneficioso fijar ciertos hilos a núcleos específicos, aunque esto se gestiona a nivel del sistema operativo.</p>
</li>
<li><p><strong>Reutilización de hilos:</strong> en tareas repetitivas conviene emplear un <em>thread pool</em>, que mantiene un conjunto fijo de hilos reutilizables en lugar de crearlos y destruirlos continuamente.</p>
</li>
</ul>
<hr />
<h4 id="heading-ejemplo-practico-elegir-dinamicamente-segun-carga">Ejemplo práctico: elegir dinámicamente según carga</h4>
<p>Una mejora práctica consiste en ajustar el número de hilos <strong>según la carga real</strong> y no solo por hardware. Por ejemplo:</p>
<pre><code class="lang-cpp"><span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span> hardware = <span class="hljs-built_in">std</span>::thread::hardware_concurrency();
<span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span> num_threads = <span class="hljs-built_in">std</span>::min(hardware != <span class="hljs-number">0</span> ? hardware : <span class="hljs-number">2</span>,
                                    total_tasks / min_work_per_thread);
num_threads = <span class="hljs-built_in">std</span>::max(<span class="hljs-number">1u</span>, num_threads);  <span class="hljs-comment">// garantizar al menos un hilo</span>
</code></pre>
<p>Esta estrategia evita crear más hilos de los necesarios cuando la tarea es pequeña, pero aprovecha al máximo los núcleos disponibles cuando hay suficiente trabajo.</p>
<hr />
<h2 id="heading-identificar-hilos">Identificar hilos</h2>
<p>Cuando se trabaja con múltiples hilos, puede ser necesario distinguir cuál de ellos está ejecutando una determinada parte del código. Por ejemplo, podríamos querer registrar qué hilo está procesando una tarea, asignar recursos según el hilo, o simplemente generar trazas para depurar comportamientos concurrentes.<br />La biblioteca estándar de C++ proporciona un mecanismo seguro y eficiente para realizar esta identificación mediante la clase <code>std::thread::id</code>.</p>
<hr />
<h3 id="heading-obtener-el-identificador-de-un-hilo">Obtener el identificador de un hilo</h3>
<p>Existen dos formas principales de obtener un identificador de tipo <code>std::thread::id</code>:</p>
<ol>
<li><p><strong>Desde un objeto</strong> <code>std::thread</code></p>
<pre><code class="lang-cpp"> <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(f)</span></span>;
 <span class="hljs-built_in">std</span>::thread::id id = t.get_id();
</code></pre>
</li>
</ol>
<p>El método <code>get_id()</code> devuelve el identificador del hilo asociado al objeto.</p>
<p>Si el objeto <code>std::thread</code> <strong>no está asociado</strong> a ningún hilo de ejecución (por ejemplo, porque fue creado sin función o ya se le hizo <code>join()</code> o <code>detach()</code>), la llamada devuelve un identificador por defecto, que representa <em>“ningún hilo”</em>.</p>
<ol start="2">
<li><p><strong>Desde el hilo actual</strong></p>
<pre><code class="lang-cpp"> <span class="hljs-built_in">std</span>::thread::id id = <span class="hljs-built_in">std</span>::this_thread::get_id();
</code></pre>
<p> Esta función devuelve el identificador del hilo que la invoca. Es especialmente útil dentro de funciones que pueden ser ejecutadas por distintos hilos, ya que permite saber cuál de ellos está ejecutando la llamada.</p>
</li>
</ol>
<h4 id="heading-propiedades-del-identificador">Propiedades del identificador</h4>
<p>Los objetos de tipo <code>std::thread::id</code> son <strong>copiables y comparables</strong>. Esto permite usarlos de forma natural para verificar si dos hilos son el mismo:</p>
<pre><code class="lang-cpp"><span class="hljs-keyword">if</span> (t1.get_id() == t2.get_id())
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Ambos objetos representan el mismo hilo\n"</span>;
</code></pre>
<p>Si dos identificadores son iguales, representan el mismo hilo o ambos son “ningún hilo”. Además, la clase proporciona un <strong>orden total</strong>: pueden compararse con <code>&lt;</code>, <code>&gt;</code>, etc., lo que permite usarlos como <strong>claves en contenedores asociativos</strong>, tanto ordenados (<code>std::map</code>) como no ordenados (<code>std::unordered_map</code>), gracias a que existe una especialización de <code>std::hash&lt;std::thread::id&gt;</code>.</p>
<p>Ejemplo:</p>
<pre><code class="lang-cpp"><span class="hljs-built_in">std</span>::<span class="hljs-built_in">unordered_map</span>&lt;<span class="hljs-built_in">std</span>::thread::id, <span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span>&gt; thread_names;
thread_names[<span class="hljs-built_in">std</span>::this_thread::get_id()] = <span class="hljs-string">"Hilo principal"</span>;
</code></pre>
<h3 id="heading-uso-en-registro-y-depuracion">Uso en registro y depuración</h3>
<p>El identificador de hilo resulta especialmente útil para generar trazas de ejecución.</p>
<p>Por ejemplo, en un sistema concurrente de procesamiento de tareas, podríamos imprimir qué hilo está procesando cada bloque de datos:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">process_task</span><span class="hljs-params">(<span class="hljs-keyword">int</span> task_id)</span> </span>{
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Hilo "</span> &lt;&lt; <span class="hljs-built_in">std</span>::this_thread::get_id()
              &lt;&lt; <span class="hljs-string">" procesando tarea "</span> &lt;&lt; task_id &lt;&lt; <span class="hljs-string">'\n'</span>;
}
</code></pre>
<p>Cada ejecución imprimirá un valor distinto de <code>std::thread::id</code>, lo que permite identificar fácilmente qué hilo realizó cada operación. El formato exacto del identificador depende de la implementación, pero la norma garantiza que hilos diferentes producirán salidas distintas**, y que **hilos iguales producirán la misma salida.</p>
<h4 id="heading-ejemplo-distinguir-el-hilo-maestro-de-los-trabajadores">Ejemplo: distinguir el hilo maestro de los trabajadores</h4>
<p>Supongamos que el hilo principal lanza varios hilos para realizar trabajo paralelo, pero necesita ejecutar una tarea especial que solo él debe realizar. En ese caso, puede almacenar su propio identificador antes de lanzar los hilos y luego compararlo dentro del código compartido:</p>
<pre><code class="lang-cpp"><span class="hljs-built_in">std</span>::thread::id master_thread;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">some_core_part_of_algorithm</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">std</span>::this_thread::get_id() == master_thread) {
        do_master_thread_work();   <span class="hljs-comment">// tarea exclusiva del hilo maestro</span>
    }
    do_common_work();              <span class="hljs-comment">// tarea común a todos los hilos</span>
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    master_thread = <span class="hljs-built_in">std</span>::this_thread::get_id();
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">worker1</span><span class="hljs-params">(some_core_part_of_algorithm)</span></span>;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">worker2</span><span class="hljs-params">(some_core_part_of_algorithm)</span></span>;

    some_core_part_of_algorithm(); <span class="hljs-comment">// ejecuta el maestro</span>

    worker1.join();
    worker2.join();
}
</code></pre>
<p>Aquí, todos los hilos ejecutan la misma función, pero solo el hilo maestro realiza la sección especial al comparar su <code>std::thread::id</code> con el almacenado.</p>
<hr />
<h3 id="heading-asociar-datos-a-hilos-mediante-identificadores">Asociar datos a hilos mediante identificadores</h3>
<p>En ocasiones, es útil mantener información específica de cada hilo, como estadísticas o configuraciones locales.<br />Si no se desea usar <em>thread-local storage</em>, puede construirse un contenedor donde la clave sea el identificador del hilo:</p>
<pre><code class="lang-cpp"><span class="hljs-built_in">std</span>::<span class="hljs-built_in">map</span>&lt;<span class="hljs-built_in">std</span>::thread::id, ThreadStats&gt; stats_map;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">log_event</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::<span class="hljs-built_in">string</span> event)</span> </span>{
    stats_map[<span class="hljs-built_in">std</span>::this_thread::get_id()].events.push_back(event);
}
</code></pre>
<p>Este enfoque resulta práctico cuando se requiere que un hilo externo (por ejemplo, un controlador) acceda a información de otros hilos mediante sus identificadores.</p>
<h3 id="heading-consideraciones-sobre-la-reutilizacion-de-ids">Consideraciones sobre la reutilización de IDs</h3>
<p>Aunque los identificadores son <strong>únicos durante la vida activa de un hilo</strong>, los sistemas operativos <strong>pueden reutilizarlos</strong> una vez que un hilo termina y su recurso ha sido liberado. Esto significa que un <code>std::thread::id</code> antiguo no debe conservarse para identificar un hilo después de que este haya finalizado, ya que podría ser asignado a otro hilo nuevo.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>La gestión de argumentos, el control sobre la propiedad de los datos y la organización de varios hilos sirven como base práctica para construir programas concurrentes más claros y manejables. Estos mecanismos ayudan a distribuir el trabajo de forma ordenada, a mantener un seguimiento preciso de cada hilo y a evitar errores típicos relacionados con el tiempo de vida de los datos. A partir de aquí, el siguiente paso será es explorar cómo varios hilos pueden acceder y manipular la misma información sin generar inconsistencias. Ese será el punto de partida de la próxima sección: compartir datos entre hilos.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/multithreading_cpp">https://github.com/Nobody-1321/multithreading_cpp</a></div>
]]></content:encoded></item><item><title><![CDATA[C++ Multithreading desde cero — Parte 2]]></title><description><![CDATA[1. Introducción
Hasta ahora he abordado los fundamentos conceptuales de la concurrencia: qué es, cuándo conviene usarla y por qué es un elemento esencial en los sistemas modernos. En esta sección daremos el siguiente paso, centrando nuestra atención ...]]></description><link>https://codigoenllamas.com/c-multithreading-desde-cero-pt-2</link><guid isPermaLink="true">https://codigoenllamas.com/c-multithreading-desde-cero-pt-2</guid><category><![CDATA[C++]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Wed, 12 Nov 2025 01:39:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/pitSNmqUfio/upload/945180ee4e5a1133017009b3170923b3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-1-introduccion">1. Introducción</h2>
<p>Hasta ahora he abordado los fundamentos conceptuales de la concurrencia: qué es, cuándo conviene usarla y por qué es un elemento esencial en los sistemas modernos. En esta sección daremos el siguiente paso, centrando nuestra atención en la práctica: la gestión de hilos en C++ y su aplicación a través de ejemplos concretos.</p>
<p>Todo programa en C++ comienza con un único hilo: aquel que ejecuta la función <code>main()</code>. Sin embargo, la verdadera potencia del lenguaje aparece cuando aprendemos a lanzar nuevos hilos que ejecutan tareas de manera independiente y concurrente. El punto de partida es la clase <code>std::thread</code>, introducida en C++11. Esta clase encapsula la creación y control de un hilo de ejecución. En su forma más simple, lanzar un hilo consiste en construir un objeto <code>std::thread</code> y especificar qué función se ejecutará en ese nuevo hilo.</p>
<p><strong><em>Hello_Concurrent_World CD</em>_<em>01:</em></strong></p>
<pre><code class="lang-cpp"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;thread&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">hello</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Hello Concurrent World\n"</span>;
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(hello)</span></span>;  <span class="hljs-comment">// Lanza un nuevo hilo</span>
    t.join();              <span class="hljs-comment">// Espera a que el hilo termine</span>
}
</code></pre>
<p>Este pequeño programa crea dos hilos:</p>
<ul>
<li><p>el <strong>hilo principal</strong>, que inicia en <code>main()</code>,</p>
</li>
<li><p>y un <strong>hilo secundario</strong>, que comienza ejecutando la función <code>hello()</code>.</p>
</li>
</ul>
<p>El flujo principal continúa inmediatamente después de lanzar el hilo. Si no esperáramos a que el hilo termine, el programa podría finalizar antes de que el mensaje se muestre. Por eso llamamos a <code>join()</code>, que bloquea la ejecución hasta que el hilo finaliza. Aunque el ejemplo parece trivial, marca un cambio profundo: desde este punto, <strong>el control de flujo deja de ser lineal</strong>. Cada hilo representa un camino de ejecución independiente, y depende de nosotros decidir cómo y cuándo sincronizarlos.</p>
<hr />
<h2 id="heading-2-lanzar-un-hilo">2. Lanzar un hilo</h2>
<p>En C++, crear un hilo siempre se reduce a <strong>construir un objeto</strong> <code>std::thread</code>, pasando como argumento una función o cualquier objeto <em>callable</em>.</p>
<p>Por ejemplo, podríamos usar una función normal:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">do_some_work</span><span class="hljs-params">()</span></span>;
<span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">worker</span><span class="hljs-params">(do_some_work)</span></span>;
</code></pre>
<p>o un objeto que sobrecarga el operador <code>()</code>:</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">background_task</span> {</span>
<span class="hljs-keyword">public</span>:
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">operator</span><span class="hljs-params">()</span><span class="hljs-params">()</span> <span class="hljs-keyword">const</span> </span>{
        do_something();
        do_something_else();
    }
};

background_task task;
<span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">worker</span><span class="hljs-params">(task)</span></span>;
</code></pre>
<p>Cuando se crea el objeto <code>std::thread</code>, su constructor copia la función u objeto <em>callable</em> dentro del contexto del nuevo hilo y comienza su ejecución inmediatamente. Una vez que el hilo inicia, su función de entrada se ejecutará hasta completarse, momento en el cual el hilo finalizará de forma automática.</p>
<h3 id="heading-riesgos-iniciales">Riesgos iniciales</h3>
<p>Lanzar un hilo puede parecer una operación simple, pero implica un cambio fundamental en la gestión de recursos y en el tiempo de vida de los datos. Si el programa termina o un objeto local se destruye antes de que el hilo haya terminado de usarlo, se entra en terreno de <strong>comportamiento indefinido</strong>.</p>
<p>En particular, si no esperas a que el hilo finalice (por ejemplo, al no llamar a <code>join()</code>, debes asegurarte de que los datos a los que accede el hilo sigan siendo válidos hasta que este complete su ejecución.</p>
<p>Este problema no es exclusivo del código concurrente: incluso en un solo hilo es incorrecto acceder a un objeto destruido. Sin embargo, con múltiples hilos, la posibilidad de error se amplifica, porque el momento exacto en que el hilo accede a la memoria depende del planificador del sistema operativo.</p>
<p>Considera el siguiente ejemplo:</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">func</span> {</span>
    <span class="hljs-keyword">int</span>&amp; i;
    func(<span class="hljs-keyword">int</span>&amp; i_) : i(i_) {}

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">operator</span><span class="hljs-params">()</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">unsigned</span> j = <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">1000000</span>; ++j) {
            do_something(i);
        }
    }
};

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">oops</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">int</span> some_local_state = <span class="hljs-number">0</span>;
    <span class="hljs-function">func <span class="hljs-title">my_func</span><span class="hljs-params">(some_local_state)</span></span>;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">my_thread</span><span class="hljs-params">(my_func)</span></span>;
    my_thread.detach();  <span class="hljs-comment">// No esperamos a que termine</span>
}
</code></pre>
<p>Aquí el hilo creado con <code>my_thread</code> continúa ejecutándose incluso después de que la función <code>oops()</code> haya retornado, el objeto <code>some_local_state</code> deja de existir al salir de la función, pero el hilo aún podría estar ejecutando <code>do_something(i)</code>, accediendo a una referencia colgante, esto constituye un acceso a memoria destruida, lo que provoca un comportamiento indefinido: desde fallos silenciosos hasta bloqueos o corrupciones de datos.</p>
<p>La situación es similar a mantener un puntero o referencia a una variable local fuera de su ámbito, pero en programación concurrente el error es más fácil de pasar por alto, porque el hilo podría seguir corriendo en segundo plano sin que sea evidente.</p>
<p>Una manera segura de evitar este tipo de errores es:</p>
<ul>
<li><p>Hacer que la función del hilo sea <strong>autosuficiente</strong>, copiando los datos que necesita en lugar de referenciarlos.</p>
</li>
<li><p>Asegurarse de que el hilo haya completado su ejecución antes de que los recursos que usa sean destruidos, normalmente mediante una llamada a <code>join()</code>.</p>
</li>
</ul>
<hr />
<h2 id="heading-3-estado-joinable-y-ciclo-de-vida-del-hilo">3. Estado <em>joinable</em> y ciclo de vida del hilo</h2>
<p>Cada objeto <code>std::thread</code> mantiene una asociación con un hilo real del sistema operativo. Mientras esa asociación exista, el hilo se considera <strong>joinable</strong>, es decir, puede ser esperado o desacoplado.</p>
<p>Cuando se llama a <code>join()</code>, suceden dos cosas:</p>
<ol>
<li><p>El hilo que hace la llamada se bloquea hasta que el hilo asociado termina.</p>
</li>
<li><p>Los recursos del sistema utilizados por el hilo son liberados, y el objeto <code>std::thread</code> deja de estar asociado a ningún hilo.</p>
</li>
</ol>
<p>Después de esa llamada, el hilo <strong>ya no es joinable</strong>, y cualquier intento posterior de llamar a <code>join()</code> sobre él producirá un error en tiempo de ejecución.</p>
<p>Puedes verificar este estado con el método <code>joinable()</code>:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">worker</span><span class="hljs-params">(do_some_work)</span></span>;
<span class="hljs-keyword">if</span> (worker.joinable()) {
    worker.join();
}
</code></pre>
<p>Por lo tanto, cada hilo debe terminar de una de dos formas:</p>
<ul>
<li><p><strong>sincronizándose</strong> mediante <code>join()</code></p>
</li>
<li><p><strong>liberándose</strong> mediante <code>detach()</code>, que lo convierte en un <strong>hilo en segundo plano</strong> (<em>background thread</em>).</p>
</li>
</ul>
<h3 id="heading-hilos-en-segundo-plano">Hilos en segundo plano</h3>
<p>El método <code>detach()</code> permite que un hilo se ejecute <strong>de manera independiente</strong>, sin necesidad de que el hilo principal espere su finalización. Al llamarlo, el hilo queda completamente desacoplado del objeto <code>std::thread</code>, pasando a ser gestionado por el sistema operativo.</p>
<p><strong><em>background_task_CD_02</em></strong></p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">background_task</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Tarea en segundo plano iniciada\n"</span>;
    <span class="hljs-built_in">std</span>::this_thread::sleep_for(<span class="hljs-built_in">std</span>::chrono::seconds(<span class="hljs-number">3</span>));
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Tarea en segundo plano finalizada\n"</span>;
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(background_task)</span></span>;
    t.detach();  <span class="hljs-comment">// El hilo continúa ejecutándose de forma independiente</span>
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Hilo principal continúa sin esperar\n"</span>;
}
</code></pre>
<p>Este enfoque resulta útil cuando la tarea es autónoma —por ejemplo, registrar información, limpiar recursos o realizar un trabajo no crítico— y su finalización no afecta al flujo principal del programa.</p>
<p>Sin embargo, el uso de <code>detach()</code> implica una <strong>pérdida total de control</strong> sobre el hilo: ya no puede ser sincronizado, y si accede a recursos locales que ya fueron destruidos, el comportamiento será indefinido. Por ello, debe usarse con precaución y únicamente cuando el hilo no dependa de datos cuyo ciclo de vida sea más corto que el suyo.</p>
<h3 id="heading-esperas-mas-precisas">Esperas más precisas</h3>
<p>El método <code>join()</code> ofrece una sincronización binaria: esperas completamente o no esperas en absoluto. Si necesitas comprobar periódicamente si el hilo ha terminado, o limitar el tiempo de espera, deberás recurrir a mecanismos más avanzados como <strong>futures</strong>, <strong>promises</strong> o <strong>condition variables</strong>, que permiten un control más granular sobre la sincronización.</p>
<p>Estos temas se abordarán más adelante, pero por ahora basta con entender que <code>join()</code> es la forma más directa y segura de garantizar que un hilo haya finalizado antes de continuar, mientras que <code>detach()</code> ofrece una ejecución completamente independiente, con la responsabilidad adicional de asegurar que los recursos del hilo sigan siendo válidos durante su ejecución.</p>
<h2 id="heading-4-esperar-en-circunstancias-de-excepciones">4. Esperar en circunstancias de excepciones</h2>
<p>Gestionar correctamente los hilos no solo implica saber cuándo sincronizarlos, sino también <strong>asegurar que siempre sean liberados</strong>, incluso si ocurre una excepción. Recordemos que un objeto <code>std::thread</code> debe terminar su ciclo de vida habiendo sido <strong>unido (</strong><code>join()</code>) o <strong>desacoplado (</strong><code>detach()</code>).<br />De lo contrario, al destruirse un objeto <code>std::thread</code> todavía <em>joinable</em>, el programa llamará a <code>std::terminate()</code>.</p>
<p>Esto representa un riesgo evidente en contextos donde el flujo puede interrumpirse abruptamente, por ejemplo, debido a una excepción. Si el control abandona una función antes de ejecutar la llamada a <code>join()</code>, el hilo quedará sin gestionar y el programa fallará.</p>
<h4 id="heading-ejemplo-del-problema">Ejemplo del problema</h4>
<p>Supón el siguiente escenario simplificado:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">f</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">int</span> some_local_state = <span class="hljs-number">0</span>;
    <span class="hljs-function">func <span class="hljs-title">my_func</span><span class="hljs-params">(some_local_state)</span></span>;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(my_func)</span></span>;

    do_something_in_current_thread();
    t.join();
}
</code></pre>
<p>En condiciones normales, este código funciona correctamente: el hilo se une antes de salir de la función.</p>
<p>Pero si <code>do_something_in_current_thread()</code> lanza una excepción, <strong>la llamada a</strong> <code>join()</code> nunca se ejecutará, y cuando <code>t</code> se destruya, el programa terminará con <code>std::terminate()</code>.</p>
<h4 id="heading-manejo-explicito-con-trycatch">Manejo explícito con <code>try</code>/<code>catch</code></h4>
<p>Una solución inmediata consiste en usar un bloque <code>try</code>/<code>catch</code> para garantizar que el hilo se una tanto en la ejecución normal como en la excepcional:</p>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">f</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">int</span> some_local_state = <span class="hljs-number">0</span>;
    <span class="hljs-function">func <span class="hljs-title">my_func</span><span class="hljs-params">(some_local_state)</span></span>;
    <span class="hljs-function"><span class="hljs-built_in">std</span>::thread <span class="hljs-title">t</span><span class="hljs-params">(my_func)</span></span>;

    <span class="hljs-keyword">try</span> {
        do_something_in_current_thread();
    } <span class="hljs-keyword">catch</span> (...) {
        t.join();  <span class="hljs-comment">// Asegura que el hilo se libere</span>
        <span class="hljs-keyword">throw</span>;     <span class="hljs-comment">// Repropaga la excepción</span>
    }

    t.join();  <span class="hljs-comment">// Camino normal</span>
}
</code></pre>
<p>Esta estrategia es funcional, pero tiene dos inconvenientes:</p>
<ul>
<li><p>Duplica la llamada a <code>join()</code>, lo que ensucia el código.</p>
</li>
<li><p>Es propensa a errores si el bloque <code>try</code> no cubre todas las rutas de salida posibles.</p>
</li>
</ul>
<h4 id="heading-solucion-raii-clasica-el-patron-thread-guard">Solución RAII clásica: el patrón <em>thread guard</em></h4>
<p>En <em>C++ Concurrency in Action</em>, Anthony Williams propone una solución basada en <strong>RAII (Resource Acquisition Is Initialization)</strong> mediante una clase llamada <code>thread_guard</code>. Su destructor garantiza que el hilo se una automáticamente al salir del alcance, incluso si se lanza una excepción.</p>
<pre><code class="lang-cpp"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">thread_guard</span> {</span>
    <span class="hljs-built_in">std</span>::thread&amp; t;
<span class="hljs-keyword">public</span>:
    <span class="hljs-function"><span class="hljs-keyword">explicit</span> <span class="hljs-title">thread_guard</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::thread&amp; t_)</span> : <span class="hljs-title">t</span><span class="hljs-params">(t_)</span> </span>{}
    ~thread_guard() {
        <span class="hljs-keyword">if</span> (t.joinable())
            t.join();
    }
    thread_guard(thread_guard <span class="hljs-keyword">const</span>&amp;) = <span class="hljs-keyword">delete</span>;
    thread_guard&amp; <span class="hljs-keyword">operator</span>=(thread_guard <span class="hljs-keyword">const</span>&amp;) = <span class="hljs-keyword">delete</span>;
};
</code></pre>
<p>El principio es simple: cuando el objeto <code>thread_guard</code> sale de ámbito —ya sea por una salida normal o por una excepción— su destructor invoca <code>join()</code> si el hilo sigue activo. Este patrón es una aplicación directa del RAII y resulta muy eficaz para C++11 y C++17.</p>
<hr />
<h2 id="heading-5-stdjthread">5. std::jthread</h2>
<p>En C++20, el estándar introdujo <code>std::jthread</code>, que <strong>ya implementa automáticamente la lógica de unión en su destructor</strong>, reemplazando la necesidad de un <code>thread_guard</code> manual. Al destruirse un <code>std::jthread</code>, si el hilo sigue ejecutándose, el destructor llama a <code>join()</code> de manera segura, eliminando el riesgo de <code>std::terminate()</code>.</p>
<p>Además, <code>std::jthread</code> admite cancelación cooperativa mediante <code>std::stop_token</code>, lo que facilita el diseño de tareas que pueden detenerse desde fuera del hilo.</p>
<h4 id="heading-ejemplo-con-stdjthread">Ejemplo con <code>std::jthread</code></h4>
<pre><code class="lang-cpp"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;thread&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;chrono&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">do_work</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Hilo iniciado\n"</span>;
    <span class="hljs-built_in">std</span>::this_thread::sleep_for(<span class="hljs-built_in">std</span>::chrono::seconds(<span class="hljs-number">2</span>));
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Hilo finalizado\n"</span>;
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">f</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function"><span class="hljs-built_in">std</span>::jthread <span class="hljs-title">t</span><span class="hljs-params">(do_work)</span></span>;  <span class="hljs-comment">// Se unirá automáticamente al salir del scope</span>
    do_something_in_current_thread();  <span class="hljs-comment">// Si lanza, no hay problema</span>
}
</code></pre>
<p>Si <code>do_something_in_current_thread()</code> lanza una excepción, el hilo <code>t</code> <strong>se unirá automáticamente</strong> al salir del ámbito de la función, gracias al destructor de <code>std::jthread</code>.<br />Esto elimina por completo la necesidad de un bloque <code>try</code> o de una clase auxiliar.</p>
<h4 id="heading-ejemplo-con-cancelacion-cooperativa">Ejemplo con cancelación cooperativa</h4>
<p><strong><em>stop_token_CD_03</em></strong></p>
<pre><code class="lang-cpp"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;thread&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;chrono&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">task</span><span class="hljs-params">(<span class="hljs-built_in">std</span>::stop_token st)</span> </span>{
    <span class="hljs-keyword">while</span> (!st.stop_requested()) {
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Trabajando...\n"</span>;
        <span class="hljs-built_in">std</span>::this_thread::sleep_for(<span class="hljs-built_in">std</span>::chrono::milliseconds(<span class="hljs-number">500</span>));
    }
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Cancelado\n"</span>;
}

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-function"><span class="hljs-built_in">std</span>::jthread <span class="hljs-title">t</span><span class="hljs-params">(task)</span></span>;  <span class="hljs-comment">// hilo con soporte de cancelación</span>
    <span class="hljs-built_in">std</span>::this_thread::sleep_for(<span class="hljs-built_in">std</span>::chrono::seconds(<span class="hljs-number">2</span>));
    t.request_stop();      <span class="hljs-comment">// solicita la detención</span>
}
</code></pre>
<p>El hilo se ejecuta mientras no se solicite su detención, y al salir del <code>main()</code>, el destructor de <code>std::jthread</code> realiza el <code>join()</code> automáticamente, garantizando una finalización limpia.</p>
<p>Hasta este punto se he mostrado las bases para lanzar hilos y aplicar una forma sencilla de sincronización en C++. El trabajo con hilos empieza a adquirir estructura: ya no se trata solo de ejecutar tareas, sino de comprender ciclo de vida del hilo y mantener el control sobre cada una de ellos. En la siguiente parte se profundizaré en la gestión de los hilos, el paso de argumentos y el manejo de su propiedad.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/multithreading_cpp">https://github.com/Nobody-1321/multithreading_cpp</a></div>
]]></content:encoded></item><item><title><![CDATA[C++ Multithreading desde cero  — Parte 1]]></title><description><![CDATA[C++ siempre ha tenido esa fama de ser un lenguaje complejo, y la verdad es que no es un mito. Es un lenguaje amplio, profundo y con muchos matices que requieren una enorme cantidad de tiempo para dominar. Llevo casi dos años programando en C++, y dur...]]></description><link>https://codigoenllamas.com/c-multithreading-desde-cero-pt-1</link><guid isPermaLink="true">https://codigoenllamas.com/c-multithreading-desde-cero-pt-1</guid><category><![CDATA[Cpp Tutorial ]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Mon, 10 Nov 2025 23:52:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/pitSNmqUfio/upload/945180ee4e5a1133017009b3170923b3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>C++ siempre ha tenido esa fama de ser un lenguaje complejo, y la verdad es que no es un mito. Es un lenguaje amplio, profundo y con muchos matices que requieren una enorme cantidad de tiempo para dominar. Llevo casi dos años programando en C++, y durante ese tiempo había evitado el tema del multihilo. La primera vez que intenté estudiarlo apenas estaba comenzando mi recorrido con el lenguaje, y la complejidad del tema me sobrepasó, al punto de frustrarme y alejarme de él, Desde entonces lo había evitado, pero sabía que tarde o temprano llegaría el momento de enfrentarlo.</p>
<p>Recientemente estuve desarrollando un <em>plugin</em> para Photoshop, implementado de forma completamente secuencial. Sin embargo, el tiempo de ejecución resultaba demasiado lento, y pronto me di cuenta de que la tarea podía dividirse fácilmente en bloques independientes, lo que la convertía en un algoritmo altamente paralelizable.</p>
<p>Me decidí a tomar un curso, ver algunos tutoriales y revisar ejemplos de código. Aunque logré aprender ciertos conceptos, no puedo decir que los comprendi en profundidad. Por eso opté por empezar de nuevo, esta vez con calma, yendo paso a paso desde el principio. Elegí leer el libro <em>C++ Concurrency in Action</em>, que muchos consideran el mejor material para aprender de forma sólida y profunda sobre la programación multihilo en C++.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762816772878/21316e05-8060-4d5a-be35-a9d5088868f3.png" alt="Cover of &quot;C++ Concurrency in Action,&quot; 2nd Edition by Anthony Williams. Features an illustration of a person in historical attire against a brown background, with the Manning logo." class="image--center mx-auto" /></p>
<p>He leído varios capítulos del libro y he afianzado mucho más mi comprensión del tema. Me ha fascinado el nivel de conocimiento y detalle que requiere desarrollar aplicaciones de calidad que aprovechen el paralelismo y la concurrencia de forma correcta.</p>
<p>Por eso, esta serie de artículos estará dedicada a explorar los temas tratados en el libro, explicando sus ejemplos y acompañándolos con aplicaciones propias que iré desarrollando a lo largo del proceso.</p>
<hr />
<h2 id="heading-1-que-es-la-concurrencia"><strong>1. ¿Qué es la concurrencia?</strong></h2>
<p>En su forma más básica, <strong>la concurrencia es la capacidad de realizar múltiples actividades al mismo tiempo</strong>. No se trata únicamente de ejecutar instrucciones en paralelo, sino de organizar el trabajo de manera que varias tareas progresen de forma independiente, solapando su ejecución en el tiempo. Este concepto no es exclusivo de la computación: en la vida diaria también actuamos concurrentemente. Podemos caminar y hablar a la vez, escribir con una mano mientras sostenemos un objeto con la otra o realizar distintas tareas en paralelo con personas diferentes.</p>
<p>En programación, la concurrencia busca aprovechar esta idea para <strong>aumentar la eficiencia y la capacidad de respuesta de los programas</strong>, especialmente en sistemas modernos donde los procesadores disponen de múltiples núcleos. Sin embargo, <strong>concurrencia no significa necesariamente paralelismo</strong>: un programa concurrente puede manejar varias tareas en progreso, aunque el hardware ejecute solo una a la vez; en cambio, el paralelismo implica que las tareas realmente se ejecutan simultáneamente en distintos núcleos o procesadores.</p>
<hr />
<h3 id="heading-enfoques-de-la-concurrencia"><strong>Enfoques de la concurrencia</strong></h3>
<p>Existen dos formas principales de implementar concurrencia en una aplicación:<br /><strong>usando múltiples procesos</strong> o <strong>usando múltiples hilos dentro de un mismo proceso</strong>. Aunque ambos enfoques persiguen el mismo objetivo —permitir que varias tareas avancen de forma independiente—, difieren en la manera en que gestionan los recursos y se comunican.</p>
<h3 id="heading-concurrencia-con-multiples-procesos"><strong>Concurrencia con múltiples procesos</strong></h3>
<p>En este modelo, la aplicación se divide en <strong>varios procesos independientes</strong>, cada uno con su propio espacio de memoria y su propio flujo de ejecución.<br />Cada proceso se comporta como una entidad aislada: si uno falla, no afecta directamente a los demás, y la comunicación entre ellos se realiza mediante <strong>mecanismos de comunicación entre procesos (IPC)</strong>, como <strong>tuberías (pipes)</strong>, <strong>sockets</strong>, <strong>archivos compartidos</strong> o <strong>señales</strong> del sistema operativo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762817347481/2e888828-3789-4f2e-9169-9e39942c863b.gif" alt class="image--center mx-auto" /></p>
<p>Este enfoque tiene ventajas importantes:</p>
<ul>
<li><p><strong>Mayor aislamiento y seguridad.</strong> Los procesos están protegidos entre sí, lo que reduce el riesgo de errores de memoria o corrupción de datos compartidos.</p>
</li>
<li><p><strong>Mayor estabilidad.</strong> Si un proceso se bloquea, los demás pueden continuar funcionando sin problema.</p>
</li>
<li><p><strong>Escalabilidad.</strong> Es posible distribuir los procesos entre distintos equipos conectados por red, aumentando así la capacidad de procesamiento del sistema.</p>
</li>
</ul>
<p>Sin embargo, también presenta desventajas:</p>
<ul>
<li><p><strong>Comunicación más costosa.</strong> Los datos deben copiarse entre espacios de memoria separados, lo que añade latencia.</p>
</li>
<li><p><strong>Mayor sobrecarga del sistema.</strong> Crear y gestionar varios procesos consume más recursos del sistema operativo.</p>
</li>
<li><p><strong>Dificultad de sincronización.</strong> Coordinar el trabajo entre procesos puede requerir estructuras de comunicación complejas.</p>
</li>
</ul>
<p>A pesar de estos inconvenientes, <strong>muchos lenguajes y sistemas distribuidos</strong> (como <em>Erlang</em>) usan este modelo por su fiabilidad y facilidad para construir aplicaciones robustas.</p>
<h3 id="heading-concurrencia-con-multiples-hilos"><strong>Concurrencia con múltiples hilos</strong></h3>
<p>El segundo enfoque —y el más utilizado en C++— es la <strong>concurrencia basada en hilos</strong>. Aquí, una única aplicación (un solo proceso) puede contener <strong>varios hilos de ejecución</strong> que comparten el mismo espacio de memoria. Cada hilo se comporta como un flujo independiente, capaz de ejecutar una parte distinta del programa, pero todos tienen acceso directo a las mismas variables y recursos.</p>
<p>Esto hace que la comunicación entre hilos sea mucho más rápida y eficiente, ya que <strong>los datos pueden compartirse directamente sin mecanismos externos</strong>. Sin embargo, esta facilidad trae consigo nuevos desafíos: si varios hilos acceden a la misma variable al mismo tiempo, pueden producirse <strong>condiciones de carrera</strong> o <strong>inconsistencias en la memoria</strong>.</p>
<p>Por ello, el programador debe garantizar manualmente la sincronización del acceso a los datos compartidos mediante <strong>mutexes</strong>, <strong>bloqueos</strong> u otros mecanismos de control.</p>
<p>Las principales ventajas de este enfoque son:</p>
<ul>
<li><p><strong>Bajo costo de creación y comunicación.</strong> Los hilos son más livianos que los procesos.</p>
</li>
<li><p><strong>Mayor eficiencia.</strong> Permiten aprovechar al máximo los procesadores multinúcleo.</p>
</li>
<li><p><strong>Integración directa con C++.</strong> Desde C++11, la biblioteca estándar incluye soporte nativo para hilos (<code>std::thread</code>) y sincronización.</p>
</li>
</ul>
<p>Pero también tiene desventajas notables:</p>
<ul>
<li><p><strong>Mayor complejidad.</strong> Es necesario diseñar cuidadosamente cómo se comparten y modifican los datos.</p>
</li>
<li><p><strong>Riesgo de errores difíciles de depurar.</strong> Problemas como bloqueos mutuos o interferencias entre hilos pueden ser difíciles de reproducir y corregir.</p>
</li>
</ul>
<p>Por estas razones, el modelo multihilo es <strong>más rápido pero también más exigente</strong>. Requiere disciplina, conocimiento del modelo de memoria y el uso adecuado de las herramientas de sincronización.</p>
<p>En esta serie de artículos me enfocaré principalmente en este segundo enfoque —la <strong>concurrencia mediante múltiples hilos</strong>—, siguiendo las bases teóricas y prácticas que plantea el libro <em>C++ Concurrency in Action</em>.</p>
<hr />
<h2 id="heading-2-concurrencia-vs-paralelismo">2. Concurrencia vs Paralelismo</h2>
<p>Aunque los términos <em>concurrencia</em> y <em>paralelismo</em> suelen usarse indistintamente, en el contexto de la programación multihilo existe una distinción conceptual importante entre ambos. Ambos se refieren a la ejecución simultánea de múltiples tareas, pero difieren en su propósito y enfoque.</p>
<p>La <strong>concurrencia</strong> se centra en la <em>estructura</em> y la <em>organización</em> del software. Su objetivo principal es permitir que diferentes tareas avancen de manera independiente, incluso si no se ejecutan estrictamente al mismo tiempo. Es una herramienta para manejar múltiples actividades que comparten recursos o requieren coordinación, buscando mejorar la <em>responsividad</em> y la <em>separación de responsabilidades</em> dentro de una aplicación.</p>
<p>Por otro lado, el <strong>paralelismo</strong> es una cuestión de <em>rendimiento</em>. Se trata de aprovechar el hardware disponible —como múltiples núcleos de CPU— para realizar varios cálculos en simultáneo. Su propósito es claro: reducir el tiempo total de ejecución de una tarea o aumentar el volumen de trabajo que puede procesarse en un mismo intervalo de tiempo. En pocas palabras, mientras la concurrencia busca <em>organizar mejor el trabajo</em>, el paralelismo busca <em>hacerlo más rápido</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762817822117/735b91f1-bf82-4d4c-9587-d388daf79c1a.jpeg" alt class="image--center mx-auto" /></p>
<p>Esta diferencia de enfoque implica que no toda aplicación concurrente es necesariamente paralela. Una interfaz gráfica que mantiene su fluidez mientras realiza operaciones en segundo plano es concurrente, aunque no necesariamente paralela. En cambio, un programa que divide un conjunto de datos entre varios hilos para procesarlos por separado es claramente un caso de paralelismo.</p>
<hr />
<h2 id="heading-3-por-que-usar-concurrencia">3. ¿Por qué usar concurrencia?</h2>
<p>Existen dos razones fundamentales para utilizar concurrencia en una aplicación: <strong>separación de responsabilidades</strong> y <strong>rendimiento</strong>. Ambas son pilares del diseño moderno de software y reflejan diferentes motivaciones detrás del uso de múltiples hilos o procesos.</p>
<h3 id="heading-separacion-de-responsabilidades">Separación de responsabilidades</h3>
<p>Una de las mejores prácticas en ingeniería de software es dividir el sistema en componentes bien delimitados. La concurrencia permite llevar esta idea un paso más allá: no solo separar el código, sino también permitir que distintas partes de una aplicación <em>se ejecuten de forma independiente</em>.</p>
<p>Imaginemos una aplicación como un reproductor de DVD. Este debe, por un lado, leer los datos del disco, decodificar el video y el audio, y enviarlos al hardware sin interrupciones; y por otro lado, debe responder a las acciones del usuario —como pausar o detener la reproducción— sin retrasos. Si todo se ejecutara en un solo hilo, el programa tendría que alternar manualmente entre tareas, complicando el diseño y reduciendo la fluidez de la interfaz.</p>
<p>La solución natural es utilizar hilos separados: uno para la reproducción y otro para la interfaz de usuario. De este modo, cada componente puede concentrarse en su función principal, y la interacción entre ambos ocurre solo en puntos bien definidos, como al recibir una orden de pausa. Este enfoque mejora la organización interna del código y crea una sensación de <em>responsividad</em> inmediata, incluso cuando el sistema está ocupado realizando tareas intensivas.</p>
<h3 id="heading-concurrencia-para-mejorar-el-rendimiento">Concurrencia para mejorar el rendimiento</h3>
<p>Durante décadas, el aumento del rendimiento en software venía “gratis”: los procesadores mejoraban su velocidad con cada nueva generación, y los programas se volvían automáticamente más rápidos. Sin embargo, esta tendencia llegó a su límite físico y térmico. Los fabricantes cambiaron de estrategia: en lugar de aumentar la frecuencia de reloj, comenzaron a incorporar <em>múltiples núcleos</em> en un mismo chip.</p>
<p>El resultado es que ahora la única forma de aprovechar plenamente la capacidad de cómputo de una máquina moderna es mediante la concurrencia. Para que una aplicación se beneficie de un procesador multinúcleo, debe ser capaz de dividir su trabajo en tareas que puedan ejecutarse simultáneamente.</p>
<p>Existen dos enfoques principales para lograrlo:</p>
<ul>
<li><p><strong>Paralelismo de tareas (Task Parallelism):</strong> consiste en dividir una tarea compleja en subtareas independientes que pueden ejecutarse al mismo tiempo. Por ejemplo, distintos hilos podrían encargarse de etapas separadas de un algoritmo o de diferentes componentes de un sistema.</p>
</li>
<li><p><strong>Paralelismo de datos (Data Parallelism):</strong> implica aplicar la misma operación sobre diferentes partes de un conjunto de datos. Un ejemplo clásico es el procesamiento de imágenes, donde varios hilos pueden aplicar el mismo filtro sobre diferentes regiones de la imagen.</p>
</li>
</ul>
<p>Los algoritmos que se prestan fácilmente a esta división se denominan <em>embarrassingly parallel</em>, un término que resalta lo sencillo que resulta paralelizarlos. Este tipo de algoritmos escalan muy bien, ya que pueden aprovechar un número creciente de núcleos para reducir proporcionalmente su tiempo de ejecución.</p>
<p>Finalmente, el paralelismo no siempre se usa para reducir el tiempo de procesamiento de una sola tarea: también puede servir para aumentar el <em>volumen</em> de trabajo procesado en paralelo. Por ejemplo, un programa puede procesar varias imágenes o archivos simultáneamente, aumentando así su <em>rendimiento global</em> o <em>throughput</em>.</p>
<hr />
<h2 id="heading-4-cuando-no-usar-concurrencia">4. Cuándo no usar concurrencia</h2>
<p>Tan importante como saber cuándo aplicar concurrencia es reconocer cuándo <strong>no hacerlo</strong>. A pesar de sus beneficios, la concurrencia introduce una capa adicional de complejidad que puede volverse contraproducente si el problema no lo justifica.</p>
<p>La razón fundamental para evitar la concurrencia es simple: <strong>cuando el beneficio no compensa el costo</strong>. Escribir código concurrente suele ser más difícil de entender, probar y mantener. La interacción entre múltiples hilos puede generar errores sutiles, como condiciones de carrera, bloqueos mutuos (<em>deadlocks</em>) o inconsistencias de memoria. Estos problemas no solo consumen tiempo de desarrollo, sino que también aumentan el riesgo de fallos en producción.</p>
<p>Además del costo cognitivo, la concurrencia implica <strong>costos de rendimiento</strong>. Cada hilo consume recursos del sistema operativo —memoria para su pila, estructuras de control y tiempo de planificación—, por lo que crearlo y sincronizarlo nunca es gratuito. Si la tarea es demasiado breve, ese overhead puede anular cualquier ganancia esperada, e incluso empeorar el rendimiento. A esto se suma que <strong>los hilos son un recurso limitado</strong>: lanzar demasiados puede saturar el sistema, consumir memoria en exceso o degradar la eficiencia por un exceso de <em>context switching</em>. Cada cambio de contexto implica guardar y restaurar estados del procesador, una operación costosa que se multiplica cuando el número de hilos activos supera la cantidad de núcleos disponibles.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762818481428/ebc8fee4-5466-4dfe-ab7b-b456e232af32.png" alt class="image--center mx-auto" /></p>
<p>Por estas razones, la concurrencia como estrategia de optimización debe aplicarse con el mismo criterio que cualquier otra técnica de alto rendimiento: <strong>solo vale la pena cuando los beneficios son medibles y compensan la complejidad añadida</strong>. En muchos casos, optar por un enfoque secuencial o asincrónico ofrece un mejor equilibrio entre claridad, mantenibilidad y eficiencia.</p>
<p>Sin embargo, si el diseño requiere <strong>separación de responsabilidades</strong> —por ejemplo, para mantener una interfaz fluida mientras se ejecutan tareas de fondo— o si el rendimiento depende directamente del aprovechamiento del hardware disponible, entonces la concurrencia se convierte en una herramienta valiosa y necesaria. La clave está en usarla con criterio y propósito, no por simple moda o entusiasmo técnico.<br />Una idea que resume bien esta reflexión aparece en la charla <em>“Multithreading is the answer. What is the question?”</em> de <strong>Ansel Sermersheim (CppCon 2017)</strong>, donde se enfatiza que aplicar multithreading sin un propósito claro suele generar más problemas que beneficios. En otras palabras, <strong>la concurrencia no debe ser la respuesta automática a todo desafío de rendimiento</strong>, sino una decisión técnica fundamentada en la naturaleza del problema.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=GNw3RXr-VJk&amp;t=2657s">https://www.youtube.com/watch?v=GNw3RXr-VJk&amp;t=2657s</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/multithreading_cpp">https://github.com/Nobody-1321/multithreading_cpp</a></div>
]]></content:encoded></item><item><title><![CDATA[Redimensionamiento de imágenes. técnicas clásicas de interpolación]]></title><description><![CDATA[Introducción
El redimensionamiento de imágenes consiste en cambiar las dimensiones de una imagen, ya sea para aumentar su tamaño (upscaling) o reducirlo (downscaling), manteniendo la mayor fidelidad posible respecto a la imagen original.Este proceso ...]]></description><link>https://codigoenllamas.com/tecnicas-clasicas-de-interpolacion</link><guid isPermaLink="true">https://codigoenllamas.com/tecnicas-clasicas-de-interpolacion</guid><category><![CDATA[Python]]></category><category><![CDATA[Computer Vision]]></category><category><![CDATA[image processing]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Mon, 20 Oct 2025 00:13:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/PLocvVI8-JU/upload/1c16c50d602214046f247514c4db99aa.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduccion">Introducción</h2>
<p>El redimensionamiento de imágenes consiste en cambiar las dimensiones de una imagen, ya sea para <strong>aumentar su tamaño (upscaling)</strong> o <strong>reducirlo (downscaling)</strong>, manteniendo la mayor fidelidad posible respecto a la imagen original.Este proceso es ampliamente usado, tanto en la edición de fotos y videos como en aplicaciones de procesamiento de imágenes como reconocimiento facial, visión robótica, y entrenamiento de modelos de inteligencia artificial, donde la coherencia y la calidad de los datos visuales son determinantes.</p>
<p>Aunque la idea parece sencilla —simplemente <strong>ampliar o reducir</strong> una imagen—, esto supone un desafío técnico: cada píxel agregado o eliminado debe generarse de manera que respete la estructura visual, así como los bordes. De lo contrario, pueden aparecer artefactos como <strong>pixelado</strong>, <strong>borrosidad</strong> o <strong>aliasing</strong>, afectando tanto la percepción visual como el rendimiento de algoritmos posteriores.</p>
<p>Existen múltiples métodos de redimensionamiento, cada uno con sus ventajas y limitaciones, que se diferencian principalmente por la manera en que calculan los nuevos valores de pixel a partir de los existentes. En este artículo exploraremos cuatro técnicas ampliamente utilizadas: <strong>Vecino más cercano (Nearest Neighbor), Interpolación bilineal, Interpolación bicúbica y Lanczos</strong>, analizando cómo funcionan, sus ventajas, desventajas y resultados prácticos sobre imágenes reales.</p>
<h2 id="heading-1-conceptos-basicos">1. Conceptos básicos</h2>
<p>Antes de profundizar en los métodos de redimensionamiento, es importante entender algunos conceptos fundamentales que determinan cómo y por qué se transforman las imágenes.</p>
<h3 id="heading-11-pixeles-y-resolucion">1.1 Píxeles y resolución</h3>
<p>Una imagen digital está compuesta por <strong>píxeles</strong>, que son los elementos mínimos de información visual. Cada píxel contiene información de color y, en conjunto, forman la imagen completa. La <strong>resolución</strong> de una imagen se define por la cantidad de píxeles en sus dimensiones: ancho × alto. Por ejemplo, una imagen de 800×600 píxeles tiene 480,000 píxeles en total.</p>
<p>Cambiar la resolución de una imagen implica recalcular la información de los píxeles para ajustarla a las nuevas dimensiones.</p>
<h3 id="heading-12-upscaling-y-downscaling">1.2 Upscaling y downscaling</h3>
<ul>
<li><p><strong>Upscaling</strong>: aumentar el tamaño de la imagen. Esto requiere generar nuevos píxeles a partir de los existentes. El desafío principal es <strong>preservar detalles</strong> y evitar que la imagen se vea borrosa o pixelada.</p>
</li>
<li><p><strong>Downscaling</strong>: reducir el tamaño de la imagen. Aquí se deben <strong>combinar o eliminar píxeles</strong> de manera que la información relevante se mantenga y no se pierdan detalles importantes.</p>
</li>
</ul>
<p>Cada operación tiene implicaciones diferentes sobre la calidad de la imagen y sobre el rendimiento de los algoritmos que la procesarán.</p>
<h3 id="heading-13-problemas-comunes">1.3. Problemas comunes</h3>
<p>Al redimensionar imágenes, es habitual enfrentarse a ciertos artefactos:</p>
<ul>
<li><p><strong>Pixelado</strong>: ocurre cuando se usan métodos muy simples, como el vecino más cercano, y los bordes aparecen escalonados.</p>
</li>
<li><p><strong>Borroso o suavizado excesivo</strong>: puede aparecer al usar interpolaciones que promedian demasiado los píxeles cercanos, como bilineal o bicúbica.</p>
</li>
<li><p><strong>Aliasing</strong>: sucede cuando se reduce demasiado la resolución y se pierden detalles finos, generando patrones no deseados en la imagen.</p>
</li>
</ul>
<p>Comprender estos problemas permite elegir el método de redimensionamiento más adecuado según el objetivo, ya sea velocidad, fidelidad visual o preservación de detalles.</p>
<h2 id="heading-2-vecino-mas-cercano-nearest-neighbor">2. Vecino más cercano (Nearest Neighbor)</h2>
<p>El método de <strong>vecino más cercano</strong> es la forma más simple de redimensionar imágenes. Su funcionamiento es intuitivo: cada píxel de la nueva imagen toma el valor del píxel más cercano de la imagen original, sin realizar ningún tipo de interpolación. Esto significa que no se generan valores intermedios; simplemente se copia la información existente.</p>
<h3 id="heading-ventajas"><strong>Ventajas:</strong></h3>
<ul>
<li><p>Muy rápido y eficiente.</p>
</li>
<li><p>Fácil de implementar y comprender.</p>
</li>
</ul>
<h3 id="heading-desventajas"><strong>Desventajas:</strong></h3>
<ul>
<li><p>Produce bordes escalonados o pixelados, especialmente al <strong>aumentar</strong> la imagen.</p>
</li>
<li><p>No preserva detalles finos ni suaviza transiciones de color.</p>
</li>
</ul>
<h3 id="heading-implementacion-en-python"><strong>Implementación en Python:</strong></h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">resize_nearest_neighbor</span>(<span class="hljs-params">image, new_width, new_height</span>):</span>
    <span class="hljs-string">"""
    Redimensiona una imagen usando interpolación Nearest Neighbor (vecino más cercano).

    :param image: imagen de entrada (numpy array)
    :param new_width: ancho deseado
    :param new_height: alto deseado
    :return: imagen redimensionada
    """</span>
    h_in, w_in = image.shape[:<span class="hljs-number">2</span>]
    h_out, w_out = new_height, new_width

    <span class="hljs-comment"># Crear imagen vacía de salida</span>
    <span class="hljs-keyword">if</span> len(image.shape) == <span class="hljs-number">3</span>:  <span class="hljs-comment"># Imagen en color</span>
        output = np.zeros((h_out, w_out, image.shape[<span class="hljs-number">2</span>]), dtype=image.dtype)
    <span class="hljs-keyword">else</span>:  <span class="hljs-comment"># Imagen en escala de grises</span>
        output = np.zeros((h_out, w_out), dtype=image.dtype)

    <span class="hljs-comment"># Escalamiento</span>
    <span class="hljs-keyword">for</span> y_out <span class="hljs-keyword">in</span> range(h_out):
        <span class="hljs-keyword">for</span> x_out <span class="hljs-keyword">in</span> range(w_out):
            <span class="hljs-comment"># Mapeo inverso (salida → entrada)</span>
            x_in = int(round(x_out * w_in / w_out))
            y_in = int(round(y_out * h_in / h_out))

            <span class="hljs-comment"># Clamping para evitar índices fuera de rango</span>
            x_in = min(w_in - <span class="hljs-number">1</span>, x_in)
            y_in = min(h_in - <span class="hljs-number">1</span>, y_in)

            output[y_out, x_out] = image[y_in, x_in]

    <span class="hljs-keyword">return</span> output
</code></pre>
<p><strong>Explicación del algoritmo:</strong></p>
<ol>
<li><p>Se determina el tamaño de la imagen original (<code>h_in</code>, <code>w_in</code>) y el tamaño deseado (<code>h_out</code>, <code>w_out</code>).</p>
</li>
<li><p>Se crea una matriz vacía para la imagen de salida.</p>
</li>
<li><p>Para cada píxel de la imagen de salida, se calcula la posición correspondiente en la imagen original mediante un <strong>mapeo inverso</strong>.</p>
</li>
<li><p>Se aplica un <strong>clamping</strong> para evitar acceder fuera del rango de la imagen original.</p>
</li>
<li><p>Se copia el valor del píxel más cercano al nuevo píxel.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760911335691/2f64866b-79cd-42e8-89a4-5c9290349961.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-2-interpolacion-bilineal-bilinear">2. Interpolación bilineal (Bilinear)</h2>
<p>La <strong>interpolación bilineal</strong> es un método más avanzado que el de <strong>vecino más cercano</strong>, ya que calcula cada nuevo píxel como una combinación ponderada de los <strong>cuatro píxeles más cercanos</strong> de la imagen original. En lugar de copiar un único valor, el algoritmo realiza un promedio lineal en ambas direcciones —horizontal y vertical—, lo que produce transiciones más suaves y bordes menos marcados.</p>
<p>En términos geométricos, puede interpretarse como una interpolación primero a lo largo del eje <em>x</em> (entre los píxeles vecinos horizontales) y luego a lo largo del eje <em>y</em> (entre los resultados de esas interpolaciones).</p>
<h3 id="heading-ventajas-1"><strong>Ventajas:</strong></h3>
<ul>
<li><p>Genera resultados más suaves y agradables visualmente.</p>
</li>
<li><p>Reduce el efecto de pixelado presente en el método de vecino más cercano.</p>
</li>
</ul>
<h3 id="heading-desventajas-1"><strong>Desventajas:</strong></h3>
<ul>
<li><p>Puede producir una ligera pérdida de nitidez.</p>
</li>
<li><p>Requiere más cálculos, por lo que es más lento.</p>
</li>
</ul>
<h3 id="heading-implementacion-en-python-1">Implementación en Python</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">resize_bilinear</span>(<span class="hljs-params">image, new_width, new_height</span>):</span>
    <span class="hljs-string">"""
    Redimensiona una imagen usando interpolación bilineal.

    :param image: Imagen de entrada (numpy array)
    :param new_width: Ancho de salida
    :param new_height: Alto de salida
    :return: Imagen redimensionada
    """</span>
    h_in, w_in = image.shape[:<span class="hljs-number">2</span>]
    h_out, w_out = new_height, new_width

    <span class="hljs-comment"># Imagen de salida</span>
    <span class="hljs-keyword">if</span> len(image.shape) == <span class="hljs-number">3</span>:
        output = np.zeros((h_out, w_out, image.shape[<span class="hljs-number">2</span>]), dtype=np.float32)
    <span class="hljs-keyword">else</span>:
        output = np.zeros((h_out, w_out), dtype=np.float32)

    <span class="hljs-keyword">for</span> y_out <span class="hljs-keyword">in</span> range(h_out):
        <span class="hljs-keyword">for</span> x_out <span class="hljs-keyword">in</span> range(w_out):
            <span class="hljs-comment"># Mapear coordenada de salida a entrada (espacio continuo)</span>
            x_in = (x_out + <span class="hljs-number">0.5</span>) * (w_in / w_out) - <span class="hljs-number">0.5</span>
            y_in = (y_out + <span class="hljs-number">0.5</span>) * (h_in / h_out) - <span class="hljs-number">0.5</span>

            x1 = int(np.floor(x_in))
            y1 = int(np.floor(y_in))
            x2 = min(x1 + <span class="hljs-number">1</span>, w_in - <span class="hljs-number">1</span>)
            y2 = min(y1 + <span class="hljs-number">1</span>, h_in - <span class="hljs-number">1</span>)

            a = x_in - x1
            b = y_in - y1

            <span class="hljs-comment"># Píxeles vecinos</span>
            Q11 = image[y1, x1]
            Q21 = image[y1, x2]
            Q12 = image[y2, x1]
            Q22 = image[y2, x2]

            <span class="hljs-comment"># Interpolación bilineal</span>
            output[y_out, x_out] = (<span class="hljs-number">1</span> - a) * (<span class="hljs-number">1</span> - b) * Q11 + \
                                   a * (<span class="hljs-number">1</span> - b) * Q21 + \
                                   (<span class="hljs-number">1</span> - a) * b * Q12 + \
                                   a * b * Q22

    <span class="hljs-keyword">return</span> np.clip(output, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>).astype(np.uint8)
</code></pre>
<h4 id="heading-explicacion-paso-a-paso">Explicación paso a paso</h4>
<ol>
<li><p><strong>Mapeo inverso</strong>:<br /> Cada coordenada de la imagen de salida se transforma a una posición continua en la imagen original (<code>x_in</code>, <code>y_in</code>).<br /> Este mapeo asegura que cada píxel de la salida “corresponda” a una ubicación en la entrada.</p>
</li>
<li><p><strong>Selección de píxeles vecinos</strong>:<br /> Se identifican los cuatro píxeles más cercanos a esa posición continua:</p>
<ul>
<li><p><code>Q11</code> = esquina superior izquierda</p>
</li>
<li><p><code>Q21</code> = esquina superior derecha</p>
</li>
<li><p><code>Q12</code> = esquina inferior izquierda</p>
</li>
<li><p><code>Q22</code> = esquina inferior derecha</p>
</li>
</ul>
</li>
<li><p><strong>Interpolación en dos direcciones</strong>:</p>
<ul>
<li><p>Primero se interpola horizontalmente entre <code>Q11</code> y <code>Q21</code>, y entre <code>Q12</code> y <code>Q22.</code></p>
</li>
<li><p>Luego se interpola verticalmente entre los resultados anteriores.</p>
</li>
</ul>
</li>
<li><p><strong>Normalización y clipping</strong>:<br /> Los valores resultantes se limitan al rango <code>[0, 255]</code> para mantener la validez de los niveles de intensidad.</p>
</li>
</ol>
<p>Al comparar la imagen original con la redimensionada mediante interpolación bilineal, se observa una <strong>suavidad mayor</strong> respecto al método de vecino más cercano. Sin embargo, los bordes pueden perder algo de nitidez, especialmente cuando se amplía la imagen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760911433543/17e8b7f2-d3ab-4b72-8505-da212c469fca.png" alt class="image--center mx-auto" /></p>
<p>Este método es muy usado en aplicaciones donde se busca un equilibrio entre <strong>velocidad</strong> y <strong>calidad visual</strong>, como en la visualización de imágenes en tiempo real o el preprocesamiento para modelos de aprendizaje profundo.</p>
<h2 id="heading-3-interpolacion-bicubica-bicubic">3. Interpolación bicúbica (Bicubic)</h2>
<p>La <strong>interpolación bicúbica</strong> es una extensión de la bilineal que utiliza <strong>16 píxeles vecinos (una ventana de 4×4)</strong> para estimar el valor de cada nuevo píxel. En lugar de promediar linealmente, este método aplica una función <strong>cúbica</strong> para calcular los pesos de cada píxel según su distancia a la posición interpolada.</p>
<p>Este enfoque fue propuesto por <strong>Robert G. Keys en 1981</strong>, y su función de peso es una aproximación suave y continua que logra transiciones más naturales entre los píxeles. El resultado es una imagen más nítida y con mejor preservación de detalles, especialmente al realizar <strong>upscaling</strong>.</p>
<h3 id="heading-implementacion-en-python-2">Implementación en Python</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">cubic_weight</span>(<span class="hljs-params">x, a=<span class="hljs-number">-0.5</span></span>):</span>
    <span class="hljs-string">"""Función de pesos cúbica (Keys, 1981)"""</span>
    x = abs(x)
    <span class="hljs-keyword">if</span> x &lt; <span class="hljs-number">1</span>:
        <span class="hljs-keyword">return</span> (a + <span class="hljs-number">2</span>) * (x ** <span class="hljs-number">3</span>) - (a + <span class="hljs-number">3</span>) * (x ** <span class="hljs-number">2</span>) + <span class="hljs-number">1</span>
    <span class="hljs-keyword">elif</span> x &lt; <span class="hljs-number">2</span>:
        <span class="hljs-keyword">return</span> a * (x ** <span class="hljs-number">3</span>) - (<span class="hljs-number">5</span> * a) * (x ** <span class="hljs-number">2</span>) + (<span class="hljs-number">8</span> * a) * x - <span class="hljs-number">4</span> * a
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">resize_bicubic</span>(<span class="hljs-params">image, new_width, new_height</span>):</span>
    <span class="hljs-string">"""
    Redimensiona una imagen usando interpolación bicúbica.

    :param image: Imagen de entrada (numpy array)
    :param new_width: Ancho de salida
    :param new_height: Alto de salida
    :return: Imagen redimensionada
    """</span>
    h_in, w_in = image.shape[:<span class="hljs-number">2</span>]
    h_out, w_out = new_height, new_width

    <span class="hljs-comment"># Imagen de salida</span>
    <span class="hljs-keyword">if</span> len(image.shape) == <span class="hljs-number">3</span>:
        output = np.zeros((h_out, w_out, image.shape[<span class="hljs-number">2</span>]), dtype=np.float32)
    <span class="hljs-keyword">else</span>:
        output = np.zeros((h_out, w_out), dtype=np.float32)

    scale_x = w_in / w_out
    scale_y = h_in / h_out

    <span class="hljs-keyword">for</span> y_out <span class="hljs-keyword">in</span> range(h_out):
        <span class="hljs-keyword">for</span> x_out <span class="hljs-keyword">in</span> range(w_out):
            <span class="hljs-comment"># Posición en la imagen original</span>
            x_in = (x_out + <span class="hljs-number">0.5</span>) * scale_x - <span class="hljs-number">0.5</span>
            y_in = (y_out + <span class="hljs-number">0.5</span>) * scale_y - <span class="hljs-number">0.5</span>

            x_base = int(np.floor(x_in))
            y_base = int(np.floor(y_in))

            value = np.zeros(image.shape[<span class="hljs-number">2</span>], dtype=np.float32) <span class="hljs-keyword">if</span> len(image.shape) == <span class="hljs-number">3</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0.0</span>

            <span class="hljs-comment"># Recorrer vecinos 4x4</span>
            <span class="hljs-keyword">for</span> m <span class="hljs-keyword">in</span> range(<span class="hljs-number">-1</span>, <span class="hljs-number">3</span>):
                <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> range(<span class="hljs-number">-1</span>, <span class="hljs-number">3</span>):
                    x_idx = min(max(x_base + n, <span class="hljs-number">0</span>), w_in - <span class="hljs-number">1</span>)
                    y_idx = min(max(y_base + m, <span class="hljs-number">0</span>), h_in - <span class="hljs-number">1</span>)

                    wx = cubic_weight(x_in - (x_base + n))
                    wy = cubic_weight(y_in - (y_base + m))
                    w = wx * wy

                    value += image[y_idx, x_idx] * w

            output[y_out, x_out] = value

    <span class="hljs-keyword">return</span> np.clip(output, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>).astype(np.uint8)
</code></pre>
<h4 id="heading-explicacion-del-algoritmo">Explicación del algoritmo</h4>
<ol>
<li><p><strong>Función de peso cúbica (</strong><code>cubic_weight</code>)<br /> Define cómo contribuye cada píxel vecino en función de su distancia.</p>
<ul>
<li><p>El parámetro <code>a</code> controla la forma del kernel (por defecto <code>a = -0.5</code>, conocido como <strong>Bicubic de Catmull-Rom</strong>).</p>
</li>
<li><p>Si <code>x &lt; 1</code>, el peso es mayor (vecinos más cercanos).</p>
</li>
<li><p>Si <code>1 ≤ x &lt; 2</code>, el peso decae suavemente.</p>
</li>
<li><p>Si <code>x ≥ 2</code>, el peso es cero (no contribuye).</p>
</li>
</ul>
</li>
<li><p><strong>Mapeo inverso y escalado:</strong><br /> Cada coordenada de salida (<code>x_out</code>, <code>y_out</code>) se mapea a una posición flotante en la imagen original (<code>x_in</code>, <code>y_in</code>), manteniendo las proporciones.</p>
</li>
<li><p><strong>Ventana de interpolación 4×4:</strong><br /> Se toman 16 píxeles vecinos alrededor de la posición mapeada.<br /> Para cada uno, se calculan los pesos cúbicos <code>wx</code> y <code>wy</code> y su producto <code>w = wx * wy</code>.</p>
</li>
<li><p><strong>Suma ponderada:</strong><br /> Cada píxel vecino contribuye proporcionalmente a su peso.<br /> El resultado final se limita con <code>np.clip()</code> al rango [0, 255] para asegurar valores válidos de intensidad.</p>
</li>
</ol>
<h4 id="heading-caracteristicas-y-efectos-visuales">Características y efectos visuales</h4>
<ul>
<li><p><strong>Suavidad:</strong> las transiciones de color son más naturales que con interpolación bilineal.</p>
</li>
<li><p><strong>Nitidez:</strong> mantiene mejor los bordes y detalles finos, evitando el efecto borroso.</p>
</li>
<li><p><strong>Costo computacional:</strong> más lento, ya que se evalúan 16 píxeles por cada nuevo píxel.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760911531227/bb71883e-91ed-4b42-9107-94656ae94838.png" alt class="image--center mx-auto" /></p>
<p>Bilineal (izq.) · Bicúbica (centro)</p>
<p>Este método es ideal cuando se busca una <strong>alta calidad visual</strong>, por ejemplo, en procesamiento fotográfico o en tareas donde la textura y el detalle son importantes.</p>
<hr />
<h2 id="heading-4-interpolacion-lanczos">4. Interpolación Lanczos</h2>
<p>La <strong>interpolación Lanczos</strong> es una de las técnicas más precisas y sofisticadas para el redimensionamiento de imágenes. Está basada en la <strong>función sinc</strong>, la cual representa el <strong>filtro de reconstrucción ideal</strong> en el dominio de la frecuencia. En teoría, este filtro preserva toda la información posible durante el reescalado, evitando aliasing y manteniendo los bordes definidos.</p>
<p>Sin embargo, la función sinc tiene soporte infinito, lo que la hace impráctica para implementaciones reales. Por ello, Lanczos propuso una versión <strong>ventaneada</strong> de la sinc —es decir, truncada mediante otra sinc— que conserva sus propiedades principales pero con un alcance finito controlado por el parámetro <code>a</code>.</p>
<h3 id="heading-fundamento-teorico">Fundamento teórico</h3>
<p>El kernel de Lanczos se define como:</p>
<p>$$L(x) = \begin{cases} sinc(x) \cdot sinc\!\left(\dfrac{x}{a}\right), &amp; |x| &lt; a \\[8pt] 0, &amp; \text{en otro caso} \end{cases}$$</p><p>donde a (comúnmente 2 o 3) determina el tamaño del soporte:</p>
<ul>
<li><p>Un valor mayor de <code>a</code> produce resultados más suaves y detallados, pero aumenta el costo computacional.</p>
</li>
<li><p>Un valor menor reduce el costo, pero puede generar aliasing.</p>
</li>
</ul>
<p>La multiplicación de dos funciones sinc actúa como una <strong>ventana suavizadora</strong>, que atenúa las oscilaciones de alta frecuencia y evita los artefactos típicos de los métodos más simples.</p>
<hr />
<h3 id="heading-implementacion-en-python-3">Implementación en Python</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sinc</span>(<span class="hljs-params">x</span>):</span>
    <span class="hljs-keyword">return</span> np.sinc(x)  <span class="hljs-comment"># np.sinc ya incluye π, usa sin(πx)/(πx)</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lanczos_kernel</span>(<span class="hljs-params">a=<span class="hljs-number">3</span>, size=<span class="hljs-number">1000</span></span>):</span>
    <span class="hljs-string">"""Precalcula el kernel de Lanczos en un rango continuo"""</span>
    x = np.linspace(-a+<span class="hljs-number">1</span>, a<span class="hljs-number">-1</span>, size)
    k = sinc(x) * sinc(x / a)
    k[np.abs(x) &gt;= a] = <span class="hljs-number">0</span>
    <span class="hljs-keyword">return</span> k

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">resize_lanczos_fast</span>(<span class="hljs-params">image, new_width, new_height, a=<span class="hljs-number">3</span></span>):</span>
    h_in, w_in = image.shape[:<span class="hljs-number">2</span>]
    h_out, w_out = new_height, new_width

    scale_x = w_in / w_out
    scale_y = h_in / h_out

    <span class="hljs-comment"># Precalcular posiciones en la imagen original</span>
    x_coords = (np.arange(w_out) + <span class="hljs-number">0.5</span>) * scale_x - <span class="hljs-number">0.5</span>
    y_coords = (np.arange(h_out) + <span class="hljs-number">0.5</span>) * scale_y - <span class="hljs-number">0.5</span>

    <span class="hljs-comment"># Precalcular los índices y pesos para X</span>
    x_idx = np.floor(x_coords).astype(int)
    x_weights = np.zeros((w_out, <span class="hljs-number">2</span>*a))
    <span class="hljs-keyword">for</span> i, xc <span class="hljs-keyword">in</span> enumerate(x_coords):
        <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> range(-a+<span class="hljs-number">1</span>, a+<span class="hljs-number">1</span>):
            idx = min(max(x_idx[i] + n, <span class="hljs-number">0</span>), w_in - <span class="hljs-number">1</span>)
            x_weights[i, n + a - <span class="hljs-number">1</span>] = sinc(xc - (x_idx[i] + n)) * sinc((xc - (x_idx[i] + n)) / a)

    <span class="hljs-comment"># Normalizar pesos</span>
    x_weights /= np.sum(x_weights, axis=<span class="hljs-number">1</span>, keepdims=<span class="hljs-literal">True</span>)

    <span class="hljs-comment"># Paso 1: interpolación horizontal</span>
    tmp = np.zeros((h_in, w_out, image.shape[<span class="hljs-number">2</span>]), dtype=np.float32)
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(w_out):
        <span class="hljs-keyword">for</span> n <span class="hljs-keyword">in</span> range(-a+<span class="hljs-number">1</span>, a+<span class="hljs-number">1</span>):
            idx = np.clip(x_idx[i] + n, <span class="hljs-number">0</span>, w_in<span class="hljs-number">-1</span>)
            tmp[:, i] += image[:, idx] * x_weights[i, n + a - <span class="hljs-number">1</span>]

    <span class="hljs-comment"># Precalcular los índices y pesos para Y</span>
    y_idx = np.floor(y_coords).astype(int)
    y_weights = np.zeros((h_out, <span class="hljs-number">2</span>*a))
    <span class="hljs-keyword">for</span> j, yc <span class="hljs-keyword">in</span> enumerate(y_coords):
        <span class="hljs-keyword">for</span> m <span class="hljs-keyword">in</span> range(-a+<span class="hljs-number">1</span>, a+<span class="hljs-number">1</span>):
            idy = min(max(y_idx[j] + m, <span class="hljs-number">0</span>), h_in - <span class="hljs-number">1</span>)
            y_weights[j, m + a - <span class="hljs-number">1</span>] = sinc(yc - (y_idx[j] + m)) * sinc((yc - (y_idx[j] + m)) / a)

    <span class="hljs-comment"># Normalizar pesos</span>
    y_weights /= np.sum(y_weights, axis=<span class="hljs-number">1</span>, keepdims=<span class="hljs-literal">True</span>)

    <span class="hljs-comment"># Paso 2: interpolación vertical</span>
    output = np.zeros((h_out, w_out, image.shape[<span class="hljs-number">2</span>]), dtype=np.float32)
    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(h_out):
        <span class="hljs-keyword">for</span> m <span class="hljs-keyword">in</span> range(-a+<span class="hljs-number">1</span>, a+<span class="hljs-number">1</span>):
            idy = np.clip(y_idx[j] + m, <span class="hljs-number">0</span>, h_in<span class="hljs-number">-1</span>)
            <span class="hljs-comment">#output[j] += tmp[idy, :, :] * y_weights[j, m + a - 1][:, None]</span>
            output[j] += tmp[idy, :, :] * y_weights[j, m + a - <span class="hljs-number">1</span>]


    <span class="hljs-keyword">return</span> np.clip(output, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>).astype(np.uint8)
</code></pre>
<hr />
<h4 id="heading-explicacion-del-algoritmo-1">Explicación del algoritmo</h4>
<ol>
<li><p><strong>Función sinc y ventana:</strong><br /> La función <code>np.sinc(x)</code> ya implementa la forma sin(πx)/(πx)..<br /> El kernel de Lanczos se obtiene multiplicando dos sinc: una principal y otra escalada por <code>a</code>.</p>
</li>
<li><p><strong>Separabilidad:</strong><br /> La interpolación Lanczos es separable, por lo que se puede aplicar primero en la dirección <em>x</em> y luego en <em>y</em>.<br /> Esto reduce drásticamente el costo computacional de una versión completamente 2D.</p>
</li>
<li><p><strong>Pesos precalculados:</strong><br /> Para optimizar el cálculo, los pesos de interpolación se precalculan tanto para las coordenadas horizontales como verticales, normalizándose después para conservar energía (sumen 1).</p>
</li>
<li><p><strong>Interpolación en dos pasos:</strong></p>
<ul>
<li><p><strong>Paso horizontal:</strong> se genera una imagen temporal <code>tmp</code> interpolando a lo largo de las columnas.</p>
</li>
<li><p><strong>Paso vertical:</strong> se interpola <code>tmp</code> a lo largo de las filas para obtener la imagen final.</p>
</li>
</ul>
</li>
</ol>
<hr />
<h4 id="heading-caracteristicas-visuales-y-rendimiento">Características visuales y rendimiento</h4>
<ul>
<li><p><strong>Calidad sobresaliente:</strong> preserva detalles, bordes y textura con mínima pérdida visual.</p>
</li>
<li><p><strong>Sin aliasing:</strong> evita artefactos comunes en reducciones de tamaño.</p>
</li>
<li><p><strong>Costo computacional alto:</strong> debido a los cálculos del kernel sinc, aunque puede optimizarse mediante tablas precalculadas o procesamiento vectorizado.</p>
</li>
</ul>
<p>El parámetro <code>a</code> actúa como control de calidad y rendimiento:</p>
<ul>
<li><p><code>a = 2</code>: resultado más rápido, menos preciso.</p>
</li>
<li><p><code>a = 3</code>: equilibrio ideal entre detalle y costo (valor usado en esta implementación).</p>
</li>
<li><p><code>a &gt; 3</code>: mejora marginal de calidad, pero con un aumento notable en el tiempo de cálculo.</p>
</li>
</ul>
<p>En una comparación lado a lado con los métodos anteriores (Bilinear y Bicubic), la interpolación Lanczos muestra:</p>
<ul>
<li><p>Bordes más definidos sin escalones visibles.</p>
</li>
<li><p>Preservación de texturas finas.</p>
</li>
<li><p>Menor suavizado artificial en áreas con patrones repetitivos.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760913757581/1a3e4f22-eda1-465f-b2b9-3079e3d115e2.jpeg" alt="Bilineal (izq.) · Bicúbica (centro) · Lanczos (der.)" class="image--center mx-auto" /></p>
<p>Bilineal (izq.) · Bicúbica (centro) · Lanczos (der.)</p>
<p>Por ello, este método suele ser la elección preferida en aplicaciones de <strong>procesamiento fotográfico profesional</strong>, <strong>restauración de imágenes</strong> o <strong>redimensionamiento de dataset de alta calidad</strong>.</p>
<h2 id="heading-5-limitaciones-de-la-interpolacion-y-el-camino-hacia-la-superresolucion">5. Limitaciones de la interpolación y el camino hacia la superresolución</h2>
<p>Las técnicas de interpolación tradicionales —como el <strong>vecino más cercano</strong>, <strong>bilineal</strong>, <strong>bicúbica</strong> o <strong>Lanczos</strong>— parten de un supuesto fundamental:</p>
<blockquote>
<p>La imagen original contiene suficiente información para estimar razonablemente los valores intermedios.</p>
</blockquote>
<p>En otras palabras, estos métodos <strong>no crean información nueva</strong>, solo estiman valores faltantes a partir de los píxeles existentes.</p>
<h4 id="heading-perdida-de-informacion-al-escalar">Pérdida de información al escalar</h4>
<p>Cuando una imagen se amplía significativamente (por ejemplo, 4× o más), el proceso de interpolación comienza a evidenciar sus límites:</p>
<ul>
<li><p><strong>Difuminado generalizado:</strong> los bordes pierden nitidez progresivamente.</p>
</li>
<li><p><strong>Pérdida de texturas finas:</strong> patrones como cabello, pasto o piel se suavizan hasta volverse irreconocibles.</p>
</li>
<li><p><strong>Artefactos de interpolación:</strong> algunos métodos producen halos, aliasing o patrones ondulados (especialmente en bicúbica o Lanczos).</p>
</li>
</ul>
<p>Esto ocurre porque el algoritmo no puede inferir <strong>frecuencias espaciales que no existen</strong> en la imagen original: una textura de alta frecuencia se pierde si no está codificada en los píxeles.</p>
<p>Por tanto, el límite fundamental de la interpolación clásica está dado por el <strong>teorema de muestreo de Nyquist</strong>: no se pueden reconstruir detalles más finos que la mitad de la frecuencia máxima presente en los datos originales.</p>
<h2 id="heading-6-de-la-interpolacion-al-aprendizaje-profundo">6. De la interpolación al aprendizaje profundo</h2>
<p>Ante estas limitaciones surgieron las técnicas de <strong>superresolución (SR)</strong>, cuyo objetivo es <strong>reconstruir detalles plausibles</strong> en imágenes de baja resolución.<br />A diferencia de la interpolación, que se basa en operaciones matemáticas locales, la superresolución utiliza <strong>redes neuronales profundas</strong> para aprender cómo debería lucir una imagen de alta resolución.</p>
<p>Modelos como <strong>SRCNN</strong>, <strong>EDSR</strong> o <strong>ESRGAN</strong> han demostrado que las redes pueden generar resultados con bordes definidos y texturas más naturales, al aprovechar el conocimiento aprendido de millones de imágenes.</p>
<p>En lugar de estimar píxeles faltantes mediante promedios, estas redes <strong>reconstruyen patrones visuales</strong>: detalles en cabello, piel, texto o superficies que las técnicas clásicas simplemente no pueden recuperar.<br />En artículos posteriores planeo abordar estas técnicas de superresolución con mayor profundidad, ya que, con la actual explosión de la <strong>IA generativa</strong>, han surgido numerosos modelos y arquitecturas especialmente interesantes que merecen análisis detallado.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[Introducción al Texture Mapping en OpenGL.]]></title><description><![CDATA[En el desarrollo de gráficos 3D en tiempo real, uno de los principales retos es alcanzar realismo visual sin sacrificar rendimiento. Los motores gráficos modernos abordan este problema mediante técnicas como la iluminación, el sombreado, la optimizac...]]></description><link>https://codigoenllamas.com/introduccion-al-texture-mapping-en-opengl</link><guid isPermaLink="true">https://codigoenllamas.com/introduccion-al-texture-mapping-en-opengl</guid><category><![CDATA[C++]]></category><category><![CDATA[openGL]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[graphics]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sun, 12 Oct 2025 19:48:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/MnUnDK7D69g/upload/fd8652786853abddb571133ad290562f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En el desarrollo de <strong>gráficos 3D en tiempo real</strong>, uno de los principales retos es alcanzar <strong>realismo visual</strong> sin sacrificar rendimiento. Los motores gráficos modernos abordan este problema mediante técnicas como la <strong>iluminación</strong>, el <strong>sombreado</strong>, la optimización de la geometría y, de forma central, el <strong>texture mapping</strong> o mapeo de texturas.</p>
<p>Introducido por <strong>Jim Blinn en 1978</strong>, el texture mapping es una técnica esencial del <strong>pipeline gráfico</strong> que permite aplicar <strong>imágenes 2D (texturas)</strong> sobre <strong>mallas tridimensionales</strong> mediante <strong>coordenadas UV</strong>. En APIs gráficas como <strong>OpenGL</strong>, estas coordenadas son utilizadas por la <strong>GPU</strong> para muestrear la textura en el <strong>fragment shader</strong>, haciendo posible representar materiales complejos —como madera, ladrillo o metal— sin incrementar el número de polígonos.</p>
<p>Más allá de la simple asignación de una imagen, el texturizado moderno incorpora técnicas clave como el <strong>mipmapping</strong>, los <strong>filtros de textura</strong> (bilineal, trilineal y <strong>anisotrópico</strong>), los modos de <strong>wrapping y tiling</strong>, y la <strong>corrección de distorsión por perspectiva</strong>, todas orientadas a mejorar la calidad visual, la estabilidad de la imagen y la eficiencia del renderizado.</p>
<p>Este artículo presenta una introducción práctica al <strong>texture mapping en OpenGL</strong>, abordando tanto sus fundamentos como las extensiones más importantes que permiten obtener un texturizado robusto, eficiente y visualmente coherente en aplicaciones gráficas modernas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760297196877/491efb5a-7172-4a50-86e2-4058cb9d7d1a.png" alt class="image--center mx-auto" /></p>
<p>El <strong><em>texture mapping</em></strong> actúa como un puente entre el mundo bidimensional de las imágenes y el tridimensional de los modelos, aportando realismo y eficiencia al proceso de renderizado.</p>
<p>Las principales ventajas de esta técnica son:</p>
<ul>
<li><p><strong>Mayor realismo visual:</strong> las texturas aportan color, detalle y variaciones naturales en la superficie.</p>
</li>
<li><p><strong>Mejor rendimiento:</strong> se reduce la complejidad geométrica del modelo, lo que permite procesar escenas más grandes en menos tiempo.</p>
</li>
<li><p><strong>Versatilidad:</strong> posibilita combinar distintos tipos de mapas (de color, normales, especular, entre otros) para simular materiales complejos.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760298204235/871bcebc-e242-4866-8b06-6723e7b36499.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-soporte-en-hardware-texture-units">Soporte en hardware: <em>Texture Units</em></h2>
<p>Las <strong>GPU modernas</strong> incluyen unidades especializadas llamadas <strong>Texture Units</strong>, diseñadas para almacenar y procesar texturas directamente en el hardware.<br />Gracias a ellas, un mismo objeto puede usar varios mapas —como color base (<em>diffuse</em>), normales (<em>normal map</em>) o brillo (<em>specular map</em>)— sin afectar el rendimiento.</p>
<p>En <strong>OpenGL</strong>, estas unidades se controlan mediante funciones como:</p>
<pre><code class="lang-c++">glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, brickTexture);
</code></pre>
<p>Aquí se activa la unidad de textura 0 y se vincula la textura correspondiente.<br />En el <em>fragment shader</em>, esta textura se accede a través de un <strong>sampler</strong>, que actúa como un enlace entre la unidad y el <em>shader</em>:</p>
<pre><code class="lang-c++">layout(binding = <span class="hljs-number">0</span>) uniform sampler2D samp;
vec4 color = texture(samp, texCoords);
</code></pre>
<p>De esta forma, cada fragmento obtiene su color desde la textura asociada, lo que permite representar materiales complejos con geometría mínima. El <em>texture mapping</em> es, por tanto, una de las bases del realismo visual en OpenGL: combina eficiencia de renderizado con un alto nivel de detalle.</p>
<hr />
<h2 id="heading-componentes-del-texture-mapping">Componentes del <em>Texture Mapping</em></h2>
<p>Aplicar texturas correctamente en OpenGL requiere comprender cómo interactúan tres elementos clave: el <strong>objeto de textura</strong>, las <strong>coordenadas de textura</strong> y las <strong>unidades de textura</strong>.</p>
<h3 id="heading-texture-object">Texture Object</h3>
<p>Un <strong>texture object</strong> almacena toda la información de una textura: imagen, formato, filtros, envoltura y niveles de <em>mipmap</em>.</p>
<p>Se crea y vincula con:</p>
<pre><code class="lang-c++">GLuint texID; glGenTextures(<span class="hljs-number">1</span>, &amp;texID);
glBindTexture(GL_TEXTURE_2D, texID);
</code></pre>
<p>Una vez vinculado, cualquier configuración o carga de datos afectará a ese objeto.</p>
<h3 id="heading-coordenadas-de-textura-uv">Coordenadas de textura (UV)</h3>
<p>Cada vértice del modelo tiene asociadas <strong>coordenadas de textura (s, t)</strong> que indican qué parte de la imagen se aplica sobre él. Estas se almacenan en un <strong>VBO</strong> y se asocian al <em>shader</em> mediante atributos de vértice:</p>
<pre><code class="lang-c++">glGenBuffers(<span class="hljs-number">1</span>, &amp;texCoordBuffer);
glBindBuffer(GL_ARRAY_BUFFER, texCoordBuffer); glBufferData(GL_ARRAY_BUFFER, <span class="hljs-keyword">sizeof</span>(texCoords), texCoords, GL_STATIC_DRAW);  glEnableVertexAttribArray(<span class="hljs-number">1</span>); glVertexAttribPointer(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, GL_FLOAT, GL_FALSE, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
</code></pre>
<p>En el <em>shader</em> correspondiente:</p>
<pre><code class="lang-c++">layout(location = <span class="hljs-number">1</span>) in vec2 texCoord;
</code></pre>
<h3 id="heading-texture-units">Texture Units</h3>
<p>Las <strong>texture units</strong> son espacios de memoria dedicados dentro de la GPU que permiten usar múltiples texturas simultáneamente. Cada <em>sampler</em> en el <em>shader</em> se vincula a una de estas unidades mediante su <em>binding</em>.</p>
<p>Por ejemplo:</p>
<pre><code class="lang-c++">glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
</code></pre>
<p>OpenGL garantiza al menos <strong>16 unidades</strong> por defecto, aunque la mayoría de las GPU actuales soportan muchas más, lo que permite combinar texturas de color, normales, o reflexión en un mismo objeto.</p>
<p>En conjunto, estos componentes —el objeto de textura, las coordenadas UV y las unidades de textura— forman el núcleo del <strong><em>texture mapping</em></strong>.</p>
<p>Comprender su interacción es esencial para aprovechar el potencial del pipeline gráfico antes de pasar a la etapa práctica, donde las texturas se cargan desde archivos mediante librerías como <strong>SOIL2</strong> y se aplican directamente a los modelos 3D.</p>
<hr />
<h2 id="heading-carga-de-texturas-con-soil2"><strong>Carga de texturas con SOIL2</strong></h2>
<p>Una vez configurados los elementos básicos del <em>texture mapping</em>, el siguiente paso es <strong>cargar imágenes desde archivos</strong> para convertirlas en texturas utilizables por OpenGL. Aunque es posible hacerlo manualmente con las funciones nativas, resulta más práctico y eficiente emplear una librería como <strong>SOIL2 (Simple OpenGL Image Library 2)</strong>. SOIL2 se encarga de leer los archivos de imagen, decodificarlos y transferirlos directamente a la memoria de la GPU en formato de textura.</p>
<h3 id="heading-integracion-de-soil2">Integración de SOIL2</h3>
<p>Para usar SOIL2 basta con incluir su encabezado y enlazar la librería:</p>
<p><code>#include &lt;SOIL2/SOIL2.h&gt;</code></p>
<p>Una vez integrada, se puede cargar cualquier formato de imagen compatible (.jpg, .png, .bmp, .tiff, .gif, etc.) con la función <code>SOIL_load_OGL_texture()</code>, que realiza todo el proceso de forma automática.</p>
<h3 id="heading-funcion-utilsloadtexture">Función <code>Utils::loadTexture()</code></h3>
<p>La siguiente función implementa la carga de texturas mediante SOIL2 dentro de una clase de utilidades (<code>Utils.cpp</code>). Encapsula la lectura del archivo, la generación de mipmaps y la configuración del filtrado anisotrópico:</p>
<pre><code class="lang-c++"><span class="hljs-comment">// Carga de textura con SOIL2 y configuración avanzada</span>
<span class="hljs-function">GLuint <span class="hljs-title">Utils::loadTexture</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *texImagePath)</span>
</span>{
    GLuint textureRef = SOIL_load_OGL_texture(
        texImagePath,            <span class="hljs-comment">// Ruta del archivo</span>
        SOIL_LOAD_AUTO,          <span class="hljs-comment">// Detecta formato automáticamente</span>
        SOIL_CREATE_NEW_ID,      <span class="hljs-comment">// Crea un nuevo ID de textura</span>
        SOIL_FLAG_INVERT_Y       <span class="hljs-comment">// Invierte el eje Y (compatibilidad con OpenGL)</span>
    );

    <span class="hljs-comment">// Verificación de errores</span>
    <span class="hljs-keyword">if</span> (textureRef == <span class="hljs-number">0</span>)
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"No se encontró el archivo de textura: "</span> &lt;&lt; texImagePath &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;

    <span class="hljs-comment">// --- Configuración de mipmaps y filtrado ---</span>
    glBindTexture(GL_TEXTURE_2D, textureRef);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);

    <span class="hljs-comment">// Filtrado anisotrópico (si está disponible)</span>
    <span class="hljs-keyword">if</span> (isExtensionSupported(<span class="hljs-string">"GL_EXT_texture_filter_anisotropic"</span>)) {
        GLfloat maxAniso = <span class="hljs-number">0.0f</span>;
        glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &amp;maxAniso);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, maxAniso);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"Filtrado anisotrópico no soportado"</span> &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;
    }

    <span class="hljs-keyword">return</span> textureRef;
}
</code></pre>
<h3 id="heading-desglose-del-proceso">Desglose del proceso</h3>
<ol>
<li><p><strong>Carga de la imagen:</strong><br /> <code>SOIL_load_OGL_texture()</code> abre el archivo, decodifica la imagen y genera automáticamente una textura 2D en la GPU. El uso de <code>SOIL_FLAG_INVERT_Y</code> es necesario porque muchas imágenes tienen el origen en la esquina superior izquierda, mientras que OpenGL lo maneja desde la inferior izquierda.</p>
</li>
<li><p><strong>Verificación de errores:</strong><br /> Si la carga falla, la función devuelve <code>0</code> y se muestra un mensaje en consola.</p>
</li>
<li><p><strong>Generación de mipmaps:</strong><br /> Los <strong>mipmaps</strong> son versiones reducidas de la textura que OpenGL selecciona según la distancia del objeto a la cámara, mejorando el rendimiento y la calidad visual.</p>
</li>
<li><p><strong>Filtrado anisotrópico:</strong><br /> Aumenta la nitidez de las texturas vistas en ángulos oblicuos. Si la extensión <code>GL_EXT_texture_filter_anisotropic</code> está disponible, se aplica el máximo nivel permitido por la GPU.</p>
</li>
</ol>
<p>Con esta función, cargar una textura se reduce a una sola línea:</p>
<pre><code class="lang-c++">GLuint brickTexture = Utils::loadTexture(<span class="hljs-string">"brick1.jpg"</span>);
</code></pre>
<p>En una sola llamada, se realiza todo el proceso: lectura del archivo, creación de la textura, generación de mipmaps y configuración del filtrado. Este enfoque mantiene el código <strong>modular, limpio y reutilizable</strong>, facilitando la gestión de texturas en cualquier proyecto OpenGL.</p>
<h2 id="heading-ejemplo">Ejemplo</h2>
<p>El siguiente programa muestra el flujo esencial del <em>texture mapping</em>: <strong>carga, asociación y muestreo</strong> de una textura sobre un modelo 3D.<br />Utiliza <strong>SOIL</strong> para cargar la imagen y <strong>OpenGL 4.3</strong> para gestionar los <em>buffers</em> y <em>shaders</em>.</p>
<p>Los <strong>buffers</strong> (VAO y VBO) almacenan la información necesaria para el renderizado: los <strong>vértices</strong> del cubo y sus <strong>coordenadas de textura</strong>, que van de <code>(0.0, 0.0)</code> a <code>(1.0, 1.0)</code>. Estas coordenadas indican qué parte de la imagen corresponde a cada vértice, definiendo cómo se “mapea” la textura sobre la superficie del modelo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760296354153/e2f08923-8189-43fa-9961-77efbf356a50.jpeg" alt class="image--center mx-auto" /></p>
<p>Los <strong>shaders</strong> procesan esa información directamente en la GPU:</p>
<ul>
<li><p>El <strong>vertex shader</strong> transforma los vértices del modelo al espacio de proyección y pasa las coordenadas UV al siguiente paso.</p>
</li>
<li><p>El <strong>fragment shader</strong> usa esas coordenadas para <strong>muestrear la textura</strong> mediante la función <code>texture()</code>, determinando el color final de cada fragmento.</p>
</li>
</ul>
<p>De este modo, se aplica una imagen a un cubo con gran realismo visual y un costo computacional mínimo.</p>
<h3 id="heading-resultado">Resultado</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760296833360/f3b6a40c-8782-41ef-ba92-9678f602f6ab.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-mipmapping">Mipmapping</h2>
<p>Cuando una textura se visualiza a distintas distancias de la cámara, su resolución efectiva cambia. Si siempre se utilizara la textura original, OpenGL tendría que muestrear muchos texels que finalmente terminan contribuyendo a un solo fragmento, lo que produce <strong>aliasing</strong>, parpadeos y pérdida de estabilidad visual.</p>
<p>El <strong>mipmapping</strong> soluciona este problema generando una pirámide de versiones reducidas de la textura. Cada nivel mipmap es una imagen con la mitad de resolución del nivel anterior, hasta llegar a una textura de 1×1 texel. Durante el renderizado, la GPU selecciona automáticamente el nivel más adecuado según el tamaño del fragmento en pantalla.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765742071173/c7d89fa7-ef90-464e-bc74-e4b3dfd8743c.png" alt class="image--center mx-auto" /></p>
<p>En OpenGL, los mipmaps pueden generarse de forma automática:</p>
<pre><code class="lang-cpp">glBindTexture(GL_TEXTURE_2D, textureID);
glGenerateMipmap(GL_TEXTURE_2D);
</code></pre>
<p>Para que OpenGL los utilice, es necesario configurar el filtro de minimización:</p>
<pre><code class="lang-cpp">glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
</code></pre>
<p>Este modo combina <strong>interpolación lineal</strong> dentro de cada mipmap y entre niveles consecutivos, logrando transiciones suaves entre resoluciones.</p>
<p>Las ventajas del mipmapping son claras:</p>
<ul>
<li><p>Reduce el aliasing y el shimmering en objetos lejanos.</p>
</li>
<li><p>Mejora el rendimiento, ya que se accede a texturas más pequeñas.</p>
</li>
<li><p>Aumenta la estabilidad visual en escenas con movimiento.</p>
</li>
</ul>
<p>Por estas razones, el uso de mipmaps no es opcional en motores gráficos modernos: es una práctica estándar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765741766987/b16025f8-0b45-48f9-af1e-c13c5adcc992.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-filtrado-anisotropico">Filtrado anisotrópico</h2>
<p>El mipmapping mejora la calidad a distancia, pero no resuelve un problema frecuente: las <strong>texturas vistas en ángulos oblicuos</strong>. En estos casos, una superficie puede cubrir muchos texels en una dirección y muy pocos en otra. Los filtros bilineales o trilineales asumen una huella cuadrada del fragmento, lo cual no refleja la realidad geométrica.</p>
<p>El <strong>filtrado anisotrópico</strong> corrige este efecto adaptando el muestreo de la textura a una huella elíptica, alineada con la proyección del fragmento sobre la superficie. El resultado es una textura mucho más nítida cuando se observa en perspectiva rasante, como suelos, carreteras o paredes largas.</p>
<p>En OpenGL, este filtrado depende de la extensión:</p>
<pre><code class="lang-cpp">GL_EXT_texture_filter_anisotropic
</code></pre>
<p>Si está disponible, se puede configurar de la siguiente manera:</p>
<pre><code class="lang-cpp">GLfloat maxAniso;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &amp;maxAniso);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, maxAniso);
</code></pre>
<p>El valor indica cuántas muestras adicionales puede tomar la GPU en la dirección dominante. Cuanto mayor sea, mejor será la calidad, aunque con un ligero costo adicional.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765742679012/ca9c1b08-1749-4c07-8d6a-0045d023c9b6.jpeg" alt class="image--center mx-auto" /></p>
<p>En la práctica, el filtrado anisotrópico ofrece una de las <strong>mejores mejoras visuales por costo computacional</strong>, por lo que suele activarse siempre que el hardware lo permita.</p>
<h2 id="heading-wrapping-y-tiling-de-texturas">Wrapping y Tiling de texturas</h2>
<p>Las coordenadas de textura normalmente se definen en el rango <code>[0, 1]</code>. Sin embargo, es común que estas coordenadas excedan ese intervalo, ya sea intencionalmente (para repetir una textura) o por la forma en que se modela la geometría.</p>
<p>El <strong>wrapping</strong> define cómo OpenGL maneja las coordenadas fuera del rango válido. Los modos más comunes son:</p>
<ul>
<li><p><code>GL_REPEAT</code>: la textura se repite indefinidamente.</p>
</li>
<li><p><code>GL_MIRRORED_REPEAT</code>: se repite invirtiéndose en cada ciclo.</p>
</li>
<li><p><code>GL_CLAMP_TO_EDGE</code>: se extiende el borde de la textura.</p>
</li>
<li><p><code>GL_CLAMP_TO_BORDER</code>: se usa un color constante en los bordes.</p>
</li>
</ul>
<p>Ejemplo de configuración:</p>
<pre><code class="lang-cpp">glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
</code></pre>
<p>El <strong>tiling</strong> es una consecuencia directa del wrapping. Si las coordenadas UV se escalan más allá de <code>[0,1]</code>, la textura se repite múltiples veces sobre la superficie:</p>
<pre><code class="lang-cpp">vec2 tiledUV = texCoord * <span class="hljs-number">4.0</span>;
vec4 color = texture(samp, tiledUV);
</code></pre>
<p>Esta técnica es especialmente útil para suelos, paredes o terrenos, donde una textura pequeña puede cubrir grandes áreas sin aumentar el uso de memoria.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765742994881/5a3ed07e-5b9e-4242-8191-58395377178b.png" alt class="image--center mx-auto" /></p>
<p>El control adecuado del wrapping y el tiling es esencial para evitar costuras visibles y patrones repetitivos poco realistas.</p>
<hr />
<h2 id="heading-correccion-de-distorsion-por-perspectiva">Corrección de distorsión por perspectiva</h2>
<p>Un error conceptual común es asumir que la interpolación de coordenadas UV es siempre correcta. En realidad, si la interpolación se realiza de forma lineal en espacio de pantalla, se produce una <strong>distorsión por perspectiva</strong>, visible como estiramientos o deslizamientos incorrectos de la textura en superficies inclinadas.</p>
<p>La solución es la <strong>perspective-correct interpolation</strong>, que tiene en cuenta el valor <code>w</code> del espacio homogéneo al interpolar las coordenadas. Afortunadamente, en OpenGL moderno esta corrección se realiza automáticamente cuando se usan shaders estándar y un pipeline correcto.</p>
<p>Internamente, la GPU interpola las coordenadas como:</p>
<p>$$\frac{u}{w}, \frac{v}{w}$$</p><p>y luego reconstruye el valor correcto por fragmento. Esto garantiza que la textura se proyecte correctamente en superficies tridimensionales.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765743147769/c7333590-2442-4618-ad0a-d112b8875382.png" alt class="image--center mx-auto" /></p>
<p>Solo en casos especiales —como el uso de interpoladores personalizados o técnicas de rasterización manual— es posible desactivar esta corrección, pero hacerlo casi siempre conduce a errores visuales evidentes.</p>
<p>Por tanto, la corrección de perspectiva no es una opción avanzada, sino un <strong>requisito fundamental</strong> para un texturizado físicamente coherente.</p>
<h2 id="heading-conclusion">Conclusión</h2>
<p>El texture mapping constituye uno de los pilares fundamentales del renderizado moderno en OpenGL. A través de la asignación de coordenadas UV y el muestreo de imágenes en el fragment shader, es posible representar superficies visualmente ricas sin incrementar la complejidad geométrica de los modelos, logrando una combinación óptima entre realismo y rendimiento.</p>
<p>Sin embargo, la calidad final del texturizado no depende únicamente de aplicar una imagen sobre una malla. Técnicas complementarias como el <strong>mipmapping</strong> permiten adaptar la resolución de la textura a la distancia de la cámara, reduciendo aliasing y mejorando la estabilidad visual. El <strong>filtrado anisotrópico</strong> refina este proceso al preservar el detalle en superficies observadas en ángulos oblicuos, uno de los casos más críticos en escenas tridimensionales.</p>
<p>Por otro lado, el control del <strong>wrapping y tiling</strong> ofrece flexibilidad para reutilizar texturas y cubrir grandes superficies de forma eficiente, mientras que la <strong>corrección de distorsión por perspectiva</strong>, aplicada automáticamente en el pipeline moderno, garantiza que la interpolación de las coordenadas UV sea geométricamente coherente con la proyección 3D.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Computer-Graphics-Programming">https://github.com/Nobody-1321/Computer-Graphics-Programming</a></div>
]]></content:encoded></item><item><title><![CDATA[Buffers y Uniforms en OpenGL]]></title><description><![CDATA[En la computación gráfica, el pipeline es simplemente el camino que siguen los datos hasta convertirse en los píxeles que vemos en pantalla. En un artículo anterior conté de manera general cómo funciona este flujo; ahora veremos cómo se aplica dentro...]]></description><link>https://codigoenllamas.com/buffers-y-uniforms-en-opengl</link><guid isPermaLink="true">https://codigoenllamas.com/buffers-y-uniforms-en-opengl</guid><category><![CDATA[openGL]]></category><category><![CDATA[C++]]></category><category><![CDATA[graphics]]></category><category><![CDATA[3d]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sat, 04 Oct 2025 19:54:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/0OYaO7mvT8k/upload/ac74d055652023d357d3b21fa15624c5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En la computación gráfica, el <strong>pipeline</strong> es simplemente el camino que siguen los datos hasta convertirse en los píxeles que vemos en pantalla. En un artículo anterior conté de manera general cómo funciona este flujo; ahora veremos cómo se aplica dentro de OpenGL.</p>
<p>En OpenGL, la aplicación no dibuja los píxeles directamente. Lo que hacemos es <strong>enviar datos al pipeline</strong> —posiciones de vértices, colores, texturas, transformaciones— y la GPU se encarga de procesarlos paso a paso hasta producir la imagen final en el framebuffer.</p>
<p>A grandes rasgos, el pipeline de OpenGL pasa por estas etapas:</p>
<ul>
<li><p><strong>Aplicación (CPU):</strong> desde C++ (u otro lenguaje) definimos la geometría, configuramos buffers y cargamos los shaders.</p>
</li>
<li><p><strong>Vertex Shader:</strong> transforma cada vértice aplicando las matrices de modelo, vista y proyección.</p>
</li>
<li><p><strong>Ensamblaje de primitivas:</strong> los vértices se agrupan en puntos, líneas o triángulos.</p>
</li>
<li><p><strong>Rasterización:</strong> esas primitivas se convierten en fragmentos (los candidatos a ser píxeles), interpolando atributos como color o coordenadas de textura.</p>
</li>
<li><p><strong>Fragment Shader:</strong> decide el color de cada fragmento, aplicando iluminación o texturas.</p>
</li>
<li><p><strong>Pruebas y mezcla:</strong> antes de dibujar el píxel final, se aplican pruebas (profundidad, stencil) y se combinan colores con el fondo si es necesario.</p>
</li>
</ul>
<p>En las siguientes secciones veremos cómo todo esto se traduce en la práctica con OpenGL: cómo enviamos datos a la GPU, cómo se guardan en buffers y cómo los shaders los usan para construir una escena 3D.</p>
<h2 id="heading-2-mecanismos-de-envio-de-datos-buffers-vs-uniforms">2. Mecanismos de Envío de Datos: Buffers vs. Uniforms</h2>
<p>Para que el pipeline de OpenGL funcione, necesitamos <strong>pasarle datos</strong> desde nuestra aplicación en C++. Estos datos suelen dividirse en dos tipos:</p>
<ol>
<li><p><strong>Datos que varían por vértice o por fragmento</strong> (por ejemplo, la posición de un vértice o su color).</p>
</li>
<li><p><strong>Datos que permanecen constantes para todos los vértices o fragmentos de una misma invocación de renderizado</strong> (por ejemplo, una matriz de transformación o la posición de la cámara).</p>
</li>
</ol>
<p>OpenGL ofrece dos mecanismos distintos para enviar esta información a los shaders: <strong>buffers</strong> y <strong>uniforms</strong>.</p>
<hr />
<h3 id="heading-21-buffers-y-vertex-attributes">2.1 Buffers y Vertex Attributes</h3>
<p>Los <strong>Vertex Buffer Objects (VBOs)</strong> permiten guardar en la memoria de la GPU grandes cantidades de datos, como posiciones de vértices, normales, colores o coordenadas de textura. Estos datos se asocian a <strong>atributos de vértice</strong> en el <strong>vertex shader</strong>, lo que significa que cada vértice recibe sus propios valores.</p>
<p><strong>Ejemplo conceptual en GLSL:</strong></p>
<pre><code class="lang-c++">layout(location = <span class="hljs-number">0</span>) in vec3 position; <span class="hljs-comment">// posición de cada vértice</span>
layout(location = <span class="hljs-number">1</span>) in vec3 color;    <span class="hljs-comment">// color de cada vértice</span>
</code></pre>
<p>Cada llamada a <code>glDrawArrays()</code> o <code>glDrawElements()</code> hará que OpenGL recorra el buffer, entregando a la GPU un conjunto distinto de atributos para cada vértice.</p>
<hr />
<h3 id="heading-22-variables-uniformes">2.2 Variables Uniformes</h3>
<p>Las <strong>uniforms</strong> funcionan de manera diferente: son <strong>variables globales dentro de un shader</strong> y su valor se mantiene constante durante todo un draw call.<br />A diferencia de los atributos de vértice, no cambian de uno a otro.</p>
<p>Ejemplo en GLSL:</p>
<pre><code class="lang-c++">uniform mat4 mv_matrix;   <span class="hljs-comment">// matriz de modelo-vista</span>
uniform mat4 proj_matrix; <span class="hljs-comment">// matriz de proyección</span>
</code></pre>
<p>Estas variables se cargan desde la aplicación mediante funciones como <code>glGetUniformLocation()</code> y <code>glUniformMatrix4fv()</code>. Un caso típico de uso es enviar las matrices de transformación que se aplican a todos los vértices de un objeto en una misma pasada de renderizado.</p>
<h2 id="heading-3-buffers-y-vertex-attributes-vbos-y-vaos">3. Buffers y Vertex Attributes (VBOs y VAOs)</h2>
<p>En OpenGL, los datos geométricos no se envían uno por uno desde la CPU. En vez de eso, se cargan en la memoria de la GPU mediante estructuras llamadas <strong>buffers</strong>. Más adelante, estos buffers se organizan y se enlazan a los atributos de los shaders usando los <strong>Vertex Array Objects (VAOs)</strong>.</p>
<hr />
<h3 id="heading-31-vertex-buffer-object-vbo">3.1 Vertex Buffer Object (VBO)</h3>
<p>Un <strong>Vertex Buffer Object (VBO)</strong> es un bloque de memoria en la GPU que contiene datos de vértices. Por ejemplo, un cubo puede representarse como 36 vértices (12 triángulos, 3 vértices cada uno). Estos datos se cargan una única vez y luego la GPU puede acceder a ellos directamente durante el renderizado.</p>
<p>Un ejemplo típico de creación de un VBO es:</p>
<pre><code class="lang-cpp">glGenBuffers(numVBOs, vbo);  
glBindBuffer(GL_ARRAY_BUFFER, vbo[<span class="hljs-number">0</span>]);  
glBufferData(GL_ARRAY_BUFFER, <span class="hljs-keyword">sizeof</span>(vertexPositions), vertexPositions, GL_STATIC_DRAW);
</code></pre>
<ul>
<li><p><code>glGenBuffers</code>: crea un identificador de buffer.</p>
</li>
<li><p><code>glBindBuffer</code>: lo activa para operar sobre él.</p>
</li>
<li><p><code>glBufferData</code>: copia los datos desde la RAM del CPU hasta la memoria de la GPU.</p>
</li>
</ul>
<hr />
<h3 id="heading-32-vertex-array-object-vao">3.2 Vertex Array Object (VAO)</h3>
<p>El <strong>Vertex Array Object (VAO)</strong> es un contenedor que guarda la configuración de cómo se deben interpretar los datos del VBO. Esto incluye:</p>
<ul>
<li><p>Qué atributos de vértice existen (posición, color, normales, etc.).</p>
</li>
<li><p>Cómo se distribuyen en memoria (stride, offset).</p>
</li>
<li><p>Qué buffer está asociado a cada atributo.</p>
</li>
</ul>
<p>Ejemplo de creación:</p>
<pre><code class="lang-cpp">glGenVertexArrays(<span class="hljs-number">1</span>, vao);  
glBindVertexArray(vao[<span class="hljs-number">0</span>]);
</code></pre>
<p>De esta forma, cada vez que se hace <code>glBindVertexArray(vao[0])</code>, toda la configuración de atributos y buffers queda lista sin tener que repetirla.</p>
<hr />
<h3 id="heading-33-atributos-de-vertice">3.3 Atributos de Vértice</h3>
<p>Una vez que los datos están cargados en el VBO, hay que indicarle a OpenGL <strong>cómo debe entregarlos al vertex shader</strong>. Esto se hace con <code>glVertexAttribPointer</code> y <code>glEnableVertexAttribArray</code>.</p>
<p>Ejemplo</p>
<pre><code class="lang-cpp">glBindBuffer(GL_ARRAY_BUFFER, vbo[<span class="hljs-number">0</span>]);  
glVertexAttribPointer(<span class="hljs-number">0</span>, <span class="hljs-number">3</span>, GL_FLOAT, GL_FALSE, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);  
glEnableVertexAttribArray(<span class="hljs-number">0</span>);
</code></pre>
<ul>
<li><p><code>glVertexAttribPointer(0, 3, GL_FLOAT, ...)</code>: indica que el atributo en la <strong>location 0</strong> del shader corresponde a grupos de 3 floats consecutivos en el buffer (x, y, z).</p>
</li>
<li><p><code>glEnableVertexAttribArray(0)</code>: habilita el atributo de posición para que pueda usarse en el shader.</p>
</li>
</ul>
<p>En el shader se vería así:</p>
<pre><code class="lang-cpp">layout(location = <span class="hljs-number">0</span>) in vec3 position;
</code></pre>
<p>De esta forma, cada vértice del cubo se enviará automáticamente al shader en la variable <code>position</code>.</p>
<hr />
<h3 id="heading-34-flujo-de-inicializacion-vs-renderizado">3.4 Flujo de Inicialización vs. Renderizado</h3>
<p>Es importante distinguir entre:</p>
<ul>
<li><p><strong>Etapa de inicialización (</strong><code>init()</code>)</p>
<ul>
<li><p>Crear los buffers (VBOs).</p>
</li>
<li><p>Cargar los datos en ellos.</p>
</li>
<li><p>Configurar los VAOs y atributos.</p>
</li>
</ul>
</li>
<li><p><strong>Etapa de renderizado por frame (</strong><code>display()</code>)</p>
<ul>
<li><p>Limpiar los buffers de color y profundidad.</p>
</li>
<li><p>Activar el shader program.</p>
</li>
<li><p>Actualizar uniforms (por ejemplo, las matrices de transformación).</p>
</li>
<li><p>Llamar a <code>glDrawArrays()</code> o <code>glDrawElements()</code>.</p>
</li>
</ul>
</li>
</ul>
<pre><code class="lang-cpp">glDrawArrays(GL_TRIANGLES, <span class="hljs-number">0</span>, <span class="hljs-number">36</span>);
</code></pre>
<hr />
<ul>
<li><p>Los <strong>VBOs</strong> guardan los datos en la GPU.</p>
</li>
<li><p>Los <strong>VAOs</strong> almacenan la configuración de cómo leer esos datos.</p>
</li>
<li><p>Los <strong>vertex attributes</strong> conectan directamente esos datos con el shader.</p>
</li>
</ul>
<p>Entender esta relación es fundamental para trabajar con geometría en OpenGL, porque sobre esta base se añaden más atributos (colores, normales, coordenadas de textura) en escenas más complejas.</p>
<h2 id="heading-4-variables-uniformes">4. Variables Uniformes</h2>
<p>En OpenGL no todo tiene que cambiar vértice a vértice. Muchas veces necesitamos parámetros que se mantengan <strong>constantes durante toda una llamada de dibujo</strong>. Para eso existen las <strong>variables uniformes (uniforms)</strong>, que son la forma estándar de enviar datos globales desde la aplicación en C++ a los shaders.</p>
<hr />
<h3 id="heading-41-como-se-declaran-los-uniforms-en-los-shaders">4.1 Cómo se declaran los uniforms en los shaders</h3>
<p>Un <code>uniform</code> se declara en GLSL dentro del shader. A diferencia de los atributos (<code>in</code>), un uniform no cambia de un vértice a otro: su valor es el mismo para todos los vértices y fragmentos durante un draw call.</p>
<p>Ejemplo en un <strong>vertex shader</strong>:</p>
<pre><code class="lang-c++"><span class="hljs-meta">#version 430 core</span>
layout(location = <span class="hljs-number">0</span>) in vec3 position;

uniform mat4 mv_matrix;   <span class="hljs-comment">// Modelo-Vista</span>
uniform mat4 proj_matrix; <span class="hljs-comment">// Proyección</span>

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
    gl_Position = proj_matrix * mv_matrix * vec4(position, <span class="hljs-number">1.0</span>);
}
</code></pre>
<p>Aquí <code>mv_matrix</code> combina las transformaciones de modelo y vista, mientras que <code>proj_matrix</code> define la proyección (perspectiva u ortográfica).<br />Ambas matrices se aplican igual a todos los vértices en el mismo dibujo.</p>
<hr />
<h3 id="heading-42-como-se-cargan-los-uniforms-desde-c">4.2 Cómo se cargan los uniforms desde C++</h3>
<p>Una vez compilado y enlazado el programa de shaders, podemos localizar y actualizar los uniforms desde C++. Esto ocurre en dos pasos:</p>
<ol>
<li><strong>Obtener la ubicación del uniform</strong> en el programa:</li>
</ol>
<pre><code class="lang-c++">mvLoc = glGetUniformLocation(renderingProgram, <span class="hljs-string">"mv_matrix"</span>);
projLoc = glGetUniformLocation(renderingProgram, <span class="hljs-string">"proj_matrix"</span>);
</code></pre>
<blockquote>
<p>Nota: el programa (<code>renderingProgram</code>) debe estar enlazado antes de llamar a <code>glGetUniformLocation</code>.</p>
</blockquote>
<ol start="2">
<li><strong>Enviar los datos al uniform</strong> con funciones <code>glUniform*</code>:</li>
</ol>
<pre><code class="lang-c++">glUniformMatrix4fv(mvLoc, <span class="hljs-number">1</span>, GL_FALSE, glm::value_ptr(mvMat)); glUniformMatrix4fv(projLoc, <span class="hljs-number">1</span>, GL_FALSE, glm::value_ptr(pMat));
</code></pre>
<ul>
<li><p><code>glUniformMatrix4fv</code>: actualiza un uniform de tipo <code>mat4</code>.</p>
</li>
<li><p>El <code>1</code> indica que se pasa una sola matriz.</p>
</li>
<li><p><code>GL_FALSE</code> señala que no deben transponerse los datos.</p>
</li>
<li><p><code>glm::value_ptr(...)</code> obtiene un puntero compatible con OpenGL.</p>
</li>
</ul>
<p>En la función <code>display()</code>, justo antes de dibujar, se realiza este proceso.</p>
<hr />
<h3 id="heading-43-uso-tipico-de-uniforms">4.3 Uso típico de Uniforms</h3>
<p>Algunos de los usos más comunes de uniforms son:</p>
<ul>
<li><p><strong>Matrices de transformación:</strong> modelo, vista, proyección.</p>
</li>
<li><p><strong>Parámetros de cámara:</strong> posición, dirección, matrices de vista.</p>
</li>
<li><p><strong>Propiedades de materiales:</strong> color base, reflectividad, coeficientes de iluminación.</p>
</li>
<li><p><strong>Luces:</strong> posición, color, intensidad.</p>
</li>
<li><p><strong>Texturas:</strong> identificadores de samplers (aunque las texturas en sí se gestionan con otra API, se referencian mediante uniforms).</p>
</li>
</ul>
<hr />
<h3 id="heading-44-diferencia-con-los-atributos">4.4 Diferencia con los Atributos</h3>
<ul>
<li><p><strong>Atributos (</strong><code>in</code>) → Se actualizan <strong>por vértice</strong> y pueden ser interpolados por el rasterizador.</p>
</li>
<li><p><strong>Uniforms</strong> → Se mantienen <strong>constantes para todos los vértices/fragmentos</strong> durante un draw call.</p>
</li>
</ul>
<p>Ejemplo práctico:</p>
<ul>
<li><p>Un <strong>atributo</strong> puede ser el color distinto en cada vértice del cubo.</p>
</li>
<li><p>Un <strong>uniform</strong> puede ser la matriz de proyección que se aplica por igual a todos los vértices del cubo.</p>
</li>
</ul>
<hr />
<p>En pocas palabras: los uniforms son la forma de pasar información <strong>global</strong> al shader. Permiten aplicar transformaciones, parámetros de cámara o iluminación de manera coherente a toda la geometría, sin necesidad de duplicar datos en cada vértice.</p>
<h2 id="heading-5-ejemplo">5. Ejemplo</h2>
<h3 id="heading-dibujar-un-cubo-coloreado-en-opengl">Dibujar un cubo coloreado en OpenGL</h3>
<p>Ya que hemos repasado los conceptos de buffers, shaders y uniforms, veamos cómo se usan en un programa real. El siguiente ejemplo dibuja un <strong>cubo 3D coloreado</strong> paso a paso, poniendo en práctica todo lo explicado sobre el pipeline de OpenGL.</p>
<hr />
<h2 id="heading-51-preparar-los-datos-del-cubo">5.1 Preparar los datos del cubo</h2>
<p>Lo primero es definir los vértices que forman las caras del cubo y asignarles un color. Cada cara está compuesta por dos triángulos (6 vértices), y en total el cubo necesita 36 vértices.</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// Posiciones de los vértices (36 vértices = 12 triángulos)</span>
<span class="hljs-keyword">float</span> cubeVertexPositions[<span class="hljs-number">108</span>] = {
    <span class="hljs-comment">// cara frontal</span>
    <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,
     <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,
     <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
    <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,
     <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
    <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
    <span class="hljs-comment">// ... resto de las caras (izquierda, derecha, trasera, etc.)</span>
};

<span class="hljs-comment">// Colores asociados a cada vértice</span>
<span class="hljs-keyword">float</span> cubeVertexColors[<span class="hljs-number">108</span>] = {
    <span class="hljs-comment">// frontal (rojo)</span>
    <span class="hljs-number">1.0f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-number">0.0f</span>,
    <span class="hljs-number">1.0f</span>, <span class="hljs-number">0.5f</span>, <span class="hljs-number">0.5f</span>,
    <span class="hljs-number">1.0f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-number">0.0f</span>,
    <span class="hljs-comment">// ... resto de colores</span>
};
</code></pre>
<hr />
<h2 id="heading-52-crear-vao-y-vbos">5.2 Crear VAO y VBOs</h2>
<p>Un <strong>VAO</strong> almacena la configuración de atributos, y dos <strong>VBOs</strong> guardan posiciones y colores.</p>
<pre><code class="lang-cpp">GLuint vao[<span class="hljs-number">1</span>];
GLuint vbo[<span class="hljs-number">2</span>];

<span class="hljs-comment">// Generar y enlazar VAO</span>
glGenVertexArrays(<span class="hljs-number">1</span>, vao);
glBindVertexArray(vao[<span class="hljs-number">0</span>]);

<span class="hljs-comment">// VBO de posiciones</span>
glGenBuffers(<span class="hljs-number">2</span>, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[<span class="hljs-number">0</span>]);
glBufferData(GL_ARRAY_BUFFER, <span class="hljs-keyword">sizeof</span>(cubeVertexPositions), cubeVertexPositions, GL_STATIC_DRAW);
glVertexAttribPointer(<span class="hljs-number">0</span>, <span class="hljs-number">3</span>, GL_FLOAT, GL_FALSE, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
glEnableVertexAttribArray(<span class="hljs-number">0</span>);

<span class="hljs-comment">// VBO de colores</span>
glBindBuffer(GL_ARRAY_BUFFER, vbo[<span class="hljs-number">1</span>]);
glBufferData(GL_ARRAY_BUFFER, <span class="hljs-keyword">sizeof</span>(cubeVertexColors), cubeVertexColors, GL_STATIC_DRAW);
glVertexAttribPointer(<span class="hljs-number">1</span>, <span class="hljs-number">3</span>, GL_FLOAT, GL_FALSE, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
glEnableVertexAttribArray(<span class="hljs-number">1</span>);
</code></pre>
<hr />
<h2 id="heading-53-shaders-vertex-y-fragment">5.3 Shaders: Vertex y Fragment</h2>
<p>El <strong>vertex shader</strong> transforma posiciones con las matrices de modelo-vista y proyección, y pasa el color al fragment shader.</p>
<pre><code class="lang-cpp"><span class="hljs-meta">#version 430 core</span>
layout(location = <span class="hljs-number">0</span>) in vec3 position;
layout(location = <span class="hljs-number">1</span>) in vec3 color;

out vec4 varyingColor;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
    gl_Position = proj_matrix * mv_matrix * vec4(position, <span class="hljs-number">1.0</span>);
    varyingColor = vec4(color, <span class="hljs-number">1.0</span>);
}
</code></pre>
<p>El <strong>fragment shader</strong> recibe el color interpolado y lo envía al framebuffer:</p>
<pre><code class="lang-cpp"><span class="hljs-meta">#version 430 core</span>
in vec4 varyingColor;
out vec4 fragColor;

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
    fragColor = varyingColor;
}
</code></pre>
<hr />
<h2 id="heading-54-uso-de-uniforms">5.4 Uso de uniforms</h2>
<p>Desde C++ cargamos las matrices de transformación en los uniforms:</p>
<pre><code class="lang-cpp">mvLoc   = glGetUniformLocation(renderingProgram, <span class="hljs-string">"mv_matrix"</span>);
projLoc = glGetUniformLocation(renderingProgram, <span class="hljs-string">"proj_matrix"</span>);

glUniformMatrix4fv(mvLoc, <span class="hljs-number">1</span>, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, <span class="hljs-number">1</span>, GL_FALSE, glm::value_ptr(pMat));
</code></pre>
<hr />
<h2 id="heading-55-dibujar-el-cubo">5.5 Dibujar el cubo</h2>
<p>En la función de renderizado, limpiamos la pantalla y dibujamos los vértices:</p>
<pre><code class="lang-cpp">glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(renderingProgram);

glUniformMatrix4fv(mvLoc, <span class="hljs-number">1</span>, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, <span class="hljs-number">1</span>, GL_FALSE, glm::value_ptr(pMat));

glBindVertexArray(vao[<span class="hljs-number">0</span>]);
glDrawArrays(GL_TRIANGLES, <span class="hljs-number">0</span>, <span class="hljs-number">36</span>);
</code></pre>
<hr />
<h2 id="heading-56-resultado">5.6 Resultado</h2>
<p>El programa muestra un <strong>cubo 3D con colores en cada cara</strong>, renderizado con profundidad para que las caras ocultas no se dibujen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759600515571/e9cc34fd-f1b7-407c-b7d7-c7ffbd66b2d7.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>VBOs y VAO</strong> para manejar los datos de vértices.</p>
</li>
<li><p><strong>Shaders</strong> para transformar y colorear la geometría.</p>
</li>
<li><p><strong>Uniforms</strong> para aplicar matrices de cámara y proyección.</p>
</li>
<li><p>El <strong>pipeline de OpenGL</strong> trabajando de principio a fin.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusión</h2>
<p>A lo largo del artículo vimos cómo funciona el recorrido de los datos en OpenGL: desde que definimos un conjunto de vértices en la aplicación, hasta que finalmente esos datos se transforman en píxeles en pantalla.</p>
<ul>
<li><p>Los <strong>buffers</strong> nos permiten organizar la geometría en memoria para que la GPU la procese eficientemente.</p>
</li>
<li><p>Los <strong>uniforms</strong> actúan como parámetros globales que guían al pipeline sin repetirse en cada vértice.</p>
</li>
<li><p>Las <strong>etapas programables</strong> (vertex y fragment shaders) son los lugares donde podemos intervenir directamente para dar forma y color a lo que se dibuja.</p>
</li>
<li><p>Finalmente, todo desemboca en el <strong>framebuffer</strong>, que es el lienzo final donde queda guardada la imagen.</p>
</li>
</ul>
<p>Este recorrido muestra que dibujar en 3D no es magia: es un proceso ordenado de transformaciones y cálculos que, etapa tras etapa, convierten datos numéricos en una escena visible.</p>
<p>Con estos conceptos en mente, ya cuentas con la base para dar el siguiente paso: experimentar con <strong>texturas, iluminación y efectos visuales</strong> que harán tus programas mucho más expresivos y atractivos.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Computer-Graphics-Programming">https://github.com/Nobody-1321/Computer-Graphics-Programming</a></div>
]]></content:encoded></item><item><title><![CDATA[Espacios de Coordenadas en OpenGL]]></title><description><![CDATA[Introducción
En OpenGL, cada vértice comienza su recorrido en un sistema de coordenadas muy simple: el espacio local del objeto. Sin embargo, antes de llegar a convertirse en un fragmento visible en pantalla, ese mismo vértice debe atravesar una seri...]]></description><link>https://codigoenllamas.com/espacios-de-coordenadas-en-opengl</link><guid isPermaLink="true">https://codigoenllamas.com/espacios-de-coordenadas-en-opengl</guid><category><![CDATA[openGL]]></category><category><![CDATA[C++]]></category><category><![CDATA[graphicsprogramming]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Wed, 17 Sep 2025 16:37:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/QkQbnuzvkdo/upload/0e5170aadd6f952348f394ae16ff1fa5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduccion">Introducción</h2>
<p>En OpenGL, cada vértice comienza su recorrido en un sistema de coordenadas muy simple: el espacio local del objeto. Sin embargo, antes de llegar a convertirse en un fragmento visible en pantalla, ese mismo vértice debe atravesar una serie de transformaciones geométricas que lo llevan por distintos espacios de coordenadas. Este proceso no es un capricho del pipeline gráfico, sino una necesidad: permite separar responsabilidades, facilitar cálculos como la iluminación, y mantener una forma ordenada y flexible de describir escenas complejas.</p>
<blockquote>
<p>English version of this article. <a target="_blank" href="https://medium.com/imagecraft/mathematics-for-3d-graphics-with-opengl-800e9c10e2df">Click here</a></p>
</blockquote>
<p>El objetivo final de este recorrido es obtener las llamadas <strong>Coordenadas de Dispositivo Normalizadas (NDC)</strong>, que son indispensables para que el rasterizador pueda convertir la geometría en píxeles. Para llegar a ellas, OpenGL utiliza una secuencia de transformaciones aplicadas mediante matrices: <strong>Model</strong>, <strong>View</strong> y <strong>Projection</strong>. La clave está en que estas operaciones no se aplican por separado, sino que se <strong>concatenan en una sola cadena de transformaciones</strong>, lo que permite expresar de manera compacta la transición desde el espacio local de un objeto hasta su representación en pantalla.</p>
<p>En este artículo exploraremos cada uno de los espacios de coordenadas que intervienen en este proceso —Local, Mundial, de Vista, de Clip, de NDC y de Pantalla— y veremos cómo la composición de matrices hace posible que un vértice atraviese este camino de forma precisa y eficiente.</p>
<h2 id="heading-2-el-panorama-general-la-cadena-de-transformacion">2. El Panorama General: La Cadena de Transformación</h2>
<p>Para comprender cómo OpenGL lleva un vértice desde su posición original en un objeto hasta su representación final en la pantalla, conviene tener una <strong>vista global del proceso</strong>. El pipeline gráfico no aplica una única transformación, sino una <strong>cadena de operaciones sucesivas</strong>, cada una asociada a un espacio de coordenadas específico.</p>
<p>En total, un vértice atraviesa cinco espacios fundamentales:</p>
<ol>
<li><p><strong>Espacio Local (Object Space):</strong> el sistema propio del objeto, definido durante el modelado.</p>
</li>
<li><p><strong>Espacio Mundial (World Space):</strong> unifica todos los objetos de la escena bajo un mismo sistema de referencia global.</p>
</li>
<li><p><strong>Espacio de Vista (Eye Space o View Space):</strong> representa la escena desde el punto de vista de la cámara.</p>
</li>
<li><p><strong>Espacio de Clip (Clip Space):</strong> el resultado de aplicar la proyección; aquí se determina qué geometría será visible.</p>
</li>
<li><p><strong>Espacio de Dispositivo y Pantalla (NDC y Screen Space):</strong> la conversión final a coordenadas normalizadas y luego a píxeles.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758124768778/c62089ad-a140-4977-ba52-bd29c5f8ea44.png" alt /></p>
<p>La transición entre estos espacios se logra mediante tres matrices principales:</p>
<ul>
<li><p><strong>Matriz de Modelo (M):</strong> lleva vértices del espacio local al mundo.</p>
</li>
<li><p><strong>Matriz de Vista (V):</strong> convierte las coordenadas del mundo al sistema de la cámara.</p>
</li>
<li><p><strong>Matriz de Proyección (P):</strong> proyecta la escena de 3D a 2D, generando el espacio de clip.</p>
</li>
</ul>
<p>Estas matrices se aplican de forma <strong>concatenada</strong> en una expresión compacta:</p>
<p>$$\text{MVP} = P \cdot V \cdot M$$</p><p>donde cada vértice v se transforma como:</p>
<p>$$v_{clip} = P \cdot V \cdot M \cdot v_{local}$$</p><p>Este esquema refleja el corazón del pipeline gráfico: una secuencia de transformaciones geométricas expresadas como multiplicaciones de matrices. Comprender esta cadena es esencial antes de entrar al detalle de cada espacio, pues ofrece la <strong>intuición general del recorrido de un vértice</strong> en OpenGL.</p>
<h2 id="heading-3-espacio-local-object-space">3. Espacio Local (Object Space)</h2>
<p>El recorrido de un vértice en OpenGL comienza en el <strong>espacio local</strong>, también llamado <em>object space</em>. Este es el sistema de coordenadas <strong>propio del objeto</strong>, definido normalmente durante la etapa de modelado. En este espacio, cada vértice se expresa con respecto a un <strong>origen y ejes internos</strong> al objeto, sin ninguna relación todavía con la escena global ni con la cámara.</p>
<p>Un ejemplo sencillo: al crear un cubo en un programa de modelado, sus vértices suelen estar centrados en el origen (0,0,0), con coordenadas que van de −0.5 a 0.5. En ese momento, no importa dónde aparecerá el cubo en la escena: lo único que importa es su forma y su escala relativas a su propio sistema.</p>
<pre><code class="lang-c++">    <span class="hljs-keyword">float</span> CubeVertexPositions[<span class="hljs-number">108</span>] = {
        <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,
         <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,
         <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,
         <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,
         <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
        <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
        <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
        <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
        <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,
         <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,
        <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,
         <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,  <span class="hljs-number">1.0f</span>, <span class="hljs-number">-1.0f</span>,
    };
</code></pre>
<p>El objetivo principal en este espacio es permitir transformaciones de <strong>modelado</strong> tales como:</p>
<ul>
<li><p><strong>Traslación:</strong> mover el objeto en relación con su propio origen.</p>
</li>
<li><p><strong>Rotación:</strong> girar el objeto alrededor de sus ejes locales.</p>
</li>
<li><p><strong>Escalado:</strong> modificar su tamaño en una o varias direcciones.</p>
</li>
</ul>
<p>Todas estas operaciones se combinan en la llamada <strong>Matriz de Modelo (M)</strong>, que actúa como el puente entre el espacio local y el espacio mundial. Matemáticamente, si <em>v_local</em> es un vértice en coordenadas locales, la conversión al mundo se expresa como:</p>
<p>$$v_{world} = M \cdot v_{local}$$</p><p>Un punto crucial es que estas transformaciones no son independientes, sino que pueden <strong>concatenarse</strong> en una sola matriz. Por ejemplo, si primero escalamos, luego rotamos y después trasladamos un objeto, la matriz resultante será el producto:</p>
<p>$$M = T \cdot R \cdot S$$</p><p>donde el orden importa, ya que la multiplicación de matrices <strong>no es conmutativa</strong>.</p>
<pre><code class="lang-c++">glm::mat4 mMat;

mMat = glm::translate(glm::mat4(<span class="hljs-number">1.0f</span>), glm::vec3(cubeLocX, cubeLocY, cubeLocZ));

mMat = glm::scale(mMat, glm::vec3(<span class="hljs-number">1.5f</span>, <span class="hljs-number">1.5f</span>, <span class="hljs-number">1.5f</span>));

mMat = glm::rotate(mMat, glm::radians(<span class="hljs-number">45.0f</span>), glm::vec3(<span class="hljs-number">0.0f</span>, <span class="hljs-number">1.0f</span>, <span class="hljs-number">0.0f</span>));
</code></pre>
<p>Este espacio local resulta particularmente útil porque permite describir la geometría de manera sencilla y reutilizable: un mismo modelo puede colocarse en diferentes posiciones y escalas en la escena simplemente cambiando su matriz de modelo.</p>
<h2 id="heading-4-espacio-mundial-world-space">4. Espacio Mundial (World Space)</h2>
<p>Una vez que un vértice ha sido definido en el espacio local del objeto, el siguiente paso es situarlo dentro de la escena completa. Para ello se utiliza el <strong>espacio mundial</strong> (<em>world space</em>), un sistema de coordenadas global que sirve como referencia común para todos los objetos.</p>
<p>En este espacio, cada modelo deja de estar aislado en su propio origen y se coloca en una posición coherente con el resto de la escena. Así, si un cubo se ubica a la izquierda y una esfera a la derecha, ambas comparten un mismo marco de referencia que permite describir sus relaciones espaciales de forma consistente.</p>
<p>La transición de <strong>local → mundial</strong> ya está definida por la <strong>matriz de modelo (M)</strong>, la cual integra todas las transformaciones aplicadas a cada objeto (escala, rotación y traslación). Al aplicar esta matriz sobre todos los vértices del objeto, se logra colocar el modelo en el mundo en la posición y orientación deseadas:</p>
<p>$$v_{world} = M \cdot v_{local}$$</p><p>Un aspecto esencial del espacio mundial es que permite la <strong>interacción entre múltiples objetos</strong>. Por ejemplo:</p>
<ul>
<li><p>Colocar una mesa en el centro de una habitación.</p>
</li>
<li><p>Ubicar una lámpara sobre la mesa.</p>
</li>
<li><p>Posicionar una cámara frente a toda la escena.</p>
</li>
</ul>
<p>Otro punto importante es que las transformaciones de modelado suelen organizarse de forma <strong>jerárquica</strong>. Esto significa que la posición de un objeto puede depender de otro. Por ejemplo, si un robot tiene un brazo articulado, al mover el cuerpo completo, el brazo también se mueve, ya que hereda las transformaciones de su “padre” en la jerarquía. En este caso, la concatenación de matrices resulta fundamental, pues la matriz de modelo final de un objeto puede estar compuesta por varias transformaciones heredadas.</p>
<h2 id="heading-5-espacio-de-vista-eye-space-view-space">5. Espacio de Vista (Eye Space / View Space)</h2>
<p>Una vez que los objetos han sido colocados en la escena global mediante el espacio mundial, el siguiente paso es observar esa escena desde un <strong>punto de vista particular</strong>. Aquí es donde entra en juego el <strong>espacio de vista</strong>, también conocido como <em>eye space</em> o <em>view space</em>.</p>
<p>En este sistema de coordenadas, todo se reinterpreta como si estuviéramos mirando la escena desde los “ojos” de una cámara. Sin embargo, es importante aclarar una confusión muy común: <strong>en OpenGL no existe realmente un objeto “cámara” que se pueda mover o renderizar.</strong> Lo que hacemos, en realidad, es aplicar una transformación matemática a toda la escena para simular lo que una cámara vería desde cierta posición y orientación.</p>
<p>El concepto clave es el siguiente:</p>
<ul>
<li><p><strong>Mover la cámara hacia adelante</strong> es equivalente a <strong>mover toda la escena hacia atrás</strong>.</p>
</li>
<li><p><strong>Girar la cámara hacia la derecha</strong> es lo mismo que <strong>rotar todo el mundo hacia la izquierda</strong>.</p>
</li>
</ul>
<p>De esta manera, en lugar de trasladar o rotar una cámara, lo que hacemos es aplicar la <strong>Matriz de Vista (V)</strong> a todos los vértices de la escena:</p>
<p>$$v_{view} = V \cdot v_{world}$$</p><p>La matriz de vista se construye generalmente a partir de tres parámetros:</p>
<ol>
<li><p>La <strong>posición de la cámara</strong> (desde dónde miramos).</p>
</li>
<li><p>El <strong>punto de enfoque</strong> (hacia dónde miramos).</p>
</li>
<li><p>El <strong>vector “arriba”</strong> (<em>up vector</em>), que define la orientación vertical.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758125454986/af310b69-2e2e-4d0f-bf24-67358b2ce5ae.png" alt class="image--center mx-auto" /></p>
<p>Con esta información, la matriz de vista se calcula de forma que traslada y rota la escena entera, colocando la cámara en el <strong>origen del espacio de vista</strong>, mirando hacia el eje −Z por convención en OpenGL.</p>
<pre><code class="lang-c++">glm::mat4 vMat;

vMat = glm::lookAt(
glm::vec3(cameraX, cameraY, cameraZ), <span class="hljs-comment">// eye position</span>
glm::vec3(<span class="hljs-number">0.0f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-number">0.0f</span>),         <span class="hljs-comment">// center: where the eye is looking at</span>
glm::vec3(<span class="hljs-number">0.0f</span>, <span class="hljs-number">1.0f</span>, <span class="hljs-number">0.0f</span>)          <span class="hljs-comment">// up: the upward direction</span>
);
</code></pre>
<p>¿Por qué este paso es tan útil? Porque muchos cálculos gráficos, como la iluminación y el sombreado, se simplifican al trabajar en un sistema donde el observador está en un punto fijo y conocido: el origen de coordenadas.</p>
<hr />
<h2 id="heading-6-espacio-de-clip-clip-space">6. Espacio de Clip (Clip Space)</h2>
<p>Después de transformar la escena al <strong>espacio de vista</strong> (Eye Space), el siguiente paso es proyectarla al <strong>espacio de clip</strong> (<em>clip space</em>). Este es un espacio intermedio donde OpenGL decide qué geometría está dentro del <strong>campo de visión</strong> y, por tanto, puede llegar a la pantalla.</p>
<p>La transición de <strong>vista → clip</strong> se realiza mediante la <strong>matriz de proyección</strong> P:</p>
<p>$$v_{clip} = P \cdot v_{view}$$</p><p>En este punto, cada vértice todavía tiene cuatro componentes (x,y,z,w), y será la <strong>división por w</strong> la que lo convertirá en coordenadas normalizadas (NDC).</p>
<hr />
<h3 id="heading-61-matriz-de-proyeccion-en-perspectiva">6.1 Matriz de Proyección en Perspectiva</h3>
<p>La <strong>proyección en perspectiva</strong> imita la forma en que los humanos percibimos el mundo: los objetos cercanos se ven más grandes que los lejanos. Esto se logra mediante una transformación que introduce la componente w, de manera que la profundidad afecta al tamaño aparente de los objetos.</p>
<p>Para construir esta matriz se necesitan cuatro parámetros:</p>
<ol>
<li><p><strong>Field of View (FOV):</strong> ángulo vertical del campo de visión.</p>
</li>
<li><p><strong>Aspect ratio:</strong> relación ancho/alto de la ventana de visualización.</p>
</li>
<li><p><strong>Near clipping plane (Znear):</strong> plano cercano donde se empieza a proyectar la escena.</p>
</li>
<li><p><strong>Far clipping plane (Zfar):</strong> plano lejano que limita la proyección.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758125209543/d8f03d91-08b0-4bc4-a2b4-3f522ea7880a.png" alt class="image--center mx-auto" /></p>
<p>Geométricamente, estos parámetros definen un <strong>frustum</strong>, una pirámide truncada que representa todo lo que la cámara puede ver. Todo objeto fuera de este volumen es descartado mediante <strong>clipping</strong>.</p>
<p>En código, usando GLM, se construye así:</p>
<pre><code class="lang-c++"><span class="hljs-keyword">float</span> aspect = (<span class="hljs-keyword">float</span>)width / (<span class="hljs-keyword">float</span>)height;
glm::mat4 pMat = glm::perspective(<span class="hljs-number">1.0472f</span>, aspect, <span class="hljs-number">0.1f</span>, <span class="hljs-number">1000.0f</span>);  <span class="hljs-comment">// 60 grados vertical</span>
</code></pre>
<p>Esta matriz transforma coordenadas de <strong>Eye Space</strong> a <strong>Clip Space</strong>, ajustando la perspectiva y preparando los vértices para la normalización posterior en NDC.</p>
<hr />
<h3 id="heading-62-matriz-de-proyeccion-ortografica">6.2 Matriz de Proyección Ortográfica</h3>
<p>En una <strong>proyección ortográfica</strong>, los objetos no se escalan con la distancia: las líneas paralelas permanecen paralelas, y no hay efecto de profundidad visual. Esto es útil en CAD, mapas, o cuando queremos mediciones precisas de los objetos, sin distorsión por perspectiva.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758125263725/761450ea-3d7f-4829-8e23-3de613aa0325.png" alt class="image--center mx-auto" /></p>
<p>Para construir esta matriz se definen los límites del <strong>volumen de visión ortográfico</strong>:</p>
<ul>
<li><p>L, R: límites izquierdo y derecho en X.</p>
</li>
<li><p>B, T: límites inferior y superior en Y.</p>
</li>
<li><p>Znear, Zfar: planos cercanos y lejanos en Z.</p>
</li>
</ul>
<p>La proyección ortográfica proyecta directamente todos los vértices dentro de este volumen hacia Clip Space sin alterar sus proporciones. En GLM, se construye con:</p>
<pre><code class="lang-c++">glm::mat4 pMat = glm::ortho(left, right, bottom, top, nearPlane, farPlane);
</code></pre>
<hr />
<h2 id="heading-7-coordenadas-de-dispositivo-normalizadas-ndc">7. Coordenadas de Dispositivo Normalizadas (NDC)</h2>
<p>Una vez que los vértices han sido transformados al <strong>espacio de clip</strong>, todavía no están listos para el rasterizador. En este punto, cada vértice se representa en coordenadas homogéneas de la forma (x,y,z,w). Para normalizarlos y llevarlos a un espacio uniforme, OpenGL realiza automáticamente la llamada <strong>división de perspectiva</strong> (<em>perspective divide</em>):</p>
<p>$$v_{ndc} = \left(\frac{x}{w}, \frac{y}{w}, \frac{z}{w}\right)$$</p><p>Este paso tiene dos consecuencias fundamentales:</p>
<ol>
<li><p><strong>Normalización:</strong><br /> Los vértices quedan contenidos en un cubo de coordenadas conocido como volumen NDC (Normalized Device Coordinates), que va de −1 a 1 en cada eje.</p>
<p> $$-1 \leq x<em>{ndc}, y</em>{ndc}, z_{ndc} \leq 1$$</p>
</li>
<li><p><strong>Profundidad en perspectiva:</strong><br /> En una proyección en perspectiva, los objetos lejanos obtienen un valor de w mayor, lo que hace que sus coordenadas normalizadas se reduzcan, simulando así el efecto de que los objetos se ven más pequeños al alejarse.</p>
</li>
</ol>
<p>De esta manera, la división entre w es el paso que convierte la proyección en perspectiva en algo tangible: el <strong>foreshortening</strong> o acortamiento visual que da realismo a la escena.</p>
<p>El volumen NDC tiene una forma cúbica muy estricta:</p>
<ul>
<li><p>En el eje <strong>x</strong>, de −1 (izquierda) a 1 (derecha).</p>
</li>
<li><p>En el eje <strong>y</strong>, de −1 (abajo) a 1 (arriba).</p>
</li>
<li><p>En el eje <strong>z</strong>, de −1 (cerca) a 1 (lejos) en OpenGL clásico. <em>(Nota: algunas APIs como DirectX usan [0,1] para z).</em></p>
</li>
</ul>
<p>Cualquier vértice que quede fuera de este rango será descartado, pues no puede ser representado en pantalla.</p>
<hr />
<h2 id="heading-8-espacio-de-pantalla-screen-space">8. Espacio de Pantalla (Screen Space)</h2>
<p>Después de normalizar los vértices en el <strong>espacio NDC</strong>, todavía nos queda un último paso: transformarlos a <strong>coordenadas de pantalla</strong> que correspondan a píxeles reales en el monitor. Este espacio se conoce como <strong>screen space</strong> o <strong>window space</strong>.</p>
<p>El mapeo de NDC a pantalla se realiza mediante la <strong>transformación de ventana</strong> (<em>viewport transform</em>). Conceptualmente, esta transformación escala y traslada las coordenadas normalizadas [−1,1] de cada eje a valores enteros correspondientes a píxeles:</p>
<p>$$\text{Eje } x: \; [-1, 1] \;\;\longrightarrow\;\; [0, \text{ancho de la ventana}]$$</p><p>$$\text{Eje } y: \; [-1, 1] \;\;\longrightarrow\;\; [0, \text{alto de la ventana}]$$</p><p>La función de OpenGL <code>glViewport(x, y, width, height)</code> define esta correspondencia. Esencialmente, indica:</p>
<ul>
<li><p><code>(x, y)</code>: la esquina inferior izquierda del área de renderizado.</p>
</li>
<li><p><code>(width, height)</code>: las dimensiones del viewport, es decir, el área de la ventana donde se dibujará la escena.</p>
</li>
</ul>
<p>Matemáticamente, la conversión de NDC a pantalla para cada eje se puede expresar como:</p>
<p>$$x_{screen} = \frac{(x_{ndc} + 1)}{2} \cdot \text{width} + x$$</p><p>$$y_{screen} = \frac{(y_{ndc} + 1)}{2} \cdot \text{height} + y$$</p><p>El eje <strong>z</strong> también se transforma a un rango apropiado para el <strong>depth buffer</strong>, que permite el cálculo de la visibilidad de los fragmentos durante el rasterizado. (este tema sera tratado en articulos posteriores).</p>
<h2 id="heading-ejemplo">Ejemplo.</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758126901582/b69d11fb-163e-49bb-b3f9-584d53ced0a5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion-por-que-tantos-espacios-de-coordenadas">Conclusión: ¿Por Qué Tantos Espacios de Coordenadas?</h2>
<p>A lo largo del recorrido de un vértice en OpenGL, hemos visto cómo pasa por diferentes <strong>espacios de coordenadas</strong>: local, mundial, de vista, de clip, NDC y finalmente de pantalla. A primera vista, puede parecer que el vértice “viaja” por distintos mundos o que cada espacio es un lugar físico distinto. Pero no es así.</p>
<p>En realidad, cada espacio no es más que un <strong>marco de referencia matemático</strong>, una forma conveniente de describir posiciones y transformaciones en el proceso de generación de gráficos. No hay múltiples escenas o cámaras flotando en paralelo: todo ocurre dentro de un mismo sistema numérico, y lo único que hacemos es reinterpretar las coordenadas en función del problema que queremos resolver en cada etapa.</p>
<ul>
<li><p><strong>Espacio local:</strong> sirve como marco para modelar un objeto respecto a sí mismo.</p>
</li>
<li><p><strong>Espacio mundial:</strong> nos da un punto de referencia común para posicionar varios objetos en una misma escena.</p>
</li>
<li><p><strong>Espacio de vista:</strong> cambia el marco de referencia a la perspectiva de la cámara (o más bien, del observador).</p>
</li>
<li><p><strong>Espacio de clip y NDC:</strong> son marcos diseñados para simplificar el proceso de proyección y rasterización.</p>
</li>
</ul>
<p>La clave está en la <strong>concatenación de matrices</strong> (modelo, vista, proyección), que nos permite pasar de un marco a otro sin perder información esencial. Cada transformación se aplica con un propósito específico, y al final todo converge en el mismo resultado: coordenadas listas para dibujarse como píxeles en pantalla.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Computer-Graphics-Programming">https://github.com/Nobody-1321/Computer-Graphics-Programming</a></div>
]]></content:encoded></item><item><title><![CDATA[Matemáticas para Gráficos 3D con OpenGL]]></title><description><![CDATA[Introducción.
Las matemáticas son el lenguaje oculto de los gráficos por computadora: nos permiten describir y manipular situaciones complejas que, de otra forma, serían difíciles de abordar. Sin embargo, los símbolos cobran sentido solo cuando compr...]]></description><link>https://codigoenllamas.com/matematicas-para-graficos-3d-con-opengl</link><guid isPermaLink="true">https://codigoenllamas.com/matematicas-para-graficos-3d-con-opengl</guid><category><![CDATA[openGL]]></category><category><![CDATA[computer graphics]]></category><category><![CDATA[C++]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Fri, 12 Sep 2025 00:29:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/yz3Lt5Kwdi8/upload/3e929e736ca42c7db9d954dbdd710a9c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduccion">Introducción.</h2>
<p>Las matemáticas son el lenguaje oculto de los gráficos por computadora: nos permiten describir y manipular situaciones complejas que, de otra forma, serían difíciles de abordar. Sin embargo, los símbolos cobran sentido solo cuando comprendemos lo que representan y cómo se aplican en la práctica.</p>
<p>En programación gráfica, la mayoría de los efectos que percibimos como naturales en una escena 3D —movimiento de objetos, escala, perspectiva, iluminación o sombras— son transformaciones matemáticas cuidadosamente aplicadas. No es necesario ser un matemático profesional; lo esencial es aprender las nociones suficientes para usar las matemáticas de forma efectiva dentro de OpenGL.</p>
<blockquote>
<p><a target="_blank" href="https://medium.com/imagecraft/mathematics-for-3d-graphics-with-opengl-800e9c10e2df"><strong>English version of this article. Click here</strong></a></p>
</blockquote>
<p>En este articulo se presentan los fundamentos que sostienen el pipeline gráfico: sistemas de coordenadas, notación homogénea de puntos, matrices y sus transformaciones, y operaciones vectoriales clave. Lejos de ser conceptos abstractos, estas herramientas serán indispensables al diseñar y animar escenas tridimensionales.</p>
<h2 id="heading-1-sistemas-de-coordenadas-en-3d-la-base-de-todo-mundo-virtual"><strong>1. Sistemas de Coordenadas en 3D: La Base de Todo Mundo Virtual</strong></h2>
<p>Al ingresar al universo de los gráficos 3D, lo primero que debemos comprender es el espacio en el que se desarrolla todo. Así como un cartógrafo necesita de latitud y longitud para situar un punto en un mapa, un programador gráfico requiere un <strong>sistema de coordenadas</strong> para definir la posición, orientación y escala de cada objeto dentro de una escena. Este sistema constituye el marco de referencia invisible sobre el que se levanta cualquier mundo virtual.</p>
<h3 id="heading-11-definicion-de-los-ejes-x-y-y-z"><strong>1.1 Definición de los Ejes X, Y y Z</strong></h3>
<p>Un sistema de coordenadas tridimensional está formado por tres ejes <strong>ortogonales</strong> (perpendiculares entre sí) que se cruzan en un punto común, el <strong>origen</strong> (0, 0, 0):</p>
<ul>
<li><p><strong>Eje X:</strong> Representa la dirección horizontal (izquierda-derecha). Por convención, los valores positivos se extienden hacia la derecha y los negativos hacia la izquierda.</p>
</li>
<li><p><strong>Eje Y:</strong> Representa la dirección vertical (arriba-abajo). Los valores positivos crecen hacia arriba y los negativos hacia abajo.</p>
</li>
<li><p><strong>Eje Z:</strong> Representa la <strong>profundidad</strong> (adelante-atrás). Es el eje que otorga tridimensionalidad, diferenciando un plano bidimensional de un espacio 3D real.</p>
</li>
</ul>
<p>Cualquier punto dentro de este espacio puede describirse de manera única mediante un trío de valores (X, Y, Z), conocidos como coordenadas o vector de posición.</p>
<h3 id="heading-12-la-diferencia-crucial-sistemas-diestros-right-handed-vs-zurdos-left-handed"><strong>1.2 La Diferencia Crucial: Sistemas Diestros (Right-Handed) vs. Zurdos (Left-Handed)</strong></h3>
<p>A primera vista, definir tres ejes parece trivial. Sin embargo, la forma en que se orientan en la práctica determina toda la coherencia espacial de la escena. Aquí es donde aparece una distinción fundamental: los sistemas de coordenadas <strong>diestros</strong> y <strong>zurdos</strong>.</p>
<ul>
<li><p><strong>Sistema Diestro (Right-Handed):</strong><br />  Imagina tu <strong>mano derecha</strong> abierta. Apunta los dedos (excepto el pulgar) en la dirección positiva del eje X. Luego dóblalos hacia la dirección positiva del eje Y. Tu pulgar extendido señalará automáticamente la dirección positiva del eje Z.</p>
<ul>
<li><strong>Visualización:</strong> el eje Z positivo “sale” de la pantalla hacia el observador. Este sistema es el estándar en matemáticas y física, y también el que usa <strong>OpenGL</strong> de forma predominante.</li>
</ul>
</li>
<li><p><strong>Sistema Zurdo (Left-Handed):</strong><br />  Ahora haz el mismo gesto con tu <strong>mano izquierda</strong>. Los dedos siguen apuntando hacia X positivo y al doblarlos hacia Y positivo, el pulgar indicará la dirección positiva del eje Z.</p>
<ul>
<li><strong>Visualización:</strong> en este caso, el eje Z positivo se interna “hacia adentro” de la pantalla, alejándose del observador. Este sistema es empleado en <strong>Direct3D</strong> y puede resultar más intuitivo en entornos bidimensionales, ya que un mayor valor de Z equivale a “más lejos”.</li>
</ul>
</li>
</ul>
<h3 id="heading-13-consecuencia-practica-rotaciones-positivas"><strong>1.3 Consecuencia Práctica: Rotaciones Positivas</strong></h3>
<p>La elección entre un sistema diestro o zurdo no es un mero tecnicismo. Una de sus implicaciones más directas es la definición de las <strong>rotaciones positivas</strong>.</p>
<ul>
<li><p>En un sistema diestro, la dirección del giro se determina aplicando la <strong>regla de la mano derecha</strong>: si apuntas el pulgar en la dirección positiva de un eje, la curva de los demás dedos indica el sentido positivo de rotación.</p>
</li>
<li><p>En un sistema zurdo, se aplica la misma regla, pero con la <strong>mano izquierda</strong>.</p>
</li>
</ul>
<p>Si se mezclan ambos sistemas en un mismo proyecto, los objetos girarán en sentido contrario al esperado, lo que puede generar inconsistencias difíciles de detectar. Por ello, es esencial tener claro desde el inicio cuál sistema de coordenadas se está utilizando.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757617292864/189a1464-7d83-42fe-8e97-f593ce523c1b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-2-representacion-de-puntos-precision-y-potencia-en-notacion-homogenea"><strong>2. Representación de Puntos: Precisión y Potencia en Notación Homogénea</strong></h2>
<p>Definir la posición de un vértice es quizá la acción más fundamental en los gráficos por computadora. Sin embargo, la manera en que representamos matemáticamente estos puntos tiene un impacto directo en la <strong>eficiencia</strong> y en las <strong>posibilidades de transformación</strong> que ofrece un motor gráfico. La evolución desde la notación cartesiana clásica hacia la notación homogénea no fue un simple cambio de convención, sino un paso crucial que cimentó gran parte del rendering moderno.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757818798376/f5ede3f6-7aca-48f0-81bb-05237a703729.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-21-notacion-cartesiana-clasica-x-y-z"><strong>2.1 Notación Cartesiana Clásica: (x, y, z)</strong></h3>
<p>Es la forma más intuitiva y familiar de describir un punto en el espacio tridimensional.</p>
<ul>
<li><p><strong>Definición:</strong> Un trío de números reales que indican la distancia a lo largo de cada eje respecto al origen.</p>
</li>
<li><p><strong>Ejemplo:</strong> El punto <code>(2, 8, -3)</code> se ubica 2 unidades a la derecha, 8 unidades hacia arriba y 3 unidades hacia atrás, si asumimos un sistema de coordenadas diestro donde el eje Z positivo “sale” hacia el observador.</p>
</li>
<li><p><strong>Limitación:</strong> Aunque es útil para representar <strong>posiciones absolutas</strong>, esta notación se queda corta al intentar unificar transformaciones más complejas, como proyecciones en perspectiva, dentro de un mismo marco algebraico.</p>
</li>
</ul>
<h3 id="heading-22-notacion-homogenea-x-y-z-w"><strong>2.2 Notación Homogénea: (x, y, z, w)</strong></h3>
<p>Para superar esas limitaciones, se recurre a la <strong>notación homogénea</strong>, una herramienta matemática que se ha convertido en estándar en gráficos 3D.</p>
<ul>
<li><p><strong>Definición:</strong> Un punto en notación homogénea se representa con <strong>cuatro componentes</strong>: <code>(x, y, z, w)</code>, donde <code>w</code> es el <strong>componente homogéneo</strong>.</p>
</li>
<li><p><strong>Punto ordinario:</strong> Cuando <code>w = 1</code>, las coordenadas <code>(x, y, z, 1)</code> equivalen al punto cartesiano <code>(x, y, z)</code>.</p>
</li>
<li><p><strong>Vector de dirección:</strong> Cuando <code>w = 0</code>, la tupla <code>(x, y, z, 0)</code> ya no describe un punto, sino un <strong>vector</strong>. Este vector no tiene posición fija: indica solo dirección y magnitud, lo que lo hace inmune a las traslaciones (exactamente el comportamiento que se busca al aplicar transformaciones a normales, velocidades o rayos de luz).</p>
</li>
</ul>
<h3 id="heading-23-ventajas-de-la-notacion-homogenea"><strong>2.3 Ventajas de la Notación Homogénea</strong></h3>
<p>El verdadero poder de la notación homogénea se revela en el manejo de las transformaciones geométricas:</p>
<ol>
<li><p><strong>Unificación de operaciones:</strong> Traslaciones, rotaciones, escalados y proyecciones se pueden expresar bajo un mismo mecanismo: la multiplicación por matrices 4x4. En notación cartesiana, cada operación requeriría un tratamiento distinto; en homogénea, todas se reducen a un producto matricial.</p>
</li>
<li><p><strong>Proyección en perspectiva:</strong> La proyección en perspectiva, que hace que los objetos lejanos se vean más pequeños, requiere dividir por una escala dependiente de la profundidad. En coordenadas homogéneas, esta operación se integra naturalmente: la matriz de proyección ajusta el valor de <code>w</code>, y la GPU realiza automáticamente la <strong>división de perspectiva</strong> (<code>x/w, y/w, z/w</code>) antes del rasterizado.</p>
</li>
<li><p><strong>Distinción clara entre punto y vector:</strong> <code>(x, y, z, 1)</code> es un punto (posee posición), mientras que <code>(x, y, z, 0)</code> es un vector (posee dirección). Gracias a esta diferencia, las traslaciones afectan a los puntos pero no a los vectores, lo que permite modelar de manera precisa el comportamiento geométrico del mundo 3D.</p>
</li>
</ol>
<h2 id="heading-3-vectores-en-graficos-3d-representacion-y-operaciones"><strong>3. Vectores en Gráficos 3D: Representación y Operaciones</strong></h2>
<p>En gráficos 3D, los vectores son herramientas esenciales para describir posiciones, direcciones y magnitudes. Comprender su representación y propiedades es clave para manipular objetos, calcular normales y determinar relaciones geométricas entre elementos de una escena.</p>
<h3 id="heading-31-representacion-y-propiedades"><strong>3.1 Representación y Propiedades</strong></h3>
<p>Un <strong>vector</strong> se define por su <strong>magnitud</strong> y <strong>dirección</strong>. A diferencia de un <strong>punto</strong>, que indica una posición específica en el espacio, un vector representa únicamente desplazamiento o dirección, sin un origen fijo. Esta distinción conceptual es fundamental: mientras que los puntos se usan para situar objetos, los vectores se utilizan para describir movimientos, fuerzas, velocidades y normales de superficies.</p>
<p>En OpenGL y GLM, los vectores se representan principalmente mediante las estructuras <code>vec3</code> y <code>vec4</code>:</p>
<pre><code class="lang-c++"><span class="hljs-comment">// Vector 3D clásico: solo dirección</span>
<span class="hljs-function">glm::vec3 <span class="hljs-title">direccion</span><span class="hljs-params">(<span class="hljs-number">1.0f</span>, <span class="hljs-number">2.0f</span>, <span class="hljs-number">3.0f</span>)</span></span>;

<span class="hljs-comment">// Vector homogéneo: usado para multiplicaciones con matrices 4x4</span>
<span class="hljs-function">glm::vec4 <span class="hljs-title">punto</span><span class="hljs-params">(<span class="hljs-number">1.0f</span>, <span class="hljs-number">2.0f</span>, <span class="hljs-number">3.0f</span>, <span class="hljs-number">1.0f</span>)</span></span>;   <span class="hljs-comment">// w = 1 indica un punto</span>
<span class="hljs-function">glm::vec4 <span class="hljs-title">vectorDir</span><span class="hljs-params">(<span class="hljs-number">1.0f</span>, <span class="hljs-number">2.0f</span>, <span class="hljs-number">3.0f</span>, <span class="hljs-number">0.0f</span>)</span></span>; <span class="hljs-comment">// w = 0 indica un vector de dirección</span>
</code></pre>
<p>La componente <code>w</code> en <code>vec4</code> permite distinguir entre puntos y vectores cuando se aplican transformaciones homogéneas mediante matrices 4x4: los puntos (<code>w=1</code>) se trasladan y rotan, mientras que los vectores (<code>w=0</code>) solo se rotan, preservando su magnitud y dirección relativa.</p>
<p><strong>En el shader (GLSL):</strong></p>
<ul>
<li><p><code>vec3</code> y <code>vec4</code> son los análogos directos.</p>
</li>
<li><p>Los atributos de vértice suelen ser <code>vec3</code> o <code>vec4</code>, mientras que las matrices de transformación (<code>mat4</code>) actúan sobre estos datos.</p>
</li>
</ul>
<pre><code class="lang-python">// Definiciones dentro <span class="hljs-keyword">del</span> vertex shader
layout (location = <span class="hljs-number">0</span>) <span class="hljs-keyword">in</span> vec3 aPos;
layout (location = <span class="hljs-number">1</span>) <span class="hljs-keyword">in</span> vec3 aNormal;

// Convertimos a homogéneas para multiplicar por mat4
vec4 posicion = vec4(aPos, <span class="hljs-number">1.0</span>);
vec4 normal   = vec4(aNormal, <span class="hljs-number">0.0</span>);
</code></pre>
<h3 id="heading-32-operaciones-con-vectores"><strong>3.2 Operaciones con Vectores</strong></h3>
<p>Los vectores admiten una serie de operaciones algebraicas que resultan fundamentales para gráficos 3D:</p>
<ol>
<li><p><strong>Suma y resta</strong>:<br /> La suma o resta de vectores se realiza componente a componente. Por ejemplo, dado A = (x₁, y₁, z₁) y B = (x₂, y₂, z₂), se tiene:</p>
<p> $$\begin{aligned} A + B &amp;= (x_1 + x_2, \; y_1 + y_2, \; z_1 + z_2) \[6pt] A - B &amp;= (x_1 - x_2, \; y_1 - y_2, \; z_1 - z_2) \end{aligned}$$</p>
</li>
<li><p><strong>Normalización</strong>:<br /> Convertir un vector a <strong>longitud unitaria</strong> (magnitud 1) es crucial para calcular direcciones, iluminación y reflejos. La normalización se realiza dividiendo cada componente por la magnitud del vector:</p>
<p> $$\hat{V} = \frac{V}{|V|} = \frac{(x, y, z)}{\sqrt{x^2 + y^2 + z^2}}$$</p>
</li>
<li><p><strong>Producto punto (dot product)</strong>:<br /> El producto punto entre dos vectores <code>A</code> y <code>B</code> se define como:</p>
<p> $$A \cdot B = x_A x_B + y_A y_B + z_A z_B$$</p>
<p> Sus aplicaciones incluyen:</p>
<ul>
<li><p>Determinar el <strong>ángulo</strong> entre vectores:</p>
<p>  $$\cos\theta = \frac{A \cdot B}{|A||B|}$$</p>
</li>
<li><p>Verificar <strong>perpendicularidad</strong>: <code>A·B = 0</code> indica vectores ortogonales.</p>
</li>
<li><p>Calcular la <strong>distancia a un plano</strong> a partir de la normal del mismo.</p>
</li>
</ul>
</li>
<li><p><strong>Producto cruz (cross product)</strong>:<br /> Produce un vector <strong>perpendicular</strong> a dos vectores dados. Dado <code>A</code> y <code>B</code>:</p>
</li>
</ol>
<p>$$A \times B = \begin{bmatrix} y_A z_B - z_A y_B \\ z_A x_B - x_A z_B \\ x_A y_B - y_A x_B \end{bmatrix}$$</p><p>Esta operación es esencial para: - Calcular <strong>normales de superficies</strong>, necesarias para iluminación y sombreado. - Determinar <strong>dirección de ejes ortogonales</strong> en sistemas de coordenadas locales de objetos.</p>
<hr />
<h2 id="heading-4-uso-de-matrices-en-graficos-3d"><strong>4. Uso de Matrices en Gráficos 3D</strong></h2>
<p>Las matrices son el <strong>andamiaje algebraico</strong> sobre el que se construyen los mundos virtuales. Funcionan como máquinas que, con una elegancia matemática, transforman puntos y vectores en el espacio. Gracias a ellas, un objeto puede moverse, girar, cambiar de tamaño o proyectarse en perspectiva, y todo ocurre mediante reglas precisas que la GPU ejecuta millones de veces por segundo.</p>
<h3 id="heading-41-definicion-y-tipos-de-matrices"><strong>4.1 Definición y Tipos de Matrices</strong></h3>
<p><strong>Estructura de una matriz 4x4</strong></p>
<p>Una matriz es un arreglo rectangular de números organizados en filas y columnas. En gráficos 3D, la más relevante es la <strong>matriz 4x4</strong>, con 16 elementos dispuestos en 4 filas y 4 columnas:</p>
<p>$$\begin{bmatrix} m_{00} &amp; m_{01} &amp; m_{02} &amp; m_{03} \\ m_{10} &amp; m_{11} &amp; m_{12} &amp; m_{13} \\ m_{20} &amp; m_{21} &amp; m_{22} &amp; m_{23} \\ m_{30} &amp; m_{31} &amp; m_{32} &amp; m_{33} \end{bmatrix}$$</p><p>Los subíndices siguen la convención <code>[fila][columna]</code>.<br />Esta estructura no es arbitraria: está diseñada para operar de forma directa con puntos en <strong>coordenadas homogéneas</strong> <code>(x, y, z, w)</code>, mediante la multiplicación de matrices.</p>
<hr />
<h3 id="heading-42-matriz-identidad"><strong>4.2 Matriz Identidad</strong></h3>
<p>La matriz identidad (<code>I</code>) es el equivalente algebraico a “no hacer nada”. Es una matriz cuadrada con unos en la diagonal principal y ceros en las demás posiciones:</p>
<p>$$\begin{bmatrix} 1 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 1 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 1 \end{bmatrix}$$</p><p>Su propiedad fundamental es mantener inalterado cualquier objeto sobre el que actúe:</p>
<ul>
<li><p><code>M * I = M</code></p>
</li>
<li><p><code>I * M = M</code></p>
</li>
<li><p><code>I * P = P</code></p>
</li>
</ul>
<p>En gráficos, representa la <strong>transformación neutra</strong> o de “reposo”. En GLM se construye con <code>glm::mat4(1.0f)</code>.</p>
<h3 id="heading-43-transpuesta-de-una-matriz"><strong>4.3 Transpuesta de una Matriz</strong></h3>
<p>La transpuesta (<code>Mᵀ</code>) se obtiene intercambiando filas por columnas.</p>
<p>Ejemplo:</p>
<p>$$M = \begin{bmatrix} a &amp; b \\ c &amp; d \end{bmatrix}, \quad M^{\mathrm{T}} = \begin{bmatrix} a &amp; c \\ b &amp; d \end{bmatrix}$$</p><p>En gráficos, se utiliza en operaciones como el cálculo de <strong>matrices normales</strong>, fundamentales para el sombreado e iluminación. Tanto GLM como GLSL incluyen la función <code>transpose()</code>.</p>
<h3 id="heading-44-operaciones-basicas"><strong>4.4 Operaciones Básicas</strong></h3>
<ul>
<li><p><strong>Suma de matrices:</strong> Se realiza sumando cada elemento con su correspondiente. Solo es válida entre matrices del mismo tamaño.</p>
</li>
<li><p><strong>Multiplicación de matrices (concatenación):</strong> Es la operación clave en 3D.</p>
<ul>
<li><p>No es conmutativa → <code>A * B ≠ B * A</code> en general.</p>
</li>
<li><p>Es asociativa → <code>A * (B * C) = (A * B) * C</code>.</p>
</li>
</ul>
</li>
</ul>
<p>La <strong>no conmutatividad</strong> refleja que el orden importa: rotar un objeto y luego trasladarlo no produce el mismo resultado que trasladarlo primero y después rotarlo.</p>
<hr />
<h3 id="heading-45-multiplicacion-matriz-punto"><strong>4.5 Multiplicación Matriz-Punto</strong></h3>
<p>Un punto en coordenadas homogéneas puede representarse como una columna:</p>
<p>$$P = \begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix}$$</p><p>Al multiplicarlo por una matriz <code>M (4x4)</code> obtenemos un nuevo punto <code>P'</code>:</p>
<p>$$P' = M * P$$</p><p>En código (GLM o GLSL), esta operación se expresa naturalmente como <code>vec4 nuevoPunto = M * P;</code>.</p>
<h3 id="heading-46-multiplicacion-matriz-matriz-concatenacion"><strong>4.6 Multiplicación Matriz-Matriz (Concatenación)</strong></h3>
<p>Multiplicar dos matrices <code>A</code> y <code>B</code> produce una nueva matriz <code>C = A * B</code> que combina ambas transformaciones. La GPU aprovecha la <strong>propiedad asociativa</strong> para optimizar cálculos:</p>
<p>En lugar de aplicar tres matrices diferentes a cada vértice:</p>
<p>$$\text{NuevoPunto} = M_1 \cdot \big( M_2 \cdot \big( M_3 \cdot P \big) \big)$$</p><p>se calcula primero la matriz compuesta:</p>
<p>$$\text{Modelo} = M_1 \cdot M_2 \cdot M_3$$</p><p>y luego:</p>
<p>$$\text{NuevoPunto} = \text{Modelo} \cdot P$$</p><p>Esto evita millones de operaciones redundantes en escenas con gran cantidad de vértices.</p>
<h3 id="heading-47-matriz-inversa"><strong>4.7 Matriz Inversa</strong></h3>
<p>La inversa de <code>M</code> (denotada <code>M⁻¹</code>) es la única matriz que cumple:</p>
<p>$$M \cdot M^{-1} = I$$</p><p>No todas las matrices son invertibles, y calcular la inversa es costoso. Aun así, es esencial en situaciones como:</p>
<ul>
<li><p>Transformar <strong>vectores normales</strong> (usando la transpuesta de la inversa).</p>
</li>
<li><p>Convertir coordenadas de un objeto al <strong>espacio de la cámara</strong>.</p>
</li>
</ul>
<p>GLM y GLSL proporcionan <code>inverse()</code>, pero debe usarse con cautela en tiempo real.</p>
<hr />
<h2 id="heading-5-matrices-de-transformacion"><strong>5. Matrices de Transformación</strong></h2>
<p>Las matrices de transformación constituyen una herramienta fundamental en gráficos por computadora, pues permiten modificar la posición, orientación y tamaño de los objetos dentro de un espacio tridimensional. El uso de matrices 4x4 junto con coordenadas homogéneas ofrece una representación unificada de traslaciones, escalados y rotaciones. Gracias a ello, estas transformaciones pueden combinarse de manera consistente, lo que constituye la base del pipeline de renderizado moderno.</p>
<h3 id="heading-51-traslacion"><strong>5.1 Traslación</strong></h3>
<p>La <strong>traslación</strong> es la operación que desplaza un objeto de una posición a otra en el espacio.</p>
<ul>
<li><strong>Matriz de traslación:</strong><br />  Se obtiene a partir de la matriz identidad, insertando los valores de desplazamiento <code>(Tx, Ty, Tz)</code> en la última columna:</li>
</ul>
<p>$$- \begin{bmatrix} 1.0 &amp; 0.0 &amp; 0.0 &amp; T_x \\ 0.0 &amp; 1.0 &amp; 0.0 &amp; T_y \\ 0.0 &amp; 0.0 &amp; 1.0 &amp; T_z \\ 0.0 &amp; 0.0 &amp; 0.0 &amp; 1.0 \end{bmatrix}$$</p><ul>
<li><p><strong>Efecto:</strong><br />  Al multiplicar un punto homogéneo <code>P = (x, y, z, 1)</code> por esta matriz, se obtiene</p>
<p>  $$P' = (x + Tx, y + Ty, z + Tz, 1)$$</p>
<p>  La traslación es una <strong>transformación afín</strong>, lo que significa que conserva distancias relativas y ángulos.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757617753258/d70bb522-e01d-4bd6-8ac8-7fba01550987.gif" alt class="image--center mx-auto" /></p>
<p><strong>Ejemplo:</strong></p>
<p>Para desplazar un objeto 5 unidades en el eje X, se utiliza <code>(Tx=0, Ty=5, Tz=0)</code>. En GLM, esto se implementa con:</p>
<pre><code class="lang-C++">glm::translate(glm::mat4(<span class="hljs-number">1.0f</span>), glm::vec3(<span class="hljs-number">5.0f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-number">0.0f</span>));
</code></pre>
<h3 id="heading-52-escalado"><strong>5.2 Escalado</strong></h3>
<p>El <strong>escalado</strong> modifica el tamaño de un objeto o lo refleja respecto a un plano.</p>
<ul>
<li><strong>Matriz de escalado:</strong></li>
</ul>
<p>$$\begin{bmatrix} S_x &amp; 0.0 &amp; 0.0 &amp; 0.0 \\ 0.0 &amp; S_y &amp; 0.0 &amp; 0.0 \\ 0.0 &amp; 0.0 &amp; S_z &amp; 0.0 \\ 0.0 &amp; 0.0 &amp; 0.0 &amp; 1.0 \end{bmatrix}$$</p><ul>
<li><p><strong>Efecto sobre un punto:</strong><br />  Al aplicar esta matriz a <code>P = (x, y, z, 1)</code> se obtiene</p>
</li>
<li><p>$$P' = (Sx * x, Sy * y, Sz * z, 1).$$</p></li>
<li><p>Los factores de escala producen los siguientes efectos:</p>
<ul>
<li><p><code>&gt; 1</code>: el objeto se amplía.</p>
</li>
<li><p><code>0 &lt; valor &lt; 1</code>: el objeto se reduce.</p>
</li>
<li><p><code>&lt; 0</code>: el objeto se refleja (efecto espejo).</p>
</li>
</ul>
</li>
</ul>
<p><strong>Aplicación práctica:</strong><br />Una de sus utilidades es la conversión entre sistemas de coordenadas diestro y zurdo. Como se explicó en la sección 1, la diferencia principal radica en la dirección del eje Z. Para invertirla, basta usar un escalado con <code>(Sx=1, Sy=1, Sz=-1)</code>, lo que invierte el eje Z y cambia la handedness del sistema.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757617777484/b196f87c-ce87-4e48-8ea7-77f17a38579c.gif" alt class="image--center mx-auto" /></p>
<pre><code class="lang-cpp"> mv_matrix = glm::scale(mv_matrix, glm::vec3(<span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>));
</code></pre>
<h3 id="heading-53-rotacion"><strong>5.3 Rotación</strong></h3>
<p>La <strong>rotación</strong> es la transformación más compleja, ya que implica girar un objeto alrededor de un eje.</p>
<ul>
<li><p><strong>Rotaciones alrededor de ejes principales:</strong><br />  Las formas más sencillas corresponden a giros alrededor de los ejes cartesianos. Siendo <code>θ</code> el ángulo (en radianes), las matrices son:</p>
<ul>
<li><strong>En X:</strong></li>
</ul>
</li>
</ul>
<p>$$\begin{bmatrix} 1.0 &amp; 0.0 &amp; 0.0 &amp; 0.0 \\ 0.0 &amp; \cos\theta &amp; -\sin\theta &amp; 0.0 \\ 0.0 &amp; \sin\theta &amp; \cos\theta &amp; 0.0 \\ 0.0 &amp; 0.0 &amp; 0.0 &amp; 1.0 \end{bmatrix}$$</p><p>- <strong>En Y:</strong></p>
<p>$$R_y = \begin{bmatrix} \cos\theta &amp; 0.0 &amp; \sin\theta &amp; 0.0 \\ 0.0 &amp; 1.0 &amp; 0.0 &amp; 0.0 \\ -\sin\theta &amp; 0.0 &amp; \cos\theta &amp; 0.0 \\ 0.0 &amp; 0.0 &amp; 0.0 &amp; 1.0 \end{bmatrix}$$</p><p>- <strong>En Z:</strong></p>
<p>$$R_z = \begin{bmatrix} \cos\theta &amp; -\sin\theta &amp; 0.0 &amp; 0.0 \\ \sin\theta &amp; \cos\theta &amp; 0.0 &amp; 0.0 \\ 0.0 &amp; 0.0 &amp; 1.0 &amp; 0.0 \\ 0.0 &amp; 0.0 &amp; 0.0 &amp; 1.0 \end{bmatrix}$$</p><ul>
<li><p><strong>Ángulos de Euler:</strong><br />  El teorema de Euler establece que cualquier rotación 3D puede expresarse como una secuencia de tres rotaciones elementales sobre los ejes X, Y y Z, conocidas como <strong>ángulos de Euler</strong> (e.g., <code>pitch-yaw-roll</code>). Para rotar alrededor de un eje arbitrario que no pase por el origen, la estrategia típica es:</p>
<ol>
<li><p>Trasladar el objeto para que el eje coincida con el origen.</p>
</li>
<li><p>Aplicar la rotación deseada.</p>
</li>
<li><p>Revertir la traslación inicial.</p>
</li>
</ol>
</li>
<li><p><strong>Limitación: Gimbal Lock:</strong><br />  Los ángulos de Euler presentan un problema conocido como <strong>Gimbal Lock</strong>, que ocurre cuando dos ejes se alinean, reduciendo los grados de libertad de la rotación. Esto afecta a la animación fluida y a la interpolación de orientaciones.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757617991419/ec29e79d-fdeb-45e0-81a4-876ca2b9955b.gif" alt class="image--center mx-auto" /></p>
<pre><code class="lang-cpp">  mv_matrix = glm::rotate(mv_matrix, angle, glm::vec3(<span class="hljs-number">0.0f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-number">1.0f</span>));
</code></pre>
</li>
<li><p><strong>Alternativa: Cuaterniones:</strong><br />  Para evitar el Gimbal Lock y manejar rotaciones de forma más robusta, se emplean <strong>cuaterniones</strong>. Estas estructuras de cuatro componentes permiten:</p>
<ul>
<li><p>Representar rotaciones sin pérdida de grados de libertad.</p>
</li>
<li><p>Concatenar e interpolar rotaciones de forma eficiente (<code>slerp</code>).</p>
</li>
<li><p>Convertirse fácilmente en matrices de rotación mediante librerías como GLM (<code>glm::mat4_cast(myQuat)</code>).</p>
</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-conclusion"><strong>Conclusión</strong></h2>
<p>En gráficos 3D, la combinación de <strong>matrices de transformación</strong> y <strong>vectores</strong> constituye el fundamento matemático que permite ubicar, orientar y escalar objetos en un espacio tridimensional. Las matrices 4x4 homogéneas ofrecen un marco unificado para realizar traslaciones, escalados y rotaciones, mientras que los vectores permiten representar posiciones, direcciones y magnitudes, así como realizar operaciones clave como suma, resta, normalización, producto punto y producto cruz.</p>
<p>Comprender la distinción conceptual entre puntos y vectores, así como el uso correcto de <code>vec3</code> y <code>vec4</code> en OpenGL/GLM, es esencial para garantizar que las transformaciones se apliquen de forma coherente y eficiente en el pipeline de renderizado. Asimismo, dominar estas herramientas permite calcular normales, definir sistemas de coordenadas locales, realizar interpolaciones y evitar problemas comunes, como el Gimbal Lock en rotaciones.</p>
<p>En conjunto, matrices y vectores no solo representan conceptos matemáticos abstractos, sino que constituyen el <strong>lenguaje central</strong> para manipular geometría y controlar la dinámica de cualquier escena 3D, sentando las bases para técnicas más avanzadas de modelado, animación e iluminación</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Computer-Graphics-Programming">https://github.com/Nobody-1321/Computer-Graphics-Programming</a></div>
]]></content:encoded></item><item><title><![CDATA[Filtrado de Ruido Periódico en el Dominio de Frecuencia]]></title><description><![CDATA[1. Introducción
1.1. Ruido en imágenes: conceptos básicos
En el procesamiento digital de imágenes, el ruido es cualquier elemento no deseado que altera la fidelidad visual y dificulta el análisis posterior. Sus causas pueden ir desde fluctuaciones al...]]></description><link>https://codigoenllamas.com/filtrado-de-ruido-periodico</link><guid isPermaLink="true">https://codigoenllamas.com/filtrado-de-ruido-periodico</guid><category><![CDATA[image processing]]></category><category><![CDATA[Python]]></category><category><![CDATA[Computer Vision]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[opencv]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sun, 10 Aug 2025 02:53:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/tMcg1qrz2Hc/upload/bb6f89e4e275e1d10b0bfa8d26d994d3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-1-introduccion"><strong>1. Introducción</strong></h2>
<h3 id="heading-11-ruido-en-imagenes-conceptos-basicos"><strong>1.1. Ruido en imágenes: conceptos básicos</strong></h3>
<p>En el procesamiento digital de imágenes, el <em>ruido</em> es cualquier elemento no deseado que altera la fidelidad visual y dificulta el análisis posterior. Sus causas pueden ir desde fluctuaciones aleatorias en los sensores hasta interferencias generadas por el propio equipo o el entorno.</p>
<blockquote>
<p><a target="_blank" href="https://medium.com/imagecraft/filtering-of-periodic-noise-in-the-frequency-domain-9b24d7b380d8"><strong>English version of this article. Click here</strong></a></p>
</blockquote>
<p>En la práctica, el ruido modifica la representación original de la escena: reduce el contraste, introduce patrones que no existen en la realidad o esconde detalles importantes. Por eso, eliminarlo o al menos reducirlo es un paso clave en áreas donde la calidad de la imagen es crítica, como el diagnóstico médico, la observación satelital o la visión por computadora.</p>
<h3 id="heading-12-ruido-periodico-caracteristicas-y-causas"><strong>1.2. Ruido periódico: características y causas</strong></h3>
<p>El <em>ruido periódico</em> se distingue del aleatorio porque forma patrones repetitivos y predecibles: franjas, bandas o texturas regulares que, a simple vista, parecen parte de la imagen, pero en realidad no lo son.</p>
<p>En el dominio espacial, estos patrones suelen mantener una orientación y amplitud constantes. En el dominio de Fourier, se hacen evidentes como picos bien definidos, situados en posiciones específicas lejos del centro de la transformada.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754790692566/a05f7121-11e0-4533-ae11-eb0e124e7f7b.jpeg" alt class="image--center mx-auto" /></p>
<p>Entre sus causas más comunes se encuentran:</p>
<ul>
<li><p><strong>Interferencias eléctricas</strong> durante la captura, como en sistemas de escaneo o transmisión de datos.</p>
</li>
<li><p><strong>Defectos de calibración en sensores</strong>, que generan líneas o bandas fijas.</p>
</li>
<li><p><strong>Problemas de muestreo</strong> como el aliasing, que replican patrones indeseados.</p>
</li>
</ul>
<p>Un ejemplo claro es el <em>banding</em> en imágenes satelitales causado por fallos en detectores CCD, o las resonancias magnéticas con artefactos producidos por vibraciones mecánicas.</p>
<hr />
<h2 id="heading-2-filtrado-tradicional-el-filtro-notch"><strong>2. Filtrado tradicional: el filtro notch</strong></h2>
<h3 id="heading-21-fundamentos-del-filtro-notch-en-el-dominio-de-fourier"><strong>2.1. Fundamentos del filtro notch en el dominio de Fourier</strong></h3>
<p>El <em>filtro notch</em> es una herramienta diseñada para atenuar frecuencias específicas sin alterar el resto del espectro. Cuando el ruido es periódico, en la transformada de Fourier aparece como pares de picos simétricos respecto al centro. Si se eliminan o reducen esas frecuencias, gran parte del patrón indeseado desaparece, mientras que el resto de la imagen se conserva.</p>
<hr />
<h3 id="heading-22-implementacion-paso-a-paso"><strong>2.2. Implementación paso a paso</strong></h3>
<p>El proceso clásico consiste en:</p>
<ol>
<li><p><strong>Calcular la transformada de Fourier</strong> de la imagen para obtener su espectro.</p>
</li>
<li><p><strong>Localizar los picos asociados al ruido</strong> —a menudo por inspección visual—.</p>
</li>
<li><p><strong>Diseñar el filtro notch</strong>, que puede ser de corte abrupto (ideal) o con transición suave (gaussiano).</p>
</li>
<li><p><strong>Aplicar el filtro</strong> multiplicándolo por el espectro.</p>
</li>
<li><p><strong>Obtener la imagen filtrada</strong> mediante la transformada inversa.</p>
</li>
</ol>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">NotchFiltering</span>(<span class="hljs-params">img, d0, notch_coords, n=<span class="hljs-number">2</span></span>):</span>
    <span class="hljs-string">"""
    Applies a Butterworth notch filter to an image at specified frequency coordinates.

    Parameters:
        img : np.ndarray
            Input grayscale image to be filtered.
        d0 : float
            The cutoff radius of the notch filter. Frequencies within this radius will be attenuated.
        notch_coords : list of tuples
            A list of (u_k, v_k) coordinates in the frequency domain where periodic noise is present.
        n : int, optional
            The order of the Butterworth filter. Higher values result in a sharper transition. Default is 2.

    Returns:
        img_filtered : np.ndarray
            The filtered image in the spatial domain.
        magnitude_spectrum : np.ndarray
            The magnitude spectrum of the original image in the frequency domain.
        H_total : np.ndarray
            The combined notch filter applied in the frequency domain.

    Notes:
    -----
    - The function computes the Fourier Transform of the input image, applies the notch filter, 
      and then performs the inverse Fourier Transform to return the filtered image.
    - The filter is applied at all specified coordinates in [notch_coords] and their symmetric counterparts.
    """</span>
    f = np.fft.fft2(img)
    fshift = np.fft.fftshift(f)

    magnitude_spectrum = <span class="hljs-number">20</span> * np.log(np.abs(fshift) + <span class="hljs-number">1</span>)

    <span class="hljs-comment"># Construct combined filter</span>
    H_total = np.ones_like(img, dtype=np.float32)
    <span class="hljs-keyword">for</span> u_k, v_k <span class="hljs-keyword">in</span> notch_coords:
        H = ButterworthNotchFilter(img.shape, d0, u_k, v_k, n)
        H_total *= H

    <span class="hljs-comment"># Apply the notch filter</span>
    filtered_spectrum = fshift * H_total
    f_ishift = np.fft.ifftshift(filtered_spectrum)
    img_filtered = np.fft.ifft2(f_ishift)
    img_filtered = np.abs(img_filtered)

    <span class="hljs-keyword">return</span> img_filtered, magnitude_spectrum, H_total
</code></pre>
<hr />
<h3 id="heading-23-ejemplo-practico"><strong>2.3. Ejemplo práctico</strong></h3>
<p>Supongamos que tenemos una imagen satelital con bandas horizontales. En el espectro, estas se manifiestan como picos sobre el eje vertical.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754790740495/18796155-0caa-4f02-b251-30289bd5c3ac.png" alt /></p>
<p>Al aplicar un filtro notch Butterworth en esas posiciones, el patrón de bandas se reduce considerablemente, conservando la mayoría de los detalles originales.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754790764779/3a625e58-b58f-4898-acf0-11d95c9b3076.png" alt /></p>
<hr />
<h2 id="heading-3-limitaciones-del-filtro-notch"><strong>3. Limitaciones del filtro notch</strong></h2>
<p>hay que identificar manualmente las frecuencias a eliminar y ajustar el ancho del filtro. Esto resulta poco práctico cuando el espectro está plagado de múltiples picos dispersos, o cuando el ruido se encuentra muy cerca de la frecuencia cero (DC), ya que puede confundirse con información real de la imagen.</p>
<h2 id="heading-4-filtrado-automatico-de-ruido-cuasiperiodico"><strong>4. Filtrado automático de ruido cuasiperiódico</strong></h2>
<h3 id="heading-41-idea-general-del-metodo"><strong>4.1. Idea general del método</strong></h3>
<p>El enfoque propuesto por Sur y Grédiac parte de una observación clave: cuando una imagen está contaminada con ruido periódico que se extiende por toda su superficie, ese patrón es el único elemento que se repite de manera consistente en cualquier región que seleccionemos.</p>
<p>Aprovechando esto, se calcula un <strong>espectro de potencia promedio</strong> a partir de múltiples parches extraídos de la imagen. Al promediar, las texturas y detalles propios de la escena tienden a cancelarse, mientras que el patrón repetitivo del ruido permanece visible en el dominio de Fourier como picos bien localizados.</p>
<hr />
<h3 id="heading-42-modelado-y-deteccion-de-picos-espurios"><strong>4.2. Modelado y detección de picos espurios</strong></h3>
<p>El espectro promedio obtenido se compara con el comportamiento esperado para imágenes “naturales”, cuyo espectro de potencia sigue una <strong>ley de decaimiento en frecuencia</strong> (aproximadamente proporcional a 1/f^α).<br />Cualquier desviación significativa respecto a esta tendencia —en especial, picos muy por encima del nivel esperado— se interpreta como un indicio de ruido cuasiperiódico.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754790805698/70af2d29-d1db-4b64-b54f-1065613064d0.png" alt /></p>
<p>En vez de seleccionar manualmente las frecuencias problemáticas, el algoritmo identifica automáticamente estas “anomalías” como <strong>valores atípicos</strong> en el espectro.</p>
<hr />
<h3 id="heading-43-construccion-del-filtro-notch-adaptativo"><strong>4.3. Construcción del filtro notch adaptativo</strong></h3>
<p>Una vez localizadas las frecuencias sospechosas, se genera un <strong>mapa de picos</strong> que actúa como plantilla para construir un filtro notch. Este filtro no se limita a eliminar un par de frecuencias concretas, sino que atenúa de forma controlada todas las regiones del espectro asociadas al ruido detectado.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754790864323/ad0e2a51-4133-4d53-a731-bafad2be8a19.png" alt /></p>
<p>La imagen filtrada se reconstruye aplicando la transformada inversa de Fourier, resultando en una versión con el patrón periódico reducido o eliminado.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754790870997/4e6972cf-cc1c-44a9-9f6e-1b8d3641d8e0.png" alt /></p>
<hr />
<h2 id="heading-5-descripcion-del-algoritmo-paso-a-paso"><strong>5. Descripción del algoritmo paso a paso</strong></h2>
<p>El procedimiento para eliminar ruido cuasiperiódico de manera automática se puede dividir en varias fases bien definidas, inspiradas en el método de Sur y Grédiac y adaptadas a una implementación práctica en Python:</p>
<ol>
<li><p><strong>Conversión y normalización de la imagen</strong><br /> La imagen se transforma a escala de grises y se normaliza a valores entre 0 y 1. Esto unifica el formato de entrada y evita que las diferencias de iluminación o codificación de color influyan en el análisis espectral.</p>
</li>
<li><p><strong>División en parches con ventana de Hann</strong><br /> Se fragmenta la imagen en parches cuadrados de tamaño fijo (<em>patch size</em>) usando un solapamiento moderado. Cada parche se multiplica por una ventana de Hann bidimensional, lo que suaviza los bordes y reduce artefactos en el espectro causados por discontinuidades.</p>
</li>
<li><p><strong>Cálculo del espectro promedio</strong><br /> Para cada parche se obtiene su espectro de potencia (módulo al cuadrado de la Transformada de Fourier). Estos espectros se combinan mediante <strong>media geométrica</strong>, que atenúa valores extremos y realza la estructura común: el patrón del ruido.</p>
</li>
<li><p><strong>Ajuste de un modelo estadístico</strong><br /> Se calcula la frecuencia radial de cada punto del espectro promedio y se ajusta una <strong>ley de potencia</strong> $1/f^\alpha$ mediante regresión robusta (Huber). Este modelo describe cómo debería decaer la energía en una imagen natural sin ruido periódico.</p>
</li>
<li><p><strong>Detección de picos anómalos</strong><br /> Se calculan los residuos entre el espectro real y el modelo ajustado. Aquellos puntos que superan un umbral estadístico (kσ) y que están por encima de una frecuencia mínima f₂ se marcan como <strong>outliers</strong>, es decir, posibles frecuencias de ruido.</p>
</li>
<li><p><strong>Construcción del mapa de picos</strong><br /> Los outliers se organizan en una máscara binaria que respeta la simetría del espectro. Esta máscara se interpola al tamaño original de la imagen y se suaviza con un filtro gaussiano para evitar cortes bruscos.</p>
</li>
<li><p><strong>Filtrado notch adaptativo</strong><br /> Se multiplica el espectro original de la imagen por el complemento de la máscara suavizada. Este paso atenúa o elimina las frecuencias asociadas al ruido detectado, dejando intactas las demás.</p>
</li>
<li><p><strong>Reconstrucción y extracción del ruido</strong><br /> Mediante la transformada inversa de Fourier se obtiene la imagen filtrada. El <strong>componente de ruido</strong> se calcula restando la imagen filtrada de la original, lo que permite analizar qué ha sido eliminado.</p>
</li>
<li><p><strong>Visualización y validación</strong><br /> Se muestran los espectros antes y después del filtrado, junto con el espectro del ruido, lo que permite verificar visualmente la efectividad del proceso.</p>
</li>
</ol>
<pre><code class="lang-python">
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">RemoveQuasiperiodicNoise</span>(<span class="hljs-params">image, patch_size=<span class="hljs-number">128</span>, threshold=<span class="hljs-number">3.0</span>, fmax=<span class="hljs-number">0.61</span></span>):</span>
    <span class="hljs-string">"""
    Removes quasiperiodic noise from images using adaptive notch filtering.
    Based on the method by Sur &amp; Grédiac (2015) with practical adjustments.

    Parameters:
        image (np.ndarray): Input image in grayscale or color (BGR).
        patch_size (int): Size of the square patch for spectral analysis.
        threshold (float): Standard deviation factor for detecting noise peaks.
        fmax (float): Maximum frequency for noise detection.

    Returns:
        denoised_image (uint8): Filtered image (values in range 0-255).
        noise_component (uint8): Extracted noise component (values in range 0-255).
    """</span>
    <span class="hljs-comment"># Convert to grayscale and normalize to [0, 1]</span>
    <span class="hljs-keyword">if</span> image.ndim == <span class="hljs-number">3</span>:
        image = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
    image = image.astype(np.float32) / <span class="hljs-number">255.0</span>
    height, width = image.shape

    <span class="hljs-comment"># Adjust parameters according to image dimensions</span>
    patch_size = min(patch_size, height, width)
    step = max(<span class="hljs-number">1</span>, patch_size // <span class="hljs-number">8</span>)  <span class="hljs-comment"># Overlap L/8</span>
    f2 = <span class="hljs-number">8</span> / patch_size             <span class="hljs-comment"># Minimum frequency</span>

    <span class="hljs-comment"># Precompute Hann window</span>
    hann_window = np.outer(np.hanning(patch_size), np.hanning(patch_size))

    <span class="hljs-comment"># Extract patches and compute power spectra</span>
    patches = [
        image[y:y+patch_size, x:x+patch_size] * hann_window
        <span class="hljs-keyword">for</span> y <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, height - patch_size, step)
        <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, width - patch_size, step)
    ]
    power_spectra = np.array([np.abs(fftshift(fft2(p)))**<span class="hljs-number">2</span> <span class="hljs-keyword">for</span> p <span class="hljs-keyword">in</span> patches])

    <span class="hljs-comment"># Average power spectrum (geometric mean)</span>
    avg_power_spectrum = np.exp(np.mean(np.log(power_spectra + <span class="hljs-number">1e-10</span>), axis=<span class="hljs-number">0</span>))

    <span class="hljs-comment"># Radial frequencies</span>
    fy = np.fft.fftfreq(patch_size)[:, np.newaxis]
    fx = np.fft.fftfreq(patch_size)
    f = np.sqrt(fx**<span class="hljs-number">2</span> + fy**<span class="hljs-number">2</span>)
    valid_mask = (f &gt; f2 / <span class="hljs-number">4</span>) &amp; (f &lt; fmax)

    <span class="hljs-comment"># Robust fit of the power law</span>
    log_f = np.log(f[valid_mask]).reshape(<span class="hljs-number">-1</span>, <span class="hljs-number">1</span>)
    log_P = np.log(avg_power_spectrum[valid_mask]).ravel()
    model = HuberRegressor().fit(log_f, log_P)

    log_P_pred = model.predict(log_f)
    residuals = log_P - log_P_pred
    std_res = np.std(residuals)
    upper_bound = log_P_pred + threshold * std_res

    <span class="hljs-comment"># Noise peak detection</span>
    outliers = (log_P &gt; upper_bound) &amp; (f[valid_mask].ravel() &gt;= f2)

    <span class="hljs-comment"># Outlier map with symmetry</span>
    outlier_mask = np.zeros_like(avg_power_spectrum, dtype=bool)
    outlier_mask[valid_mask] = outliers
    outlier_mask |= np.flip(outlier_mask, axis=<span class="hljs-number">0</span>)
    outlier_mask |= np.flip(outlier_mask, axis=<span class="hljs-number">1</span>)

    <span class="hljs-comment"># Resize and smooth the mask</span>
    outlier_map = cv.resize(outlier_mask.astype(np.float32), (width, height), interpolation=cv.INTER_LINEAR)
    outlier_map = gaussian_filter(outlier_map, sigma=<span class="hljs-number">2.0</span>)

    <span class="hljs-comment"># Protect the DC component</span>
    cy, cx = height // <span class="hljs-number">2</span>, width // <span class="hljs-number">2</span>
    outlier_map[cy<span class="hljs-number">-1</span>:cy+<span class="hljs-number">2</span>, cx<span class="hljs-number">-1</span>:cx+<span class="hljs-number">2</span>] = <span class="hljs-number">0.0</span>

    <span class="hljs-comment"># Notch filtering</span>
    fft_image = fftshift(fft2(image))
    fft_filtered = fft_image * (<span class="hljs-number">1</span> - outlier_map)
    denoised_image = np.real(ifft2(ifftshift(fft_filtered)))
    noise_component = image - denoised_image

    <span class="hljs-comment"># Normalize and convert to uint8</span>
    denoised_image = np.clip(denoised_image * <span class="hljs-number">255</span>, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>).astype(np.uint8)
    noise_component = ((noise_component - noise_component.min()) / 
                       (noise_component.max() - noise_component.min()) * <span class="hljs-number">255</span>).astype(np.uint8)

    <span class="hljs-keyword">return</span> denoised_image, noise_component
</code></pre>
<hr />
<h3 id="heading-6-limitaciones-y-consideraciones-practicas"><strong>6. Limitaciones y consideraciones prácticas</strong></h3>
<ul>
<li><p><strong>Suposición de naturalidad</strong>: el modelo espectral está pensado para imágenes naturales. En datos sintéticos o experimentales, puede no ajustarse bien.</p>
</li>
<li><p><strong>Sensibilidad en frecuencias bajas</strong>: cuando el ruido está muy cerca de la frecuencia cero (DC), su separación respecto a la información útil se complica, pudiendo introducir pérdidas de detalle.</p>
</li>
<li><p><strong>Ruido de alta frecuencia</strong>: no aborda el ruido aleatorio fino ni patrones no periódicos.</p>
</li>
<li><p><strong>Confusión con detalles reales</strong>: si el patrón de ruido se parece a estructuras de la imagen (p. ej., líneas finas en la misma dirección), puede eliminar información válida o generar artefactos.</p>
</li>
</ul>
<blockquote>
<p>El algoritmo ofrece parámetros ajustables, como el tamaño de parche, el umbral de detección y el ancho del suavizado, que permiten optimizar su rendimiento según el tipo de imagen. Sin embargo, estos ajustes deben hacerse con cuidado, especialmente cuando se intenta eliminar frecuencias cercanas a DC, donde existe</p>
</blockquote>
<hr />
<h2 id="heading-7-resultados"><strong>7. Resultados</strong></h2>
<p>Derecha: imagen original, Centro: image resultante, Izquierda: ruido filtrado.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754791339691/1dd81753-2c50-4f14-99be-34f4228ec3b0.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754792376405/b07c59cc-64ab-4914-80b9-1e02b19536fc.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754793191401/5fd6732d-7b42-43f1-90de-b7578eb6b0d3.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754793931285/79a6dd16-f809-4a36-b4fb-f89bda4b8e31.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-8-conclusiones"><strong>8. Conclusiones</strong></h2>
<p>El filtrado automático de ruido cuasiperiódico mediante análisis estadístico del espectro promedio ofrece una alternativa potente a los métodos manuales tradicionales. Su capacidad para identificar picos espurios sin intervención humana lo convierte en una herramienta ideal para flujos de trabajo donde se procesan grandes volúmenes de imágenes.</p>
<p>Si bien no es infalible —especialmente en escenas no naturales o cuando el ruido se confunde con detalles reales—, en la mayoría de los casos logra un equilibrio eficaz entre limpieza y preservación de la información visual. Además, su naturaleza adaptativa le permite enfrentarse a patrones complejos y distribuciones irregulares de ruido que serían tediosas de manejar de forma manual.</p>
<p><strong>bibliografía</strong></p>
<blockquote>
<p>Frédéric Sur and Michel Grediac "Automated removal of quasiperiodic noise using frequency domain statistics," <em>Journal of Electronic Imaging</em> 24(1), 013003 (11 February 2015). <a target="_blank" href="https://doi.org/10.1117/1.JEI.24.1.013003">https://doi.org/10.1117/1.JEI.24.1.013003</a></p>
</blockquote>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[Filtrado en el Dominio de la Frecuencia: Fundamentos y Aplicaciones]]></title><description><![CDATA[Las operaciones de filtrado suelen realizarse en el dominio espacial, a través de convoluciones con máscaras (o kernels) que operan directamente sobre los píxeles. Sin embargo, existe una alternativa: el filtrado en el dominio de la frecuencia, basad...]]></description><link>https://codigoenllamas.com/filtrado-en-el-dominio-de-la-frecuencia-fundamentos-y-aplicaciones</link><guid isPermaLink="true">https://codigoenllamas.com/filtrado-en-el-dominio-de-la-frecuencia-fundamentos-y-aplicaciones</guid><category><![CDATA[Computer Vision]]></category><category><![CDATA[opencv]]></category><category><![CDATA[Python]]></category><category><![CDATA[image processing]]></category><category><![CDATA[Mathematics]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sun, 27 Jul 2025 21:36:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/QCp1SboQ2Es/upload/ee8cd4ac49e9fbec55e1d42c9991865e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Las operaciones de filtrado suelen realizarse en el <strong>dominio espacial</strong>, a través de convoluciones con máscaras (o kernels) que operan directamente sobre los píxeles. Sin embargo, existe una alternativa: el <strong>filtrado en el dominio de la frecuencia</strong>, basado en la Transformada Discreta de Fourier (DFT).</p>
<p>Este enfoque aprovecha una propiedad fundamental de la teoría de señales: el <strong>teorema de convolución circular</strong>, que establece que la convolución espacial entre dos señales es equivalente a la multiplicación de sus representaciones en frecuencia. Esto permite transformar una operación local —como un desenfoque— en una operación global pero computacionalmente eficiente. En el caso de imágenes, esto significa que podemos aplicar un filtro multiplicando espectros, para luego recuperar la imagen filtrada mediante la transformada inversa.</p>
<p>Además de la eficiencia, una de las grandes ventajas del dominio frecuencial es la <strong>intuitividad en el diseño de filtros</strong>. Es más sencillo entender cómo un filtro afecta una imagen al observar su respuesta en frecuencia que al inspeccionar directamente los valores de un kernel en el espacio. Esta perspectiva permite crear filtros como el <strong>pasa-bajos ideal</strong>, el <strong>filtro de Butterworth</strong>, o el <strong>filtro homomórfico</strong>, cada uno con aplicaciones específicas que van desde la restauración de imágenes degradadas hasta el realce de detalles.</p>
<h2 id="heading-1-fundamentos-del-filtrado-en-el-dominio-de-la-frecuencia">1. Fundamentos del Filtrado en el Dominio de la Frecuencia</h2>
<h3 id="heading-11-equivalencia-entre-convolucion-espacial-y-multiplicacion-frecuencial">1.1. Equivalencia entre convolución espacial y multiplicación frecuencial</h3>
<p>Uno de los pilares del análisis en frecuencia es el <strong>teorema de la convolución</strong>, que establece una relación directa entre la convolución en el dominio espacial y la multiplicación en el dominio frecuencial. Formalmente, si se tiene una imagen <em>g(x, y)</em> y un filtro espacial <em>h(x, y)</em>, su convolución se denota como:</p>
<p>$$g_r(x, y) = g(x, y) * h(x, y)$$</p><p>donde * representa la operación de convolución bidimensional. La <strong>Transformada de Fourier</strong> convierte esta operación en una simple multiplicación:</p>
<p>$$G_r(k_x, k_y) = G(k_x, k_y) \cdot H(k_x, k_y)$$</p><p>donde:</p>
<p>$$\begin{align*} G(k_x, k_y) &amp; \quad \text{: transformada de Fourier de la imagen original}, \\ H(k_x, k_y) &amp; \quad \text{: transformada de Fourier del kernel de filtrado}, \\ G_r(k_x, k_y) &amp; \quad \text{: transformada de Fourier de la imagen resultante}. \end{align*}$$</p><p>Esta propiedad es especialmente útil porque muchas operaciones de convolución que en el dominio espacial requieren recorrer cada píxel y aplicar un kernel local, en el dominio de la frecuencia se reducen a multiplicaciones punto a punto entre matrices de la misma dimensión.</p>
<p>Cabe mencionar que, debido a la naturaleza discreta y finita de las imágenes digitales, esta relación se da en términos de <strong>convolución circular</strong>. Para que la equivalencia sea válida en la práctica, es necesario aplicar <strong>relleno con ceros (zero-padding)</strong> antes de transformar las imágenes, de modo que se eviten artefactos de aliasing o envolvimiento (wrapping) al realizar la multiplicación en frecuencia.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ApplyFrequencyDomainFilter</span>(<span class="hljs-params">image, kernel</span>):</span>
    <span class="hljs-string">"""
    Applies a frequency domain filter to a grayscale image.

    This function computes the 2D Fourier Transform of the input image, applies the given filter 
    in the frequency domain, and then performs the inverse Fourier Transform to return the filtered image.

    Parameters:
    ----------
    image : np.ndarray
        Input grayscale image (2D numpy array).

    kernel : np.ndarray
        Frequency domain filter (2D numpy array) with the same shape as the input image.

    Returns:
    -------
    filtered_image : np.ndarray
        Filtered image (uint8) normalized to the range [0, 255].

    Notes:
    -----
    - The input image is assumed to be in grayscale format.
    - The kernel should be designed in the frequency domain and have the same dimensions as the input image.
    - The output image is normalized to ensure proper visualization.
    """</span>
    <span class="hljs-comment"># Compute the 2D Fourier Transform of the image</span>
    f = np.fft.fft2(image)
    fshift = np.fft.fftshift(f)  <span class="hljs-comment"># Shift zero frequency to the center</span>

    <span class="hljs-comment"># Apply the filter in the frequency domain</span>
    filtered_freq = fshift * kernel

    <span class="hljs-comment"># Compute the inverse Fourier Transform to return to the spatial domain</span>
    temp = np.abs(np.fft.ifft2(np.fft.ifftshift(filtered_freq)))

    <span class="hljs-comment"># Normalize the result to the range [0, 255] and convert to uint8</span>
    filtered_image = cv.normalize(temp, <span class="hljs-literal">None</span>, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>, cv.NORM_MINMAX)
    filtered_image = np.uint8(filtered_image)

    <span class="hljs-keyword">return</span> filtered_image
</code></pre>
<p>Esta equivalencia no solo es una herramienta matemática elegante, sino que fundamenta toda una clase de técnicas de filtrado frecuencial utilizadas tanto en restauración como en mejoramiento de imágenes.</p>
<hr />
<h3 id="heading-12-ventajas-del-dominio-frecuencial">1.2. Ventajas del dominio frecuencial</h3>
<p>El filtrado en el dominio de la frecuencia presenta ventajas claras sobre el filtrado espacial en ciertos contextos:</p>
<ul>
<li><p><strong>Diseño intuitivo de filtros</strong>: En el dominio espacial, los kernels de convolución pueden parecer arbitrarios o difíciles de interpretar. Por el contrario, en el dominio de la frecuencia, los filtros se diseñan directamente en función de las componentes espectrales que se desean atenuar o resaltar. Por ejemplo, un filtro pasa-bajos simplemente bloquea las frecuencias altas que corresponden a detalles finos o ruido, mientras deja pasar las bajas frecuencias responsables de las estructuras globales.</p>
</li>
<li><p><strong>Eficiencia computacional con kernels grandes</strong>: Aunque la Transformada de Fourier y su inversa requieren procesamiento adicional, la <strong>Transformada Rápida de Fourier (FFT)</strong> permite implementaciones altamente eficientes. Cuando el kernel de convolución es grande, realizar una convolución directa en el dominio espacial tiene un costo computacional de <em>Ο(N² M²)</em> para una imagen de tamaño <em>N × N</em> y un kernel de tamaño <em>M × M</em>. En cambio, la transformación a frecuencia, multiplicación espectral y reconversión por FFT se realiza en <em>Ο(N² log N)</em>, lo cual es más eficiente para kernels grandes.</p>
</li>
</ul>
<p>Sin embargo, es importante notar que esta ventaja se diluye para kernels pequeños (por ejemplo, de 3x3 o 5x5), donde la convolución directa en el dominio espacial puede ser más rápida. Además, muchas aplicaciones modernas utilizan enfoques multiescala o convoluciones separables que reducen el costo espacial sin necesidad de transformarse al dominio frecuencial.</p>
<h2 id="heading-2-tipos-de-filtrado-y-aplicaciones">2. Tipos de Filtrado y Aplicaciones</h2>
<p>El filtrado en el dominio de la frecuencia permite realizar tanto <strong>restauración</strong> como <strong>mejoramiento</strong> de imágenes. Dependiendo de cómo se diseñe la respuesta en frecuencia del filtro, se pueden atenuar detalles finos, resaltar bordes, eliminar ruido o incluso modificar propiedades de iluminación. A continuación se describen los tipos más comunes de filtros frecuenciales, sus fundamentos matemáticos y sus aplicaciones prácticas.</p>
<h3 id="heading-21-restauracion-vs-mejoramiento">2.1. Restauración vs. Mejoramiento</h3>
<p>Es importante distinguir entre dos objetivos principales del filtrado:</p>
<ul>
<li><p><strong>Restauración</strong>: Busca reconstruir la imagen original eliminando distorsiones o ruido que hayan degradado la calidad de la imagen. Este tipo de filtrado suele requerir una estimación del proceso de degradación (modelo del sistema de adquisición o transmisión) y es más común en aplicaciones científicas o médicas.</p>
</li>
<li><p><strong>Mejoramiento (enhancement)</strong>: No busca recuperar una "verdadera imagen", sino hacerla más útil para una tarea específica, como mejorar la visibilidad de bordes, estructuras o texturas. Es frecuente en aplicaciones de visión artificial y fotografía.</p>
</li>
</ul>
<p>Ambos enfoques pueden beneficiarse del análisis en frecuencia, ya que permiten aislar las escalas o rangos espectrales relevantes a la tarea.</p>
<hr />
<h3 id="heading-22-filtros-pasa-bajos-lowpass">2.2. Filtros Pasa-Bajos (Lowpass)</h3>
<p>Los <strong>filtros pasa-bajos</strong> permiten el paso de las bajas frecuencias y atenúan las altas. Su función principal es <strong>suavizar</strong> la imagen, reduciendo el ruido o eliminando detalles finos.</p>
<h4 id="heading-filtro-ideal">Filtro ideal</h4>
<p>La versión más simple y teóricamente pura es el <strong>filtro pasa-bajos ideal</strong>, definido en el dominio de la frecuencia como:</p>
<p>$$H_{\text{ideal}}(f)| = \begin{cases} 1, &amp; \text{si } |f| \leq f_c \\ 0, &amp; \text{si } |f| &gt; f_c \end{cases}$$</p><p>donde <em>fc</em> es la frecuencia de corte. Este filtro elimina completamente todas las frecuencias por encima de <em>fc</em>. Sin embargo, su implementación práctica es problemática: la transformada inversa de este filtro da lugar a una <strong>función sinc</strong> en el dominio espacial, que se extiende infinitamente y presenta <strong>efectos de oscilación (ringing)</strong> debido al <strong>fenómeno de Gibbs</strong>.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateIdealLowpassFilter</span>(<span class="hljs-params">shape, cutoff_frequency</span>):</span>
    <span class="hljs-string">"""
    Creates an ideal low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Cutoff frequency for the low-pass filter.

    Returns:
    -------
    filter_kernel : np.ndarray
        Ideal low-pass filter kernel as a 2D numpy array.
    """</span>
    rows, cols = shape
    crow, ccol = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**<span class="hljs-number">2</span> + (Y - crow)**<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Ideal low-pass filter formula</span>
    filter_kernel = distance &lt;= cutoff_frequency
    <span class="hljs-keyword">return</span> filter_kernel.astype(np.float32)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753562819709/a199c1c0-6d2e-4468-96ea-819bb5d96700.png" alt /></p>
<h4 id="heading-filtro-gaussiano">Filtro Gaussiano</h4>
<p>Una alternativa más suave es el <strong>filtro gaussiano</strong>, cuya respuesta en frecuencia está dada por:</p>
<p>$$H_{\text{gauss}}(f)| = e^{-\frac{f^2}{2\sigma^2}}$$</p><p>Este filtro tiene la ventaja de no presentar ringing y de ser separable (puede aplicarse por filas y columnas), lo cual lo hace computacionalmente eficiente. Sin embargo, su desventaja es su <strong>transición suave</strong>, lo que implica un menor control sobre las frecuencias que se atenúan o conservan.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateGaussianLowpassFilter</span>(<span class="hljs-params">shape, cutoff_frequency</span>):</span>
    <span class="hljs-string">"""
    Creates a Gaussian low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Cutoff frequency for the low-pass filter.

    Returns:
    -------
    filter_kernel : np.ndarray
        Gaussian low-pass filter kernel as a 2D numpy array.
    """</span>
    rows, cols = shape
    crow, ccol = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**<span class="hljs-number">2</span> + (Y - crow)**<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Gaussian low-pass filter formula</span>
    filter_kernel = np.exp(-(distance**<span class="hljs-number">2</span>) / (<span class="hljs-number">2</span> * (cutoff_frequency**<span class="hljs-number">2</span>)))
    <span class="hljs-keyword">return</span> filter_kernel.astype(np.float32)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753562879300/0c3cccab-178a-4f61-ac07-980eb3916d67.jpeg" alt /></p>
<h4 id="heading-filtro-de-butterworth">Filtro de Butterworth</h4>
<p>Este filtro busca un compromiso entre la transición abrupta del filtro ideal y la suavidad del gaussiano. Su respuesta está definida como:</p>
<p>$$H_{\text{bw}}(f)| = \frac{1}{1 + \left( \frac{f}{f_c} \right)^{2n}}$$</p><p>donde <em>n</em> es el <strong>orden del filtro</strong>, que controla la pendiente de la caída en la banda de transición. Es conocido como un filtro <strong>"máximamente plano"</strong>, ya que no presenta ondulaciones (ripple) en la banda pasante.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateButterworthLowpassFilter</span>(<span class="hljs-params">shape, cutoff_frequency, order</span>):</span>
    <span class="hljs-string">"""
    Creates a Butterworth low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Cutoff frequency for the low-pass filter.
    order : int
        Order of the Butterworth filter, controlling the sharpness of the transition.

    Returns:
    -------
    filter_kernel : np.ndarray
        Butterworth low-pass filter kernel as a 2D numpy array.
    """</span>
    rows, cols = shape
    crow, ccol = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**<span class="hljs-number">2</span> + (Y - crow)**<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Butterworth low-pass filter formula</span>
    filter_kernel = <span class="hljs-number">1</span> / (<span class="hljs-number">1</span> + (distance / (cutoff_frequency + <span class="hljs-number">1e-5</span>))**(<span class="hljs-number">2</span> * order))  <span class="hljs-comment"># Avoid division by zero</span>
    <span class="hljs-keyword">return</span> filter_kernel.astype(np.float32)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753562951018/306760c5-eea7-4c10-9af7-cc30873a0829.jpeg" alt /></p>
<h4 id="heading-filtro-de-lanczos">Filtro de Lanczos</h4>
<p>El filtro de Lanczos surge principalmente en tareas de reescalado de imágenes y reconstrucción, y está basado en una ventana truncada de la función sinc:</p>
<p>$$h(x) = \text{sinc}(x) \cdot \text{sinc}\left(\frac{x}{a}\right)$$</p><p>donde <em>a</em> es un parámetro que controla el ancho de la ventana. En frecuencia, ofrece una buena supresión de aliasing con una transición más nítida que el gaussiano, pero con menor ringing que el filtro ideal.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateLanczosLowpassFilter</span>(<span class="hljs-params">shape, cutoff_frequency, a=<span class="hljs-number">3</span></span>):</span>
    <span class="hljs-string">"""
    Creates a Lanczos low-pass filter kernel in the frequency domain.

    Parameters:
    ----------
    shape : tuple
        Shape of the filter (rows, cols), typically matching the image dimensions.
    cutoff_frequency : float
        Frequency scaling factor (controls sharpness).
    a : int
        Lanczos window parameter (commonly 2 or 3). Larger values = narrower main lobe.

    Returns:
    -------
    filter_kernel : np.ndarray
        Lanczos low-pass filter kernel as a 2D numpy array.
    """</span>
    rows, cols = shape
    crow, ccol = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>

    <span class="hljs-comment"># Coordenadas relativas al centro</span>
    Y, X = np.ogrid[:rows, :cols]
    dx = X - ccol
    dy = Y - crow
    radius = np.sqrt(dx**<span class="hljs-number">2</span> + dy**<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Normalizar el radio para hacerlo compatible con el parámetro a</span>
    x = (radius / cutoff_frequency).astype(np.float32)

    <span class="hljs-comment"># sinc(x) = sin(pi x) / (pi x), definida como 1 en x = 0</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sinc</span>(<span class="hljs-params">z</span>):</span>
        z = np.where(z == <span class="hljs-number">0</span>, <span class="hljs-number">1e-8</span>, z)  <span class="hljs-comment"># evitar división por cero</span>
        <span class="hljs-keyword">return</span> np.sin(np.pi * z) / (np.pi * z)

    <span class="hljs-comment"># Kernel de Lanczos en 2D: sinc(x) * sinc(x/a)</span>
    lanczos_kernel = sinc(x) * sinc(x / a)

    <span class="hljs-comment"># Forzar ceros fuera de la ventana a</span>
    lanczos_kernel[x &gt; a] = <span class="hljs-number">0</span>

    <span class="hljs-keyword">return</span> lanczos_kernel.astype(np.float32)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753562997312/9e33603b-d9f7-49ba-8396-41b627b848f1.jpeg" alt /></p>
<h3 id="heading-23-filtrado-de-imagenes-a-color-en-el-dominio-de-la-frecuencia">2.3 Filtrado de Imágenes a Color en el Dominio de la Frecuencia</h3>
<p>Al extender el filtrado en el dominio de la frecuencia a imágenes a color, una estrategia directa consiste en aplicar el mismo filtro frecuencial a cada canal (B, G, R) de forma independiente. La función <code>ApplyFrequencyDomainFilterBGR</code> realiza exactamente esto: procesa por separado cada componente de color, aplica el filtro en frecuencia y luego los vuelve a combinar. Aunque este método es sencillo y coherente con el filtrado en escala de grises, puede introducir ligeras distorsiones de color, ya que cada canal se modifica sin considerar su relación perceptual con los otros. Para evitar este problema, se pueden utilizar espacios de color alternativos como <strong>Lab</strong> o <strong>HSV</strong>, en los que se aplica el filtrado únicamente sobre el componente de luminancia, preservando así mejor la fidelidad del color original.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ApplyFrequencyDomainFilterBGR</span>(<span class="hljs-params">image_bgr, kernel</span>):</span>
    <span class="hljs-string">"""
    Applies a frequency domain filter to each BGR channel of a color image independently.

    Parameters:
        image_bgr : np.ndarray
            Input color image (H x W x 3) in uint8 format.

        kernel : np.ndarray
            Frequency domain filter (2D array) of shape (H, W).

    Returns:
        np.ndarray:
            Filtered BGR image (uint8), same size as input.
    """</span>
    <span class="hljs-keyword">if</span> image_bgr.ndim != <span class="hljs-number">3</span> <span class="hljs-keyword">or</span> image_bgr.shape[<span class="hljs-number">2</span>] != <span class="hljs-number">3</span>:
        <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"Input image must be BGR (H x W x 3)."</span>)

    filtered_channels = []
    <span class="hljs-keyword">for</span> c <span class="hljs-keyword">in</span> range(<span class="hljs-number">3</span>):
        channel = image_bgr[:, :, c]
        filtered = ApplyFrequencyDomainFilter(channel, kernel)
        filtered_channels.append(filtered)

    <span class="hljs-keyword">return</span> cv.merge(filtered_channels)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753563055390/7699dbba-e5c2-4ec1-8d45-9da659f0e338.png" alt /></p>
<h3 id="heading-24-filtros-pasa-altos-highpass">2.4. Filtros Pasa-Altos (Highpass)</h3>
<p>Los <strong>filtros pasa-altos</strong> cumplen la función opuesta a los pasa-bajos: <strong>atenúan las bajas frecuencias</strong> (responsables de estructuras suaves y cambios graduales) y <strong>conservan o enfatizan las altas frecuencias</strong>, que suelen corresponder a bordes, texturas finas y detalles locales.</p>
<p>Una forma directa de obtener un filtro pasa-alto es <strong>restar</strong> un filtro pasa-bajo de una función constante:</p>
<p>$$H_{\text{highpass}}(f)| = 1 - |H_{\text{lowpass}}(f)$$</p><p>Esto permite generar versiones pasa-altas correspondientes a cualquier diseño pasa-bajo conocido, como los ideales, gaussianos o Butterworth.</p>
<h4 id="heading-filtro-ideal-1">Filtro Ideal</h4>
<p>Similar al caso pasa-bajo, el filtro pasa-alto ideal se define como:</p>
<p>$$H_{\text{highpass}}(f)| = 1 - |H_{\text{lowpass}}(f)$$</p><p>En el dominio espacial, este filtro genera oscilaciones significativas (ringing) alrededor de los bordes y también se ve afectado por el fenómeno de Gibbs, por lo que rara vez se implementa directamente.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateIdealHighpassFilter</span>(<span class="hljs-params">shape, cutoff_frequency</span>):</span>
    <span class="hljs-string">"""
    Creates an ideal high-pass filter kernel in the frequency domain.

    Parameters:
        shape : tuple
            Shape of the filter (rows, cols), typically matching the image dimensions.
        cutoff_frequency : float
            Cutoff frequency for the high-pass filter.

    Returns:
        filter_kernel : np.ndarray
            Ideal high-pass filter kernel as a 2D numpy array.
    """</span>

    rows, cols = shape
    crow, ccol = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**<span class="hljs-number">2</span> + (Y - crow)**<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Ideal high-pass filter formula</span>
    filter_kernel = distance &gt; cutoff_frequency
    <span class="hljs-keyword">return</span> filter_kernel.astype(np.float32)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753563193747/2df68eba-7e8a-48d8-a3d2-dae1ccd2151e.jpeg" alt /></p>
<h4 id="heading-filtro-gaussiano-1">Filtro Gaussiano</h4>
<p>La versión pasa-alta del filtro gaussiano se obtiene de manera complementaria:</p>
<p>$$H_{\text{gauss-high}}(f)| = 1 - e^{-\frac{f^2}{2\sigma^2}}$$</p><p>Este filtro es útil para <strong>detectar bordes suaves o graduales</strong>, especialmente cuando se requiere evitar artefactos de sobre-resaltado.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateGaussianHighpassFilter</span>(<span class="hljs-params">shape, cutoff_frequency</span>):</span>
    <span class="hljs-string">"""
    Creates a Gaussian high-pass filter kernel in the frequency domain.

    Parameters:
        shape : tuple
            Shape of the filter (rows, cols), typically matching the image dimensions.
        cutoff_frequency : float
            Cutoff frequency for the high-pass filter.

    Returns:
        filter_kernel : np.ndarray
            Gaussian high-pass filter kernel as a 2D numpy array.
    """</span>
    rows, cols = shape
    crow, ccol = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**<span class="hljs-number">2</span> + (Y - crow)**<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Gaussian high-pass filter formula</span>
    lowpass_kernel = np.exp(-(distance**<span class="hljs-number">2</span>) / (<span class="hljs-number">2</span> * (cutoff_frequency**<span class="hljs-number">2</span>)))
    highpass_kernel = <span class="hljs-number">1</span> - lowpass_kernel
    <span class="hljs-keyword">return</span> highpass_kernel.astype(np.float32)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753563236606/698e71aa-1bfd-47b4-836e-55ac88311d4a.jpeg" alt /></p>
<h4 id="heading-filtro-de-butterworth-1">Filtro de Butterworth</h4>
<p>El filtro pasa-alto de Butterworth se define como:</p>
<p>$$H_{\text{bw}}(f)| = \frac{1}{1 + \left( \frac{f_c}{f} \right)^{2n}}$$</p><p>Este diseño, con su control por orden <em>n</em>, permite ajustar finamente el compromiso entre nitidez y estabilidad numérica. A diferencia del filtro ideal, el de Butterworth presenta una <strong>transición continua y suave</strong>, evitando oscilaciones excesivas.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateButterworthHighpassFilter</span>(<span class="hljs-params">shape, cutoff_frequency, order</span>):</span>
    <span class="hljs-string">"""
    Creates a Butterworth high-pass filter kernel in the frequency domain.

    Parameters:
        shape : tuple
            Shape of the filter (rows, cols), typically matching the image dimensions.
        cutoff_frequency : float
            Cutoff frequency for the high-pass filter.
        order : int
            Order of the Butterworth filter, controlling the sharpness of the transition.

    Returns:
        filter_kernel : np.ndarray
            Butterworth high-pass filter kernel as a 2D numpy array.
    """</span>
    rows, cols = shape
    crow, ccol = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>
    Y, X = np.ogrid[:rows, :cols]
    distance = np.sqrt((X - ccol)**<span class="hljs-number">2</span> + (Y - crow)**<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Butterworth high-pass filter formula</span>
    filter_kernel = <span class="hljs-number">1</span> / (<span class="hljs-number">1</span> + (cutoff_frequency / (distance + <span class="hljs-number">1e-5</span>))**(<span class="hljs-number">2</span> * order))  <span class="hljs-comment"># Avoid division by zero</span>
    <span class="hljs-keyword">return</span> filter_kernel.astype(np.float32)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753563301901/a63d6ec7-dbfc-4721-a820-79fec0a7cd20.jpeg" alt /></p>
<h4 id="heading-aplicaciones">Aplicaciones</h4>
<p>Los filtros pasa-altos son esenciales en tareas como:</p>
<ul>
<li><p><strong>Detección de bordes</strong>: Las transiciones abruptas en la intensidad de la imagen se traducen en frecuencias altas, que estos filtros conservan o acentúan. Aunque los métodos espaciales como Sobel o Laplaciano son comunes, los enfoques frecuenciales permiten un control más preciso de la respuesta espectral.</p>
</li>
<li><p><strong>Realce de detalles (Detail Enhancement)</strong>: Al aplicar un filtro pasa-alto y sumarlo nuevamente a la imagen original, se pueden enfatizar detalles sin perder información global. Esta técnica es la base del <strong>enmascaramiento no agudo</strong> y otras estrategias de mejora visual.</p>
</li>
</ul>
<h3 id="heading-25-filtros-pasa-banda-bandpass">2.5. Filtros Pasa-Banda (Bandpass)</h3>
<p>Los <strong>filtros pasa-banda</strong> están diseñados para <strong>aislar un rango específico de frecuencias</strong>, bloqueando tanto las bajas como las altas. Son útiles cuando se desea conservar estructuras que se encuentren a una <strong>escala intermedia</strong>, ignorando patrones demasiado gruesos o demasiado finos.</p>
<p>Una implementación clásica de filtro pasa-banda se logra como la diferencia entre un filtro pasa-bajo y un filtro pasa-alto con distintas frecuencias de corte. Pero existen también filtros diseñados directamente para resaltar componentes de ciertas frecuencias, como el <strong>Laplaciano de Gaussiano</strong> y técnicas de <strong>enmascaramiento no agudo</strong>.</p>
<hr />
<h4 id="heading-filtro-laplaciano-de-gaussiano-log">Filtro Laplaciano de Gaussiano (LoG)</h4>
<p>El filtro LoG surge de la combinación de dos operaciones: suavizado mediante un filtro gaussiano, seguido por la aplicación del operador Laplaciano (segunda derivada). Aunque originalmente se define en el dominio espacial, también tiene una expresión directa en frecuencia:</p>
<p>$$H_{\text{LoG}}(f)| = -f^2 \cdot e^{-\frac{f^2}{2f_c^2}}$$</p><p>Este filtro actúa como un detector de bordes, pero a diferencia de los filtros pasa-altos convencionales, <strong>resalta frecuencias intermedias</strong> y <strong>suprime tanto bajas como altas</strong>. De ahí su naturaleza pasa-banda.</p>
<ul>
<li><p>La parte f² enfatiza componentes de frecuencia creciente.</p>
</li>
<li><p>El término exponencial atenúa frecuencias más allá del umbral <em>fc.</em></p>
</li>
</ul>
<p>Este filtro también es <strong>isotrópico</strong> (invariante a rotaciones) y ampliamente utilizado en visión por computadora y reconocimiento de patrones, como en la detección de blobs.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateLaplacianOfGaussianFilter</span>(<span class="hljs-params">shape, cutoff_freq</span>):</span>
    <span class="hljs-string">"""
    Create a Laplacian of Gaussian (LoG) filter in the frequency domain.

    Parameters:
        shape        : tuple, (height, width) of the image
        cutoff_freq  : float, frequency cutoff (f_c) that controls the Gaussian spread

    Returns:
        log_filter   : 2D numpy array with the filter in the frequency domain
    """</span>
    rows, cols = shape
    cy, cx = rows // <span class="hljs-number">2</span>, cols // <span class="hljs-number">2</span>

    <span class="hljs-comment"># Create frequency grids centered at (0,0)</span>
    u = np.fft.fftfreq(cols).reshape(<span class="hljs-number">1</span>, <span class="hljs-number">-1</span>)
    v = np.fft.fftfreq(rows).reshape(<span class="hljs-number">-1</span>, <span class="hljs-number">1</span>)

    <span class="hljs-comment"># Shift the frequency grids so that (0,0) is at the center</span>
    u = np.fft.fftshift(u)
    v = np.fft.fftshift(v)

    <span class="hljs-comment"># Compute squared frequency radius: f^2 = u^2 + v^2</span>
    f_squared = u**<span class="hljs-number">2</span> + v**<span class="hljs-number">2</span>

    <span class="hljs-comment"># Laplacian of Gaussian filter in frequency domain</span>
    log_filter = <span class="hljs-number">-4</span> * (np.pi**<span class="hljs-number">2</span>) * f_squared * np.exp(-f_squared / (<span class="hljs-number">2</span> * (cutoff_freq ** <span class="hljs-number">2</span>)))

    <span class="hljs-keyword">return</span> log_filter
</code></pre>
<hr />
<h4 id="heading-enmascaramiento-no-agudo-unsharp-masking">Enmascaramiento no agudo (Unsharp Masking)</h4>
<p>A pesar de su nombre, el enmascaramiento no agudo (unsharp masking) es una técnica para <strong>agudizar</strong> (realzar) los detalles de una imagen. Su funcionamiento se basa en extraer las componentes de alta frecuencia y sumarlas de nuevo a la imagen original:</p>
<p>$$g_{\text{realzada}}(x, y) = g(x, y) + \alpha \cdot \left[g(x, y) - g_{\text{suavizada}}(x, y)\right]$$</p><p>Este esquema puede verse como:</p>
<p>$$g_{\text{realzada}} = (1 + \alpha) \cdot g - \alpha \cdot (g * h)$$</p><p>donde <em>h</em> es un filtro pasa-bajo (por ejemplo, Gaussiano) y <em>α</em> un parámetro de realce.</p>
<p>En frecuencia, esta operación corresponde a aplicar un filtro con respuesta:</p>
<p>$$H_{\text{unsharp}}(f) = 1 + \alpha \cdot \left[1 - H_{\text{lowpass}}(f)\right]$$</p><p>Esto da como resultado una <strong>respuesta pasa-banda modificada</strong>, que enfatiza un rango intermedio de frecuencias con ganancia ajustable.</p>
<ul>
<li><p><strong>Ventajas</strong>: control preciso sobre el nivel de realce.</p>
</li>
<li><p><strong>Aplicaciones</strong>: mejora de detalles en imágenes médicas, documentos escaneados, o fotografía digital.</p>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CreateUnsharpMaskingFilter</span>(<span class="hljs-params">shape, cutoff_freq, alpha=<span class="hljs-number">1.0</span>, method=<span class="hljs-string">'gaussian'</span></span>):</span>
    <span class="hljs-string">"""
    Create an unsharp masking filter in the frequency domain.

    Parameters:
        shape        : tuple, (height, width) of the image
        cutoff_freq  : float, cutoff frequency for the lowpass component
        alpha        : float, sharpening factor (&gt;0)
        method       : str, type of lowpass ('gaussian', 'ideal', 'butterworth')

    Returns:
        H_unsharp    : 2D numpy array with the unsharp masking filter
    """</span>
    <span class="hljs-keyword">if</span> method == <span class="hljs-string">'gaussian'</span>:
        H_lowpass = CreateGaussianLowpassFilter(shape, cutoff_freq)
    <span class="hljs-keyword">elif</span> method == <span class="hljs-string">'ideal'</span>:
        H_lowpass = CreateIdealLowpassFilter(shape, cutoff_freq)
    <span class="hljs-keyword">elif</span> method == <span class="hljs-string">'butterworth'</span>:
        H_lowpass = CreateButterworthLowpassFilter(shape, cutoff_freq, order=<span class="hljs-number">2</span>)
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">"Unsupported method. Choose 'gaussian', 'ideal', or 'butterworth'."</span>)

    <span class="hljs-comment"># Unsharp masking filter: H_unsharp(f) = 1 + alpha * (1 - H_lowpass(f))</span>
    H_unsharp = <span class="hljs-number">1</span> + alpha * (<span class="hljs-number">1</span> - H_lowpass)

    <span class="hljs-keyword">return</span> H_unsharp
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753563473555/545e256a-a9cd-449d-b1d1-5461f1ce874f.jpeg" alt /></p>
<h3 id="heading-26-filtrado-homomorfico">2.6. Filtrado Homomórfico</h3>
<p>En muchas imágenes del mundo real, especialmente en condiciones de iluminación natural, la intensidad registrada por el sensor es una combinación de dos factores principales:</p>
<ul>
<li><p><strong>Iluminación</strong> <em>(L(x, y)):</em> una componente de variación lenta, asociada con las condiciones externas de luz, sombras suaves y gradientes globales.</p>
</li>
<li><p><strong>Reflectancia</strong> <em>(R(x, y))</em>: una componente de variación rápida, relacionada con los detalles locales, texturas y estructuras intrínsecas de la escena.</p>
</li>
</ul>
<p>El modelo multiplicativo que describe esta relación es:</p>
<p>$$E(x, y) = L(x, y) \cdot R(x, y)$$</p><p>Este modelo es problemático para el análisis frecuencial directo, ya que la Transformada de Fourier <strong>no maneja productos de funciones</strong> de forma directa. Para resolver esto, el filtrado homomórfico transforma el modelo multiplicativo en uno aditivo mediante un logaritmo:</p>
<p>$$log E(x, y) = \log L(x, y) + \log R(x, y)$$</p><p>Una vez en esta forma, es posible aplicar un <strong>filtro frecuencial</strong> que suprima las bajas frecuencias (iluminación) y preserve o realce las altas (reflectancia). Típicamente, se utiliza un filtro pasa-alto suave o un filtro pasa-banda con énfasis controlado.</p>
<h4 id="heading-proceso-general">Proceso general:</h4>
<ol>
<li><p>Aplicar logaritmo a la imagen:</p>
<p> $$s(x, y) = \log E(x, y)$$</p>
</li>
<li><p>Transformar al dominio de la frecuencia:</p>
<p> $$S(k_x, k_y) = \mathcal{F}{s(x, y)}$$</p>
</li>
<li><p>Aplicar un filtro <em>H(kₓ , ky)</em> que atenúe las bajas frecuencias:</p>
<p> $$S_r(k_x, k_y) = S(k_x, k_y) \cdot H(k_x, k_y)$$</p>
</li>
<li><p>Transformar de regreso al dominio espacial e invertir el logaritmo:</p>
<p> $$E_r(x, y) = \exp\left(\mathcal{F}^{-1}{S_r(k_x, k_y)}\right)$$</p>
</li>
</ol>
<h4 id="heading-resultados-y-aplicaciones">Resultados y aplicaciones</h4>
<ul>
<li><p><strong>Realce de detalles en sombras</strong>: al atenuar la iluminación global, se pueden destacar detalles que de otro modo quedarían ocultos.</p>
</li>
<li><p><strong>Compensación de iluminación desigual</strong>: muy útil en imágenes médicas, fotografía artística y escaneos de documentos.</p>
</li>
<li><p><strong>Preprocesamiento para segmentación</strong>: al normalizar variaciones de iluminación, se mejora la robustez de algoritmos posteriores.</p>
</li>
</ul>
<p>La clave del filtrado homomórfico está en elegir adecuadamente el filtro <em>H(kₓ, ky)</em>, que suele tener forma de filtro pasa-alto modulado:</p>
<p>$$H(k_x, k_y) = (\gamma_H - \gamma_L) \cdot \left[1 - e^{-\frac{(k_x^2 + k_y^2)}{2\sigma^2}}\right] + \gamma_L$$</p><p>donde:</p>
<p>$$\begin{align*} \gamma_L &amp;&lt; 1 \quad \text{: controla el nivel de atenuación de la iluminación}, \\ \gamma_H &amp;&gt; 1 \quad \text{: define el realce de detalles}, \\ \sigma &amp;\quad \text{: regula la transición entre bandas}. \end{align*}$$</p><pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">HomomorphicFilterLab</span>(<span class="hljs-params">bgr_img: np.ndarray, gammaL=<span class="hljs-number">0.5</span>, gammaH=<span class="hljs-number">1.5</span>, sigma=<span class="hljs-number">30</span></span>) -&gt; np.ndarray:</span>
    <span class="hljs-string">"""
    Applies homomorphic filtering to the L (lightness) channel of a BGR image using the CIELAB color space.

    Parameters:
        bgr_img : np.ndarray
            Input image in BGR format (as used by OpenCV), with dtype uint8 and shape (H, W, 3).
        gammaL : float
            Gain for low frequencies (&lt;1, suppresses illumination).
        gammaH : float
            Gain for high frequencies (&gt;1, enhances details).
        sigma : float
            Controls the transition between low and high frequencies.

    Returns:
        np.ndarray:
            BGR image after homomorphic filtering on the luminance channel (dtype uint8, same shape as input).
    """</span>

    <span class="hljs-comment"># Convert to LAB color space</span>
    lab = cv.cvtColor(bgr_img, cv.COLOR_BGR2LAB)
    l, a, b = cv.split(lab)

    <span class="hljs-comment"># Convert L to float32 and scale to [0, 255] if necessary (OpenCV stores L in [0, 255] already)</span>
    l_float = l.astype(np.float32)

    <span class="hljs-comment"># Step 1: Log-transform</span>
    log_l = np.log1p(l_float)

    <span class="hljs-comment"># Step 2: DFT (centered)</span>
    dft = np.fft.fft2(log_l)
    dft_shift = np.fft.fftshift(dft)

    <span class="hljs-comment"># Step 3: Homomorphic filter in frequency domain</span>
    rows, cols = l.shape
    u = np.arange(-cols//<span class="hljs-number">2</span>, cols//<span class="hljs-number">2</span>)
    v = np.arange(-rows//<span class="hljs-number">2</span>, rows//<span class="hljs-number">2</span>)
    U, V = np.meshgrid(u, v)
    D2 = U**<span class="hljs-number">2</span> + V**<span class="hljs-number">2</span>
    H = (gammaH - gammaL) * (<span class="hljs-number">1</span> - np.exp(-D2 / (<span class="hljs-number">2</span> * sigma**<span class="hljs-number">2</span>))) + gammaL

    <span class="hljs-comment"># Step 4: Apply filter</span>
    filtered_dft = dft_shift * H

    <span class="hljs-comment"># Step 5: Inverse DFT</span>
    inv_dft = np.fft.ifft2(np.fft.ifftshift(filtered_dft))
    inv_dft = np.real(inv_dft)

    <span class="hljs-comment"># Step 6: Inverse log</span>
    l_filtered = np.expm1(inv_dft)

    <span class="hljs-comment"># Normalize and clip to [0, 255]</span>
    l_filtered = np.clip(l_filtered, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>).astype(np.uint8)

    <span class="hljs-comment"># Merge back and convert to BGR</span>
    lab_filtered = cv.merge([l_filtered, a, b])
    bgr_result = cv.cvtColor(lab_filtered, cv.COLOR_LAB2BGR)

    <span class="hljs-keyword">return</span> bgr_result
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753563824737/7d18a886-4197-4124-bbe2-9b176aea622e.jpeg" alt /></p>
<h2 id="heading-3-limitaciones-y-alternativas">3. Limitaciones y Alternativas</h2>
<p>Aunque el filtrado en el dominio de la frecuencia ofrece herramientas poderosas para modificar, restaurar o mejorar imágenes, no está exento de limitaciones. En esta sección se abordan los principales desafíos asociados a esta técnica, así como algunas alternativas o extensiones que intentan superarlos.</p>
<h3 id="heading-31-limite-de-gabor-y-localizacion-tiempo-frecuencia">3.1. Límite de Gabor y Localización Tiempo-Frecuencia</h3>
<p>Un principio fundamental en el análisis de señales es que <strong>no se puede lograr simultáneamente una alta resolución en el dominio espacial y en el dominio frecuencial</strong>. Esta idea se expresa en el llamado <strong>límite de Gabor</strong>, una manifestación de la desigualdad de Heisenberg adaptada al procesamiento de señales:</p>
<p>$$\sigma_x \cdot \sigma_f \geq \frac{1}{4\pi}$$</p><p>donde:</p>
<p>$$\begin{align*} \sigma_x &amp;= \text{Dispersión (ancho) en el dominio espacial}, \\ \sigma_f &amp;= \text{Dispersión en el dominio de la frecuencia}. \end{align*}$$</p><p>Este límite implica que <strong>al utilizar la Transformada de Fourier clásica</strong>, se pierde completamente la información de localización espacial: sabemos qué frecuencias están presentes, pero no <strong>dónde</strong> ocurren. Esto es suficiente para imágenes globalmente estacionarias, pero no para patrones locales o texturas que cambian en distintas regiones.</p>
<h4 id="heading-ejemplo-ilustrativo">Ejemplo ilustrativo:</h4>
<p>En procesamiento de audio, una señal musical puede analizarse en frecuencia, pero con la Transformada de Fourier tradicional no se puede saber <strong>cuándo</strong> suenan ciertas notas, solo que están presentes. En imágenes, ocurre algo similar: podemos detectar ciertas frecuencias, pero no identificar en qué zonas específicas se encuentran.</p>
<h4 id="heading-alternativas">Alternativas:</h4>
<p>Para superar esta limitación, se han desarrollado métodos que permiten un compromiso mejor entre resolución espacial y frecuencial. Algunas de las alternativas más relevantes son:</p>
<ul>
<li><p><strong>Transformada de Fourier de ventana (STFT)</strong>: Aplica la transformada a segmentos locales de la imagen, lo que permite una cierta localización espacial. Sin embargo, la resolución está limitada por el tamaño fijo de la ventana.</p>
</li>
<li><p><strong>Transformadas multiescala (como wavelets)</strong>: Proveen una descomposición jerárquica con mejor adaptabilidad. Las <strong>wavelets</strong> permiten una buena localización espacial para frecuencias altas y una buena localización frecuencial para frecuencias bajas, lo cual es ideal para imágenes con estructuras a múltiples escalas.</p>
</li>
<li><p><strong>Filtros de Gabor</strong>: Son versiones localizadas en espacio y frecuencia de la transformada de Fourier. Ofrecen una excelente representación para texturas y patrones periódicos en distintas orientaciones y escalas, a costa de mayor complejidad computacional.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusión</h2>
<p>El filtrado en el dominio de la frecuencia ofrece una perspectiva poderosa y elegante para el procesamiento de imágenes. A diferencia de los métodos espaciales, permite analizar y modificar el contenido de una imagen según la escala y complejidad de sus estructuras internas, revelando patrones que no siempre son evidentes en el dominio de los píxeles.</p>
<p>A lo largo del artículo hemos visto cómo, gracias al teorema de la convolución circular, es posible transformar una operación costosa como la convolución espacial en una multiplicación eficiente en frecuencia. Esto abre la puerta a filtros intuitivos, como los pasa-bajos, pasa-altos, pasa-banda y homomórficos, cada uno con propiedades específicas y aplicaciones particulares.</p>
<p>También se destacó que, si bien el dominio frecuencial permite diseñar filtros globales con mayor control y eficiencia para kernels grandes, presenta limitaciones inherentes en la localización espacial. Por ello, en escenarios donde el contexto local es crítico —como en el análisis de texturas o en imágenes no estacionarias—, conviene considerar enfoques híbridos o alternativos como los filtros de Gabor o las transformadas wavelet.</p>
<p>El dominio de la frecuencia no solo amplía el repertorio de herramientas disponibles en el procesamiento de imágenes, sino que ofrece un marco teórico profundo para entender cómo fluye la información visual en distintos niveles de escala y complejidad. Conocerlo, y saber cuándo aplicarlo, es esencial para el diseño de sistemas robustos de análisis visual, compresión, restauración y mejora de calidad.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[Fundamentos de la Probabilidad: Espacio Muestral y Eventos]]></title><description><![CDATA[La probabilidad y estadística es una rama de las matemáticas que permite analizar fenómenos inciertos y cuantificar el grado de confianza que podemos tener en distintos resultados posibles. Va más allá del simple cálculo numérico: nos proporciona un ...]]></description><link>https://codigoenllamas.com/fundamentos-de-la-probabilidad</link><guid isPermaLink="true">https://codigoenllamas.com/fundamentos-de-la-probabilidad</guid><category><![CDATA[Data Science]]></category><category><![CDATA[probability]]></category><category><![CDATA[fundamentals]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Tue, 22 Jul 2025 05:58:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/yG9pCqSOrAg/upload/e838da61cf24dc3d6d77a5be32ef7a9f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La probabilidad y estadística es una rama de las matemáticas que permite analizar fenómenos inciertos y cuantificar el grado de confianza que podemos tener en distintos resultados posibles. Va más allá del simple cálculo numérico: nos proporciona un marco lógico para razonar en condiciones de incertidumbre. Aunque hoy en día es fundamental en campos como, la inteligencia artificial y la ciencia de datos, sus orígenes se remontan a problemas prácticos relacionados con los juegos de azar.</p>
<p>En el siglo XVI, <strong>Girolamo Cardano</strong> fue uno de los primeros en abordar estos problemas desde una perspectiva sistemática. Más adelante, en el siglo XVII, el matemático <strong>Christiaan Huygens</strong> escribió el primer tratado formal sobre el tema; Aquel impulso inicial —entender cómo apostar de forma más justa— dio origen a una teoría que ha crecido en profundidad y alcance. De los dados y las cartas, <strong>la probabilidad pasó a convertirse en un marco sólido para describir fenómenos aleatorios en física, ingeniería, biología y economía</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753163748252/6c61c01c-dd3b-4bfa-a32f-4f1012b80387.jpeg" alt class="image--center mx-auto" /></p>
<p>Sin embargo, los principios fundamentales siguen siendo los mismos**.** La intuición frente a lo aleatorio sigue poniéndose a prueba, incluso en situaciones simples. Imagina que entras a un casino y comienzas a apostar en un juego de dados. Tras veinte tiradas consecutivas, el resultado ha sido siempre el número 7. ¿Esa secuencia sería suficiente para que te retires, pensando que algo no anda bien? ¿O seguirías jugando, confiando en que, aunque improbable, es un evento posible?</p>
<p>Podrías basar tu decisión en una corazonada… o en un razonamiento más estructurado: <strong>¿qué tan probable es que algo así ocurra por puro azar?</strong></p>
<h2 id="heading-conceptos-clave">Conceptos clave.</h2>
<p>En el campo de la estadística, el punto de partida es el análisis de fenómenos aleatorios que se manifiestan durante experimentos planificados o investigaciones científicas. Estos fenómenos suelen registrarse como datos, ya sean numéricos —como el número de accidentes en una intersección— o categóricos —como la clasificación de productos defectuosos en una línea de producción <strong>. Lo esencial es que cada unidad de información recogida en el proceso se denomina <em>observación</em>.</strong></p>
<p><em>Una observación puede representar tanto una medición cuantitativa como una categoría cualitativa. Por ejemplo</em>, los números 2, 0, 1 y 2 pueden representar el número de accidentes registrados mes a mes en una misma localización, mientras que las letras D y N podrían indicar si ciertos productos inspeccionados resultaron “defectuosos” o “no defectuosos”. En ambos casos, se trata de observaciones que conforman el insumo primario del análisis estadístico.</p>
<p>Para describir el <strong>proceso mediante el cual se generan estas observaciones, se utiliza el término <em>experimento</em></strong>. En estadística, un experimento no se limita a entornos controlados como los de laboratorio. Puede tratarse de un evento tan simple como lanzar una moneda o tan complejo como medir la velocidad de un proyectil o recolectar opiniones sobre una política pública. Lo relevante es que <em>cada repetición del experimento produce un resultado que, aunque no puede conocerse con certeza de antemano, conocemos el conjunto completo de posibilidades.</em></p>
<p>Dado que los resultados de muchos experimentos dependen del azar, la repetición bajo condiciones similares no garantiza la obtención del mismo resultado. Esta variabilidad es precisamente lo que da sentido al uso de herramientas probabilísticas en estadística. Incluso procesos aparentemente simples, como el lanzamiento de una moneda, revelan una estructura subyacente al observarse repetidamente.</p>
<p>Es importante aclarar que <strong>el término <em>experimento</em> se aplica de manera general en estadística, incluso en contextos donde no hay manipulación activa de variables.</strong> Los estudios observacionales, en los que solo se registran datos sin intervenir en el sistema, o los estudios retrospectivos, que analizan registros históricos, también generan observaciones sujetas a incertidumbre. Por lo tanto, estos casos también se consideran, en esencia, experimentos desde el punto de vista estadístico.</p>
<h3 id="heading-espacio-muestral">Espacio muestral.</h3>
<p><strong>Definición:</strong> Al conjunto de todos los resultados posibles de un experimento estadístico se le denomina <strong>espacio muestral</strong>, y suele representarse con la letra <strong>S</strong>.</p>
<p>Cada resultado individual dentro de este conjunto se conoce como <strong>punto muestral</strong>, aunque también puede llamarse elemento o miembro del espacio muestral. Cuando el espacio muestral contiene un número finito de resultados, es posible enumerarlos explícitamente, separándolos por comas y encerrándolos entre llaves.</p>
<p>Por ejemplo, si el experimento consiste en lanzar una moneda al aire, el espacio muestral correspondiente puede expresarse como: <em>S={H, T}</em> Donde H representa “cara” y T, “cruz”.</p>
<p><strong>Ejemplo</strong>: Considere el experimento de lanzar un dado. Si nos interesa el número que aparece en la cara superior, el espacio muestral se define como:</p>
<p>$$S_1 = \{1, 2, 3, 4, 5, 6\}$$</p><p>Sin embargo, si solo nos interesa saber si el número obtenido es par o impar, podemos definir un espacio muestral alternativo:</p>
<p>$$S_2 = \{\text{par}, \text{impar}\}$$</p><p>Este ejemplo ilustra que un mismo experimento puede describirse mediante distintos espacios muestrales, dependiendo del nivel de detalle que se desea capturar. En este caso, S₁ proporciona más información que S₂, ya que conocer el resultado en S₁ permite deducir el correspondiente en S₂​, pero no ocurre lo mismo en sentido contrario. Por lo tanto, en general, es preferible elegir un espacio muestral que conserve la mayor cantidad de información relevante sobre los posibles resultados del experimento.</p>
<p><strong>Ejemplo:</strong> Imagina una bolsa que contiene tres bolas de colores diferentes: una roja, una azul y una verde. El experimento consiste en sacar <strong>las tres bolas una por una, sin devolverlas</strong>, y registrar la secuencia de colores en el orden en que salen.</p>
<p>El espacio muestral aquí está formado por todas las posibles secuencias ordenadas que se pueden obtener al extraer las bolas.</p>
<p>Por lo tanto, el espacio muestral S queda definido como el conjunto de todas las permutaciones de los colores:</p>
<p>$$S = \{ (\text{roja}, \text{azul}, \text{verde}),\ (\text{roja}, \text{verde}, \text{azul}),\ (\text{azul}, \text{roja}, \text{verde}),\ (\text{azul}, \text{verde}, \text{roja}),\ (\text{verde}, \text{roja}, \text{azul}),\ (\text{verde}, \text{azul}, \text{roja}) \}$$</p><p>Cada elemento de S representa una posible secuencia en la que pueden salir las bolas. Por ejemplo, la secuencia (azul,roja,verde) indica que primero salió la bola azul, luego la roja y finalmente la verde.</p>
<p>Cuando el espacio muestral contiene un gran número de elementos, o incluso es infinito, resulta inviable enumerar todos los posibles resultados. En estos casos, es más práctico describir el espacio muestral mediante una regla o enunciado que defina sus elementos.</p>
<p><strong>Ejemplo 1:</strong> Consideremos un experimento donde seleccionamos al azar un número entero entre 1 y 1,000,000. En lugar de listar cada número, el espacio muestral se describe como</p>
<p>$$S = \{ x \mid x \text{ es un número entero tal que } 1 \leq x \leq 1{,}000{,}000 \}$$</p><p>Lo que se lee: “S es el conjunto de todos los números enteros x tales que x está entre 1 y 1,000,000 inclusive”.</p>
<p><strong>Ejemplo 2:</strong> Ahora imaginemos un experimento en el que elegimos un punto aleatorio dentro de una línea de longitud 5 metros. El espacio muestral contiene todos los puntos sobre la línea, y se representa por</p>
<p>$$S = \{ x \mid 0 \leq x \leq 5 \}$$</p><p>Donde x indica la posición sobre la línea, medida en metros desde un extremo.</p>
<p>Estos ejemplos muestran cómo se pueden manejar espacios muestrales grandes o continuos usando una descripción matemática que delimita claramente los resultados posibles, sin necesidad de listarlos uno por uno.</p>
<h3 id="heading-eventos">Eventos</h3>
<p>En cualquier experimento, a menudo nos interesa la ocurrencia de ciertos <strong>eventos</strong> más que la ocurrencia de un resultado específico dentro del espacio muestral. Un evento puede entenderse como un conjunto de resultados o puntos muestrales que cumplen una condición particular.</p>
<p>Por ejemplo, consideremos el experimento de lanzar un dado, cuyo espacio muestral es</p>
<p>$$S_1 = \{1, 2, 3, 4, 5, 6\}$$</p><p>Si nos interesa el evento A “el resultado es divisible entre 3”, este evento corresponde al subconjunto</p>
<p>$$A = \{3, 6 \} \subseteq S_1$$</p><p>Es decir, A contiene todos los puntos muestrales para los cuales la condición es verdadera.</p>
<p>Otro ejemplo puede surgir en la inspección de productos. Supongamos que se revisan tres artículos y cada uno puede ser “defectuoso” (D) o “no defectuoso” (N). El espacio muestral estará formado por todas las secuencias posibles de resultados, por ejemplo:</p>
<p>$$S = \{DDD, DDN, DND, DNN, NDD, NDN, NND, NNN\}$$</p><p>Donde cada secuencia representa el estado de los tres artículos inspeccionados. Si nos interesa el evento B: “más de un artículo es defectuoso”, entonces</p>
<p>$$B = \{DDD, DDN, DND, NDD\} \subseteq S$$</p><p>Es el subconjunto de secuencias en las cuales al menos dos artículos son defectuosos. A cada evento corresponde un subconjunto del espacio muestral que agrupa todos los resultados que hacen cierto el evento.</p>
<p><strong>Definición:</strong> Un evento es un subconjunto del espacio muestral.</p>
<h3 id="heading-complemento-de-un-evento">Complemento de un Evento</h3>
<p>En muchos experimentos, es común analizar no solo la ocurrencia de un evento específico, sino también su <strong>complemento</strong>, es decir, todos los resultados que no pertenecen a ese evento.</p>
<p>Consideremos, por ejemplo, un estudio sobre los hábitos de tabaquismo entre los empleados de una empresa industrial. Un posible espacio muestral para clasificar a cada individuo podría ser:</p>
<p>$$S = \{\text{no fumador}, \text{fumador ocasional}, \text{fumador moderado}, \text{fumador empedernido}\}.$$</p><p>Si definimos el evento A como “ser fumador”, entonces</p>
<p>$$A = \{\text{fumador ocasional }, \text{fumador moderado}, \text{fumador empedernido}\} \subseteq S$$</p><p>El <strong>complemento</strong> de este evento, que denotaremos como Aᶜ, es el conjunto de todos los elementos del espacio muestral que <strong>no</strong> están en A. En este caso, corresponde al grupo de no fumadores:</p>
<p>$$A^c = \{\text{no fumador}\}.$$</p><p>Es decir, Aᶜ agrupa todos los resultados donde el evento A no ocurre.</p>
<p><strong>Definición:</strong> El complemento de un evento A respecto del espacio muestral S es el subconjunto de todos los elementos de S que no pertenecen a A. Se denota como Aᶜ.</p>
<h2 id="heading-operaciones-con-eventos">Operaciones con eventos</h2>
<p>Una vez definidos los eventos como subconjuntos del espacio muestral, es natural preguntarse qué sucede cuando combinamos eventos. Existen operaciones entre eventos que permiten formar nuevos eventos, también representados como subconjuntos del mismo espacio muestral.</p>
<h3 id="heading-interseccion-de-eventos">Intersección de eventos</h3>
<p>Supongamos ahora que dos eventos, A y B, están asociados a un mismo experimento, es decir, son subconjuntos del mismo espacio muestral. En el lanzamiento de un dado, por ejemplo, podemos definir:</p>
<p>$$S = \{1, 2, 3, 4, 5, 6\}$$</p><p>$$A = \{2, 4, 6\} \quad \text{(número par)}, \quad B = \{4, 5, 6\} \quad \text{(número mayor que 3)}.$$</p><p>Los resultados que hacen que <strong>ambos eventos ocurran simultáneamente</strong> corresponden a los elementos comunes entre A y B, es decir: A ∩ B = {4, 6}.</p>
<p><strong>Definición:</strong> La <strong>intersección</strong> de dos eventos A y B, denotada por A ∩ B, es el conjunto de todos los elementos que pertenecen <strong>simultáneamente</strong> a ambos eventos.</p>
<p><strong>Ejemplo:</strong> Sea E el evento “la persona seleccionada al azar en un salón es estudiante de ingeniería”, y F el evento “la persona es mujer”. Entonces, la intersección E ∩ F representa el evento “la persona es una estudiante mujer de ingeniería”, es decir, aquellas personas que cumplen ambas condiciones.</p>
<hr />
<h3 id="heading-eventos-mutuamente-excluyentes">Eventos mutuamente excluyentes</h3>
<p>En algunos casos, dos eventos no pueden ocurrir al mismo tiempo. Por ejemplo, si definimos:</p>
<p>$$V = \{\text{a}, \text{e}, \text{i}, \text{o}, \text{u}\} \quad \text{(vocales)}, \quad C = \{\text{l}, \text{r}, \text{s}, \text{t}\} \quad \text{(ciertas consonantes)}$$</p><p>entonces</p>
<p>$$V \cap C = \varnothing$$</p><p>lo cual indica que no hay ningún elemento en común entre los dos conjuntos. En este caso, decimos que los eventos V y C son mutuamente excluyentes.</p>
<p><strong>Definición:</strong> Dos eventos A y B son <strong>mutuamente excluyentes</strong> (o disjuntos) si:</p>
<p>$$A \cap B = \varnothing$$</p><p>Es decir, si no tienen ningún punto muestral en común y, por lo tanto, no pueden ocurrir al mismo tiempo.</p>
<p><strong>Ejemplo</strong>: Imaginemos una empresa de televisión por cable que ofrece programación en 8 canales. La distribución es la siguiente:</p>
<ul>
<li><p><strong>3 canales afiliados a ABC</strong></p>
</li>
<li><p><strong>2 canales afiliados a NBC</strong></p>
</li>
<li><p><strong>1 canal afiliado a CBS</strong></p>
</li>
<li><p><strong>1 canal educativo</strong></p>
</li>
<li><p><strong>1 canal deportivo (ESPN)</strong></p>
</li>
</ul>
<p>Supongamos que un espectador enciende el televisor sin seleccionar un canal específico, es decir, se elige uno al azar. Definimos los siguientes eventos:</p>
<ul>
<li><p>A: “el canal pertenece a la cadena <strong>NBC</strong>”</p>
</li>
<li><p>B: “el canal pertenece a la cadena <strong>CBS</strong>”</p>
</li>
</ul>
<p>En este caso:</p>
<p>$$A = \{\text{Canal 4, Canal 5}\}, \quad B = \{\text{Canal 6}\}$$</p><p>Como ningún canal puede pertenecer a más de una cadena, <strong>los eventos A y B son disjuntos</strong>. Es decir: A∩B=∅ Por tanto, <strong>no hay intersección posible</strong>: un canal no puede ser de NBC y de CBS al mismo tiempo. Esto los convierte en <strong>eventos mutuamente excluyentes</strong>.</p>
<hr />
<h3 id="heading-union-de-eventos">Unión de eventos</h3>
<p>Hasta ahora, hemos explorado el complemento e intersección de eventos. Sin embargo, en muchos contextos prácticos estamos interesados en determinar si <strong>ocurre al menos uno de dos eventos</strong>. Este concepto se representa mediante la <strong>unión</strong> de eventos.</p>
<p>Ahora consideremos un ejemplo numérico más tradicional. Supongamos que lanzamos un dado, con espacio muestral:</p>
<p>S = {1, 2, 3, 4, 5, 6}</p>
<p>Definimos dos eventos:</p>
<ul>
<li><p>A = {2, 4, 6}: el número es par</p>
</li>
<li><p>B = {4, 5, 6}: el número es mayor que 3</p>
</li>
</ul>
<p>Queremos ahora describir el evento “ocurre A o ocurre B” (o ambos). Esto nos lleva a definir la <strong>unión</strong>:</p>
<p>$$A \cup B = \{2, 4, 5, 6\}$$</p><p><strong>Definición:</strong> La <strong>unión</strong> de dos eventos A y B, denotada por A ∪ B, es el conjunto de todos los resultados que pertenecen a <strong>A, B</strong> o a <strong>ambos</strong>.</p>
<ul>
<li><p>A ∩ B: representa los resultados que satisfacen <strong>simultáneamente</strong> ambos eventos.</p>
</li>
<li><p>A ∪ B: representa los resultados que satisfacen <strong>al menos uno</strong> de los eventos.</p>
</li>
</ul>
<hr />
<p><strong>Ejemplo con código.</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> matplotlib <span class="hljs-keyword">import</span> pyplot <span class="hljs-keyword">as</span> plt
<span class="hljs-keyword">from</span> matplotlib_venn <span class="hljs-keyword">import</span> venn3

<span class="hljs-comment"># Definir tres conjuntos con más elementos</span>
A = {<span class="hljs-string">'libro'</span>, <span class="hljs-string">'computadora'</span>, <span class="hljs-string">'papel'</span>, <span class="hljs-string">'pluma'</span>, <span class="hljs-string">'lápiz'</span>, <span class="hljs-string">'borrador'</span>, <span class="hljs-string">'regla'</span>}
B = {<span class="hljs-string">'papel'</span>, <span class="hljs-string">'teléfono'</span>, <span class="hljs-string">'libro'</span>, <span class="hljs-string">'cuaderno'</span>, <span class="hljs-string">'marcador'</span>, <span class="hljs-string">'regla'</span>, <span class="hljs-string">'calculadora'</span>}
C = {<span class="hljs-string">'computadora'</span>, <span class="hljs-string">'cuaderno'</span>, <span class="hljs-string">'papel'</span>, <span class="hljs-string">'marcador'</span>, <span class="hljs-string">'tijeras'</span>, <span class="hljs-string">'lápiz'</span>}

<span class="hljs-comment"># Crear el conjunto universo (para ilustrar el complemento)</span>
U = A.union(B).union(C)

<span class="hljs-comment"># Crear el diagrama de Venn para tres conjuntos</span>
venn = venn3([A, B, C], set_labels=(<span class="hljs-string">'A'</span>, <span class="hljs-string">'B'</span>, <span class="hljs-string">'C'</span>))

<span class="hljs-comment"># Personalizar el diagrama de Venn</span>
venn.get_label_by_id(<span class="hljs-string">'100'</span>).set_text(<span class="hljs-string">'Solo en A'</span>)
venn.get_label_by_id(<span class="hljs-string">'010'</span>).set_text(<span class="hljs-string">'Solo en B'</span>)
venn.get_label_by_id(<span class="hljs-string">'001'</span>).set_text(<span class="hljs-string">'Solo en C'</span>)
venn.get_label_by_id(<span class="hljs-string">'110'</span>).set_text(<span class="hljs-string">'A ∩ B'</span>)
venn.get_label_by_id(<span class="hljs-string">'101'</span>).set_text(<span class="hljs-string">'A ∩ C'</span>)
venn.get_label_by_id(<span class="hljs-string">'011'</span>).set_text(<span class="hljs-string">'B ∩ C'</span>)
venn.get_label_by_id(<span class="hljs-string">'111'</span>).set_text(<span class="hljs-string">'A ∩ B ∩ C'</span>)

<span class="hljs-comment"># Mostrar el gráfico</span>
plt.title(<span class="hljs-string">"Operaciones entre conjuntos A, B y C"</span>)
plt.show()

<span class="hljs-comment"># Imprimir operaciones adicionales</span>
print(<span class="hljs-string">"A ∪ B ∪ C ="</span>, A.union(B).union(C))
print(<span class="hljs-string">"A ∩ B ∩ C ="</span>, A.intersection(B).intersection(C))
print(<span class="hljs-string">"A - (B ∪ C) ="</span>, A.difference(B.union(C)))
print(<span class="hljs-string">"B - A ="</span>, B.difference(A))
print(<span class="hljs-string">"Complemento de C respecto al universo:"</span>, U.difference(C))
print(<span class="hljs-string">"Elementos comunes entre A y B pero no en C:"</span>, (A &amp; B) - C)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753137598585/4c522f40-d003-4340-9274-276243a9083b.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash">
A ∪ B ∪ C = {<span class="hljs-string">'marcador'</span>, <span class="hljs-string">'calculadora'</span>, <span class="hljs-string">'tijeras'</span>, <span class="hljs-string">'regla'</span>, <span class="hljs-string">'computadora'</span>, <span class="hljs-string">'teléfono'</span>, <span class="hljs-string">'lápiz'</span>, <span class="hljs-string">'libro'</span>, <span class="hljs-string">'borrador'</span>, <span class="hljs-string">'papel'</span>, <span class="hljs-string">'cuaderno'</span>, <span class="hljs-string">'pluma'</span>}

A ∩ B ∩ C = {<span class="hljs-string">'papel'</span>}

A - (B ∪ C) = {<span class="hljs-string">'borrador'</span>, <span class="hljs-string">'pluma'</span>}

B - A = {<span class="hljs-string">'cuaderno'</span>, <span class="hljs-string">'marcador'</span>, <span class="hljs-string">'teléfono'</span>, <span class="hljs-string">'calculadora'</span>}

Complemento de C respecto al universo: {<span class="hljs-string">'calculadora'</span>, <span class="hljs-string">'regla'</span>, <span class="hljs-string">'teléfono'</span>, <span class="hljs-string">'libro'</span>, <span class="hljs-string">'borrador'</span>, <span class="hljs-string">'pluma'</span>}

Elementos comunes entre A y B pero no en C: {<span class="hljs-string">'libro'</span>, <span class="hljs-string">'regla'</span>}
</code></pre>
<p>La probabilidad, al final, <strong>es una herramienta para dar forma numérica a la incertidumbre</strong>. Nos permite medir cuán confiable es un evento, y con ello, tomar decisiones más informadas en contextos donde el azar también juega su parte. Con estos conceptos fundamentales claros, ya estamos en condiciones de avanzar hacia ideas más abstractas y útiles dentro del estudio de la probabilidad.</p>
]]></content:encoded></item><item><title><![CDATA[Detector de bordes de Canny: teoría e implementación]]></title><description><![CDATA[La detección de bordes es una técnica ampliamente usada en procesamiento de imágenes para identificar los contornos de objetos dentro de una escena visual. En imágenes digitales, los bordes de intensidad aparecen en aquellas regiones donde la función...]]></description><link>https://codigoenllamas.com/detector-de-bordes-de-canny-teoria-e-implementacion</link><guid isPermaLink="true">https://codigoenllamas.com/detector-de-bordes-de-canny-teoria-e-implementacion</guid><category><![CDATA[Computer Vision]]></category><category><![CDATA[Python]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[image processing]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Fri, 11 Jul 2025 18:25:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752214863902/aecd754c-c307-4bae-a442-42e670c540ff.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La detección de bordes es una técnica ampliamente usada en <strong>procesamiento de imágenes</strong> para identificar los contornos de objetos dentro de una escena visual. En imágenes digitales, los bordes de intensidad aparecen en aquellas regiones donde la función de niveles de gris cambia de forma abrupta. Estos puntos de variación repentina, conocidos también como <em>edgels</em> (elementos de borde), contienen una gran cantidad de información visual porque su valor no se puede estimar fácilmente a partir de los píxeles que los rodean.</p>
<blockquote>
<p><a target="_blank" href="https://medium.com/imagecraft/canny-edge-detector-theory-and-implementation-ffd31cd28e73"><strong>English version of this article. Click here</strong></a></p>
</blockquote>
<p>Esta dificultad para anticipar su intensidad los hace destacar frente al resto de la imagen y, por tanto, son cruciales para describir su contenido. De hecho, incluso representaciones reducidas en forma de simples dibujos de líneas permiten a los humanos reconocer objetos y escenarios con notable facilidad. Este fenómeno muestra que los bordes de intensidad no solo son importantes en la percepción visual humana, sino también en los sistemas de <strong>visión por computadora</strong>, donde capturar correctamente estas transiciones es esencial para interpretar lo que hay en una imagen.</p>
<p>Entre los distintos tipos de bordes de intensidad —como los bordes escalón (<em>step edges</em>), de línea, de techo o rampa— los bordes escalón son los más comunes y representativos. En una dimensión, este tipo de borde se manifiesta como un valor elevado en la primera derivada de la señal. En dos dimensiones, esta idea se generaliza al concepto de gradiente: una medida vectorial que indica tanto la dirección como la intensidad del cambio en la imagen.</p>
<p>Si bien existen múltiples formas de estimar el gradiente mediante filtros como Sobel, Prewitt, Scharr o derivados de Gauss, obtener bordes claros y bien definidos requiere más que simplemente calcular derivadas. El <strong>algoritmo de Canny</strong> aborda esta necesidad con un enfoque robusto y refinado, y se ha convertido en una de las técnicas más utilizadas para la detección de bordes. Diseñado con criterios precisos —buena detección, localización exacta y mínima respuesta múltiple—, sigue siendo una referencia clave en segmentación, reconocimiento de patrones y análisis de escenas.</p>
<hr />
<h2 id="heading-etapas-del-algoritmo-de-deteccion-de-bordes-tipo-canny"><strong>Etapas del algoritmo de detección de bordes tipo Canny</strong></h2>
<hr />
<ul>
<li><h3 id="heading-calculo-del-gradiente-de-intensidad"><strong>Cálculo del gradiente de intensidad</strong></h3>
<p>  La imagen se suaviza mediante un filtro Gaussiano para reducir el ruido. Luego se calculan las derivadas parciales en las direcciones horizontal y vertical Gₓ y Gy, a partir de las cuales se obtiene:</p>
<p>  <strong>Magnitud del gradiente</strong>:</p>
</li>
<li><p>$$G_{\text{mag}} = \sqrt{Gx^2 + Gy^2}$$</p></li>
<li><h3 id="heading-supresion-de-no-maximos-con-interpolacion-subpixel"><strong>Supresión de no-máximos con interpolación subpíxel</strong></h3>
<p>  Para conservar únicamente los bordes más significativos, se suprimen los valores que no son máximos locales en la dirección del gradiente. En lugar de discretizar esta dirección en pocos ángulos (como 0°, 45°, 90°, 135°), se realiza una interpolación lineal entre los píxeles vecinos a lo largo de la dirección exacta del gradiente. Esta aproximación subpíxel mejora la precisión de la detección, eliminando bordes falsos y afinando las líneas detectadas.</p>
</li>
<li><h3 id="heading-normalizacion-de-la-respuesta"><strong>Normalización de la respuesta</strong></h3>
<p>  Tras la supresión, la imagen se normaliza a una escala de 0 a 255. Esta operación facilita la aplicación de umbrales en la etapa siguiente, garantizando una separación clara entre bordes fuertes y débiles.</p>
</li>
<li><h3 id="heading-umbralizacion-por-histeresis"><strong>Umbralización por histéresis</strong></h3>
<p>  Se aplican dos umbrales:</p>
<p>  <strong>Umbral alto (</strong><code>T_high</code>): identifica los píxeles que forman parte de bordes fuertes.</p>
<p>  <strong>Umbral bajo (</strong><code>T_low</code>): marca como candidatos a borde aquellos píxeles con respuesta intermedia, que podrían formar parte de un borde si están conectados a píxeles fuertes.</p>
<p>  La conectividad se propaga mediante un recorrido tipo BFS (cola FIFO), agrupando píxeles débiles que estén directa o indirectamente conectados con los fuertes. Esto permite cerrar contornos y preservar estructuras continuas.</p>
</li>
<li><h3 id="heading-generacion-del-mapa-de-bordes"><strong>Generación del mapa de bordes</strong></h3>
<p>  El resultado es una imagen binaria en la que los bordes detectados se representan con intensidad máxima (255) y el fondo con 0. Solo sobreviven los píxeles que cumplieron con todos los criterios: magnitud alta, máximo local y conexión estructural.</p>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">CannyLikeDetector</span>(<span class="hljs-params">image: np.ndarray, sigma=<span class="hljs-number">1.0</span>, tlow=<span class="hljs-number">0.1</span>, thigh=<span class="hljs-number">0.3</span></span>) -&gt; np.ndarray:</span>

    Gx, Gy, Gmag, Gphase = ComputeImageGradient(image, sigma_s=sigma, sigma_d=sigma)

    suppressed = NonMaximumSuppressionSubpixel(Gx, Gy, Gmag)

    <span class="hljs-comment"># Normalización post-supresión</span>
    norm_suppressed = np.clip((suppressed / suppressed.max()) * <span class="hljs-number">255.0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>).astype(np.float32)

    <span class="hljs-comment"># Umbrales escalados</span>
    T_high = thigh * <span class="hljs-number">255</span>
    T_low = tlow * <span class="hljs-number">255</span>

    edges = HysteresisThresholdFIFO(norm_suppressed, T_high, T_low)
    <span class="hljs-keyword">return</span> edges
</code></pre>
<hr />
<h2 id="heading-supresion-de-no-maximos-con-interpolacion-subpixel-1"><strong>Supresión de no-máximos con interpolación subpíxel</strong></h2>
<p>Esta técnica busca conservar únicamente aquellos puntos cuya magnitud del gradiente es un verdadero máximo local en la dirección de mayor cambio de intensidad. A diferencia de métodos clásicos que discretizan la orientación en pocos ángulos, esta variante mejora la precisión al trabajar directamente con la dirección continua del gradiente, empleando interpolación bilineal.</p>
<p>Para cada píxel, se calcula el vector gradiente y se normaliza para obtener la dirección <strong><em>dx, dy</em></strong>. Luego, se estima la magnitud del gradiente en posiciones desplazadas hacia adelante y hacia atrás siguiendo esa dirección, utilizando interpolación bilineal sobre la imagen de magnitudes. A continuación, se comparan tres valores: el del píxel actual y los dos interpolados. Si el valor central es mayor o igual que ambos vecinos, se conserva; en caso contrario, se suprime.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">NonMaximumSuppressionSubpixel</span>(<span class="hljs-params">Gx: np.ndarray, Gy: np.ndarray, Gmag: np.ndarray</span>) -&gt; np.ndarray:</span>
    rows, cols = Gmag.shape
    output = np.zeros_like(Gmag, dtype=np.float32)

    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, rows - <span class="hljs-number">1</span>):
        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, cols - <span class="hljs-number">1</span>):
            gx, gy = Gx[i, j], Gy[i, j]
            mag = Gmag[i, j]
            <span class="hljs-keyword">if</span> gx == <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> gy == <span class="hljs-number">0</span>:
                <span class="hljs-keyword">continue</span>
            norm = np.hypot(gx, gy)
            dx, dy = gx / norm, gy / norm

            <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">interp</span>(<span class="hljs-params">y, x</span>):</span>
                x0, y0 = int(x), int(y)
                x1 = min(x0 + <span class="hljs-number">1</span>, cols - <span class="hljs-number">1</span>)
                y1 = min(y0 + <span class="hljs-number">1</span>, rows - <span class="hljs-number">1</span>)
                a, b = x - x0, y - y0
                <span class="hljs-keyword">return</span> (
                    Gmag[y0, x0] * (<span class="hljs-number">1</span> - a) * (<span class="hljs-number">1</span> - b) +
                    Gmag[y0, x1] * a * (<span class="hljs-number">1</span> - b) +
                    Gmag[y1, x0] * (<span class="hljs-number">1</span> - a) * b +
                    Gmag[y1, x1] * a * b
                )

            mag1 = interp(i + dy, j + dx)
            mag2 = interp(i - dy, j - dx)

            <span class="hljs-keyword">if</span> mag &gt;= mag1 <span class="hljs-keyword">and</span> mag &gt;= mag2:
                output[i, j] = mag
    <span class="hljs-keyword">return</span> output
</code></pre>
<p>La imagen resultante contiene únicamente los puntos que se destacan como máximos locales en la dirección del gradiente, representando con mayor fidelidad los bordes reales en la escena y favoreciendo una delineación más precisa y continua de los contornos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752257924366/c80c0bcd-e653-4d30-b723-aeccd3c879b5.png" alt="gradiente de la imagen e imagen suprimida " class="image--center mx-auto" /></p>
<p><strong><em>Derecha: gradiente de la imagen, izquierda: imagen resultante de la supresión.</em></strong></p>
<hr />
<h2 id="heading-umbralizado-por-histeresis">Umbralizado por histéresis</h2>
<p>Después de la supresión de no-máximos, nos queda una imagen donde los bordes están bien localizados, pero aún pueden incluir ruido o detalles poco relevantes. Para decidir <strong>cuáles bordes conservar</strong>, Canny propuso usar <strong>dos umbrales</strong>:</p>
<ul>
<li><p><strong>Tₕ (umbral alto)</strong>: cualquier píxel con una magnitud mayor o igual a este se considera un <em>borde fuerte</em> (válido).</p>
</li>
<li><p><strong>Tₗ (umbral bajo)</strong>: cualquier píxel con una magnitud entre Tₗ y Tₕ es un <em>borde débil</em>, que <strong>solo será aceptado si está conectado a un borde fuerte</strong>.</p>
</li>
<li><p>Todo lo que está por debajo de Tₗ se descarta por completo.</p>
</li>
</ul>
<p>Este proceso evita tanto la pérdida de bordes importantes (si se usara un único umbral alto), como la aceptación de ruido (si se usara un único umbral bajo).</p>
<hr />
<h2 id="heading-umbralizado-por-histeresis-con-propagacion-bfs"><strong>Umbralizado por histéresis con propagación BFS</strong></h2>
<p>El umbralizado por histéresis se encarga de refinar los bordes detectados eliminando aquellos que no estén suficientemente respaldados por la estructura global de la imagen. Para ello, clasifica los píxeles según dos umbrales: alto y bajo. Aquellos cuya magnitud supera el umbral alto se consideran <strong>bordes fuertes</strong>; los que están entre ambos umbrales, <strong>bordes débiles</strong>; y el resto se descarta.</p>
<p>La técnica aplica un proceso de propagación que preserva únicamente los bordes débiles <strong>conectados a bordes fuertes</strong>, lo que garantiza una mayor continuidad y reduce los falsos positivos. Esta propagación se realiza mediante un recorrido tipo <strong>búsqueda en anchura (BFS)</strong> utilizando una cola FIFO.</p>
<p>Primero, se identifican y almacenan todos los píxeles fuertes. Luego, para cada uno de ellos, se exploran sus vecinos en una conectividad de 8. Si alguno de estos vecinos corresponde a un píxel débil aún no marcado, se lo promueve a borde definitivo y se agrega a la cola para seguir expandiendo la conexión.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">HysteresisThresholdFIFO</span>(<span class="hljs-params">image, T_high, T_low</span>):</span>
    h, w = image.shape
    strong = (image &gt;= T_high)
    weak = (image &gt;= T_low) &amp; ~strong
    result = np.zeros_like(image, dtype=np.uint8)
    queue = deque()

    <span class="hljs-keyword">for</span> y <span class="hljs-keyword">in</span> range(h):
        <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> range(w):
            <span class="hljs-keyword">if</span> strong[y, x]:
                result[y, x] = <span class="hljs-number">255</span>
                queue.append((x, y))

    directions = [(<span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">1</span>),
                  (<span class="hljs-number">0</span>, <span class="hljs-number">-1</span>),         (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>),
                  (<span class="hljs-number">1</span>, <span class="hljs-number">-1</span>), (<span class="hljs-number">1</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">1</span>, <span class="hljs-number">1</span>)]

    <span class="hljs-keyword">while</span> queue:
        x, y = queue.popleft()
        <span class="hljs-keyword">for</span> dx, dy <span class="hljs-keyword">in</span> directions:
            nx, ny = x + dx, y + dy
            <span class="hljs-keyword">if</span> <span class="hljs-number">0</span> &lt;= nx &lt; w <span class="hljs-keyword">and</span> <span class="hljs-number">0</span> &lt;= ny &lt; h:
                <span class="hljs-keyword">if</span> weak[ny, nx] <span class="hljs-keyword">and</span> result[ny, nx] == <span class="hljs-number">0</span>:
                    result[ny, nx] = <span class="hljs-number">255</span>
                    queue.append((nx, ny))
    <span class="hljs-keyword">return</span> result
</code></pre>
<p>Este mecanismo asegura que los contornos detectados sean coherentes y estén respaldados por una estructura significativa, permitiendo preservar bordes reales mientras se descartan aquellos aislados o espurios.</p>
<hr />
<h2 id="heading-resultados">Resultados.</h2>
<p>Antes de aplicar el detector de bordes de Canny, es recomendable realizar un preprocesamiento mediante técnicas de filtrado que suavicen la imagen y reduzcan el ruido. Esta etapa no solo evita la detección de bordes espurios causados por pequeñas variaciones locales, sino que también ayuda a que los contornos verdaderamente relevantes se presenten de forma más continua y estable. En particular, filtros como el gaussiano o variantes más avanzadas como el bilateral pueden mejorar significativamente la coherencia espacial de los bordes detectados en la etapa posterior.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752255668634/9fb5c8eb-ef58-43ea-9c5e-cea9a7e6f079.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752256117533/e736de08-2a7e-488b-932e-d33f1f623ea4.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752256143721/c1154774-28bb-418f-a79c-6ca12433c49f.png" alt class="image--center mx-auto" /></p>
<p>El algoritmo de Canny ha perdurado como uno de los métodos más eficaces y usados en el campo del procesamiento de imágenes debido a su formulación precisa y sus decisiones fundamentadas en principios matemáticos y perceptuales. A través de un enfoque estructurado que incluye suavizado gaussiano, cálculo de gradientes, supresión de no-máximos e histéresis con umbrales duales, logra detectar bordes bien localizados, continuos y con bajo nivel de falsos positivos.</p>
<p>La versión moderna del detector de Canny emplea técnicas avanzadas como la supresión de no-máximos con interpolación subpíxel y la propagación por histéresis basada en una cola FIFO. Estas estrategias refinan la localización de los bordes y mejoran su continuidad, incluso en escenas con transiciones suaves o estructuras poco definidas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752256184179/3c0fae6f-0ccd-4d22-a6d3-5366f4479926.png" alt class="image--center mx-auto" /></p>
<p><strong><em>En la imagen de la derecha se aplico el algoritmo original, en la imagen de la izquierda se aplico el algoritmo optimizado.</em></strong></p>
<p>Más allá de su efectividad práctica, el estudio del algoritmo de Canny revela cómo decisiones algorítmicas cuidadosamente diseñadas —como el uso conjunto de magnitud, orientación y conectividad— pueden reproducir con notable precisión la percepción humana de los contornos. Por ello, Canny no solo representa un hito histórico en visión por computadora, sino que sigue siendo una herramienta vigente y valiosa en aplicaciones modernas como análisis de imágenes, visión artificial y preprocesamiento para modelos de aprendizaje automático.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[Fusión de Imágenes Flash y No-Flash para Restauración Fotográfica]]></title><description><![CDATA[La correcta iluminación de una escena juega un papel importante en la obtención de una buena fotografía para transmitir sensaciones visuales a través de la atmósfera que crea: los matices de una vela pueden sugerir calidad, mientras que las paletas a...]]></description><link>https://codigoenllamas.com/flash-y-no-flash-restauracion-fotografica</link><guid isPermaLink="true">https://codigoenllamas.com/flash-y-no-flash-restauracion-fotografica</guid><category><![CDATA[opencv]]></category><category><![CDATA[image processing]]></category><category><![CDATA[Computer Vision]]></category><category><![CDATA[Python]]></category><category><![CDATA[Paper Review]]></category><category><![CDATA[Python 3]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sat, 05 Jul 2025 17:29:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/7Y0NshQLohk/upload/7837da489d6bd846e60cd306d5da8ae7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La correcta iluminación de una escena juega un papel importante en la obtención de una buena fotografía para transmitir sensaciones visuales a través de la atmósfera que crea: los matices de una vela pueden sugerir calidad, mientras que las paletas azuladas en penumbra evocan frío y misterio.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733239756/1a700a2a-3b63-4ddd-bd9d-7a7ad732bd27.png" alt /></p>
<p>En contextos de baja iluminación, capturar esa atmósfera sin sacrificar calidad técnica representa un desafío importante. Para lograr una imagen adecuada en entornos con poca luz, el fotógrafo debe encontrar un equilibrio delicado entre la apertura del diafragma, el tiempo del obturador y la sensibilidad ISO. Aumentar el tiempo de exposición permite capturar más luz, pero puede producir desenfoques por movimiento (motion blur). Abrir más el diafragma reduce la necesidad de tiempos largos, pero disminuye la profundidad de campo. Elevar el ISO aumenta la sensibilidad del sensor, aunque también incrementa la presencia de ruido, especialmente en exposiciones cortas.</p>
<blockquote>
<p>English version of this article. <a target="_blank" href="https://medium.com/imagecraft/enhancing-low-light-photography-fusion-of-flash-and-no-flash-images-234128c6bc86">Click here</a></p>
</blockquote>
<p>Una solución común es el uso del flash, que permite obtener imágenes nítidas y bien expuestas. Sin embargo, esta técnica introduce varios problemas: los objetos cercanos tienden a sobreexponerse, se pierden los matices de la luz ambiental, y aparecen artefactos como ojos rojos, sombras duras o brillos especulares indeseados.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733280727/214eeb1d-a782-4923-8f3a-7426dc1e394b.jpeg" alt /></p>
<p>En el trabajo <em>"Digital Photography with Flash and No-Flash Image Pairs"</em>. propusieron una técnica , para combinar una imagen con flash y otra sin el, para crear una nueva que conserva la iluminación natural de la escena mientras incorpora el nivel de detalle de la imagen con flash</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733305084/847db723-0aa0-4871-82b3-048c32ea5e4f.jpeg" alt /></p>
<h2 id="heading-explicacion-del-algoritmo">Explicación del algoritmo</h2>
<p>El algoritmo propuesto por Eisemann y Durand se compone de los siguientes pasos principales:</p>
<ol>
<li><p><strong>Reducción de ruido en la imagen sin flash</strong><br /> Se utiliza la imagen con flash, que posee menor ruido, como referencia para eliminar el ruido de la imagen tomada con luz ambiente, preservando su iluminación original.</p>
</li>
<li><p><strong>Transferencia de detalle de alta frecuencia</strong><br /> Se extraen texturas finas y bordes nítidos de la imagen con flash y se incorporan en la imagen sin flash ya filtrada, mejorando su nivel de detalle sin alterar su tonalidad global.</p>
</li>
<li><p><strong>Corrección de balance de blancos</strong><br /> (opcional) A partir del color conocido del flash, se ajusta la temperatura de color de la imagen ambiental.</p>
</li>
<li><p><strong>Interpolación continua entre ambas imágenes</strong><br /> Se ajusta la intensidad del efecto del flash, interpolando o incluso extrapolando entre las dos imágenes originales para obtener un resultado personalizado.</p>
</li>
<li><p><strong>Corrección de ojos rojos</strong><br /> (opcional) Se detecta este artefacto comparando los colores de la pupila en ambas imágenes, aplicando una corrección precisa basada en el cambio producido por el flash.</p>
</li>
</ol>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">enhance_ambient_with_flash</span>(<span class="hljs-params">ambient, flash</span>):</span>
    <span class="hljs-string">"""
    Enhances the ambient image using the flash image.

    Parameters:
        ambient (numpy array): Ambient image.
        flash (numpy array): Flash image.

    Returns:
        numpy array: Enhanced ambient image.
    """</span>
    ambient_lin = ambient.astype(np.float32) / <span class="hljs-number">255</span>
    flash_lin = flash.astype(np.float32) / <span class="hljs-number">255</span>

    <span class="hljs-comment"># Compute ambient color and denoise</span>
    denoised_ambient = joint_bilateral_filter(ambient, flash, sigma_d=<span class="hljs-number">10</span>, sigma_r=<span class="hljs-number">0.2</span>)

    <span class="hljs-comment"># Compute detail layer</span>
    detail_layer = compute_detail_layer(flash, sigma_d=<span class="hljs-number">30</span>, sigma_r=<span class="hljs-number">0.9</span>, epsilon=<span class="hljs-number">0.01</span>)

    <span class="hljs-comment"># Detect shadows and specular highlights</span>
    specular_mask = detect_flash_specularities(flash_lin, threshold=<span class="hljs-number">0.95</span>)
    mask = detect_flash_shadows(flash_lin, ambient_lin, tau=<span class="hljs-number">0.01</span>)

    <span class="hljs-comment"># Combine shadow and specular masks</span>
    full_mask = np.clip(mask + specular_mask, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>)
    full_mask = cv2.GaussianBlur(full_mask, (<span class="hljs-number">5</span>, <span class="hljs-number">5</span>), <span class="hljs-number">5</span>)
    full_mask = np.repeat(full_mask[..., np.newaxis], <span class="hljs-number">3</span>, axis=<span class="hljs-number">2</span>)

    <span class="hljs-comment"># Final merge</span>
    transferred = denoised_ambient * detail_layer
    final_image = apply_masked_merge(transferred, denoised_ambient, full_mask)

    <span class="hljs-keyword">return</span> np.clip(final_image, <span class="hljs-number">0</span>, <span class="hljs-number">255</span>).astype(np.uint8)
</code></pre>
<h2 id="heading-filtrado-de-ruido">Filtrado de ruido</h2>
<p>La imagen sin flash suele presentar un nivel elevado de ruido, especialmente en condiciones de baja iluminación. Por esta razón, uno de los primeros pasos del algoritmo consiste en aplicar un proceso de reducción de ruido.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733558287/7d5a4c27-97a6-488f-a8a4-ff8c3f0498b3.jpeg" alt /></p>
<p>El filtrado es un área ampliamente estudiada en el procesamiento de imágenes, y existen numerosos filtros diseñados con este propósito. En esta técnica se emplea el <strong>filtro bilateral</strong>, debido a que ofrece una ventaja clave: <strong>suaviza la imagen sin destruir los bordes</strong>, preservando tanto la estructura como la información de iluminación.</p>
<p>El <strong>filtro bilateral</strong> funciona como un promedio ponderado de los píxeles vecinos, pero, a diferencia del promedio tradicional, asigna pesos no solo según la cercanía espacial, sino también según la similitud de intensidad. La fórmula general es:</p>
<p>$$h(x) = \sum_{i \in \Omega(x)} g_s(x - i) \cdot g_r(I(x) - I(i))$$</p><p>Sin embargo, en condiciones extremas —cuando la imagen sin flash tiene un nivel de ruido tan alto que incluso los bordes se vuelven poco distinguibles— es mejor utilizar una variante llamada <strong>Joint Bilateral Filter</strong>. Esta versión guía el filtrado de la imagen ruidosa utilizando <strong>otra imagen más confiable</strong> (en este caso, la imagen con flash G), lo cual permite preservar mejor los bordes reales.</p>
<p>$$h(x) = \sum_{i \in \Omega(x)} g_s(x - i) \cdot g_r(G(x) - G(i))$$</p><pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">joint_bilateral_filter</span>(<span class="hljs-params">ambient, flash, sigma_d=<span class="hljs-number">15</span>, sigma_r=<span class="hljs-number">0.1</span></span>):</span>
    <span class="hljs-string">"""
    Applies a joint bilateral filter using the flash image as a guide.

    Parameters:
        ambient (numpy array): Ambient image (to be filtered).
        flash (numpy array): Flash image (used as a guide).
        sigma_d (float): Spatial sigma (controls the range of spatial smoothing).
        sigma_r (float): Range sigma (controls the range of intensity smoothing).

    Returns:
        numpy array: Filtered ambient image.
    """</span>
    <span class="hljs-keyword">import</span> cv2.ximgproc  <span class="hljs-comment"># Ensure opencv-contrib-python is installed</span>

    ambient_uint8 = ambient.astype(np.uint8)
    flash_uint8 = flash.astype(np.uint8)

    filtered = np.zeros_like(ambient_uint8)

    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">3</span>):  <span class="hljs-comment"># Process each channel (BGR)</span>
        filtered[..., i] = cv2.ximgproc.jointBilateralFilter(
            joint=flash_uint8[..., i],    <span class="hljs-comment"># Guide image (flash)</span>
            src=ambient_uint8[..., i],    <span class="hljs-comment"># Image to be filtered (ambient)</span>
            d=<span class="hljs-number">-1</span>,
            sigmaColor=sigma_r * <span class="hljs-number">255</span>,
            sigmaSpace=sigma_d
        )

    <span class="hljs-keyword">return</span> filtered.astype(np.float32)
</code></pre>
<p>Este enfoque permite reducir el ruido de manera más robusta, incluso cuando la imagen original está severamente degradada, ya que la información estructural proviene de una fuente externa más confiable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733726512/2c7bcaca-3b2f-427e-9e13-b4417e4dd990.jpeg" alt class="image--center mx-auto" /></p>
<p>Una vez que la imagen sin flash ha sido suavizada para aislar su iluminación global, el siguiente paso del algoritmo consiste en transferir los detalles de alta frecuencia —como bordes nítidos, contornos definidos y texturas finas— desde la imagen capturada con flash. Para lograr esto, se aplica nuevamente un filtro bilateral a la imagen con flash, obteniendo una versión suavizada que conserva la estructura general pero elimina las variaciones locales rápidas.</p>
<h2 id="heading-transferencia-de-detalle">Transferencia de detalle.</h2>
<p>La <strong>capa de detalle</strong> se calcula entonces como la relación entre la imagen original con flash y su versión filtrada, una operación que resalta los cambios relativos en intensidad y permite aislar las texturas de forma multiplicativa. Esta estrategia es especialmente efectiva porque es invariante a escalas de iluminación, lo que evita distorsiones cromáticas o de brillo al fusionar las imágenes.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">compute_detail_layer</span>(<span class="hljs-params">flash, sigma_d=<span class="hljs-number">10</span>, sigma_r=<span class="hljs-number">0.7</span>, epsilon=<span class="hljs-number">0.5</span></span>):</span>
    <span class="hljs-string">"""
    Computes the detail layer of the flash image.

    Parameters:
        flash (numpy array): Flash image.
        sigma_d (float): Spatial sigma for the bilateral filter.
        sigma_r (float): Range sigma for the bilateral filter.
        epsilon (float): Small constant to avoid division by zero.

    Returns:
        numpy array: Detail layer.
    """</span>
    base = bilateral_filter(flash, sigma_d, sigma_r)
    detail = (flash + epsilon) / (base + epsilon)
    <span class="hljs-keyword">return</span> detail
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733848012/b53301b6-9d35-4332-ac96-e33c3b4b3487.png" alt="capa de detalle extraída de la imagen con flash." /></p>
<p>Finalmente, esta capa de detalle se incorpora sobre la imagen sin flash suavizada, conservando su atmósfera natural pero enriquecida visualmente con la nitidez aportada por el flash.</p>
<h2 id="heading-deteccion-de-sombras-y-especularidades">Detección de sombras y especularidades</h2>
<p>El siguiente paso es <strong>identificar y excluir regiones afectadas por artefactos del flash</strong>, como las sombras proyectadas y los reflejos especulares. En el algoritmo propuesto, la detección de <strong>sombras</strong> se basa en comparar las versiones <strong>linealizadas</strong> (sin corrección gamma) de ambas imágenes, lo que garantiza que las diferencias de luminancia reflejen fielmente las variaciones físicas de iluminación.</p>
<p>Dado que los píxeles en sombra no reciben luz directa del flash, su luminancia en ambas imágenes debería ser muy similar o apenas superior, por lo que se construye una <strong>máscara de sombras</strong> mediante un umbral aplicado a la diferencia por canal en el espacio RGB lineal. Este criterio requiere que todos los canales estén por debajo de un umbral, asegurando así una detección robusta frente a variaciones cromáticas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733929672/bead3294-acb9-435e-a4cd-ace279565804.png" alt /></p>
<p>Aunque esta detección es efectiva, puede verse afectada por factores como el ruido de la imagen, las interreflexiones entre superficies, objetos de albedo muy bajo (negros absolutos) y regiones alejadas que no reciben iluminación del flash. Sin embargo, las dos últimas no comprometen el resultado final, pues ambas imágenes contienen información similar en esas zonas, evitando falsas detecciones.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">detect_flash_shadows</span>(<span class="hljs-params">flash_lin, ambient_lin, tau=<span class="hljs-number">0.09</span></span>):</span>
    <span class="hljs-string">"""
    Detects shadows caused by the flash.

    Parameters:
        flash_lin (numpy array): Flash image (linearized).
        ambient_lin (numpy array): Ambient image (linearized).
        tau (float): Threshold for shadow detection.

    Returns:
        numpy array: Shadow mask.
    """</span>
    diff = flash_lin - ambient_lin
    shadow_mask = np.all(diff &lt; tau, axis=<span class="hljs-number">2</span>).astype(np.float32)
    <span class="hljs-keyword">return</span> cv2.dilate(shadow_mask, <span class="hljs-literal">None</span>)
</code></pre>
<p>Para mejorar la máscara y evitar bordes fragmentados o ruidosos, se aplican <strong>operaciones morfológicas</strong> como la dilatación, que expanden y suavizan la cobertura de las sombras, garantizando una segmentación conservadora y continua de las áreas sombreadas. Esto resulta crucial para evitar que las sombras generen artefactos durante la fusión de las imágenes.</p>
<p>Por otro lado, las <strong>especularidades</strong> —reflejos intensos que saturan el sensor— también deben identificarse, ya que en esas zonas la imagen con flash pierde completamente el detalle. Para detectarlas, el algoritmo analiza la luminancia de la imagen con flash Fₗᵢₙ​ y marca como especular cualquier píxel cuya intensidad supere el <strong>95% del rango del sensor</strong>.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">detect_flash_specularities</span>(<span class="hljs-params">flash_lin, threshold=<span class="hljs-number">0.95</span></span>):</span>
    <span class="hljs-string">"""
    Detects specular highlights caused by the flash.

    Parameters:
        flash_lin (numpy array): Flash image (linearized).
        threshold (float): Threshold for specular highlight detection.

    Returns:
        numpy array: Specular highlight mask.
    """</span>
    luminance = <span class="hljs-number">0.2126</span> * flash_lin[..., <span class="hljs-number">2</span>] + <span class="hljs-number">0.7152</span> * flash_lin[..., <span class="hljs-number">1</span>] + <span class="hljs-number">0.0722</span> * flash_lin[..., <span class="hljs-number">0</span>]
    specular_mask = (luminance &gt;= threshold).astype(np.float32)
    <span class="hljs-keyword">return</span> cv2.dilate(specular_mask, <span class="hljs-literal">None</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751733986794/c635e107-62e2-4f4d-b69a-934db147783e.png" alt /></p>
<p>Esta máscara también se refina con operaciones morfológicas, asegurando que se cubran adecuadamente las zonas saturadas. Al evitar el uso de datos provenientes de regiones con sombras duras o saturación especular, el algoritmo preserva tanto la estética natural de la iluminación ambiental como la calidad estructural de la imagen final.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751734020700/9c5a1d6a-0512-48a8-9bd1-81dae488c278.png" alt /></p>
<h2 id="heading-etapa-final">Etapa final.</h2>
<p>La etapa final del proceso consiste en <strong>combinar la imagen enriquecida con detalles</strong> y la imagen base suavizada, de forma que se aprovechen las fortalezas de ambas. Esta combinación se realiza mediante una <strong>mezcla ponderada controlada por una máscara</strong>, la cual indica en cada píxel qué proporción de cada imagen debe utilizarse.</p>
<p>En las zonas donde no hay artefactos —como sombras duras o reflejos especulares—, la máscara tiene valores cercanos a cero, permitiendo que predomine la imagen con detalles. Por el contrario, en regiones problemáticas la máscara toma valores cercanos a uno, priorizando la imagen base para evitar introducir errores visuales.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">apply_masked_merge</span>(<span class="hljs-params">a, b, mask</span>):</span>
    <span class="hljs-string">"""
    Merges two images using a mask.

    Parameters:
        a (numpy array): First image.
        b (numpy array): Second image.
        mask (numpy array): Mask to control blending.

    Returns:
        numpy array: Merged image.
    """</span>
    <span class="hljs-keyword">return</span> (<span class="hljs-number">1</span> - mask) * a + mask * b
</code></pre>
<p>Los valores intermedios permiten transiciones suaves entre ambas imágenes, lo que ayuda a mantener una apariencia continua y libre de bordes notorios. Gracias a esta interpolación controlada, se logra una imagen final que conserva la iluminación natural y la atmósfera ambiental, pero con una mejora perceptible en textura, nitidez y contraste.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751734091519/bf3c428f-ddb3-4eae-9400-b116607db59a.png" alt /></p>
<p>La combinación de imágenes con y sin flash representa una solución ingeniosa y eficaz para superar las limitaciones de la captura de fotografias en ambientes de baja iluminacion. A través de un pipeline cuidadosamente diseñado, se logra un equilibrio entre la fidelidad atmosférica de la luz ambiental y la riqueza estructural que aporta el flash.</p>
<p>Este enfoque no solo mitiga defectos típicos como ruido, sombras duras o saturación especular, sino que también permite preservar la sensación original de la escena, respetando la intención artística del fotógrafo. Gracias al uso de herramientas como el filtrado bilateral guiado, la separación multiplicativa de detalles y de máscaras, el método ofrece una alternativa robusta para producir imágenes técnicamente sólidas y visualmente agradables, incluso en condiciones de iluminación desafiantes. La técnica propuesta en <em>Digital Photography with Flash and No-Flash Image Pairs</em> demuestra así que la fotografía computacional no solo puede corregir deficiencias, sino también ampliar las capacidades expresivas del medio fotográfico.</p>
<h2 id="heading-resultados">Resultados</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751734198290/baa46b63-f7d9-4e66-ae79-635e88231a99.jpeg" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751734216681/0181de08-e8cd-406f-90bb-96a5175751f2.jpeg" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751734220357/249503ae-908c-4912-ac82-dd22fbab3928.jpeg" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751734205719/d6217d8f-6796-43a4-97d7-76650050cac7.jpeg" alt /></p>
<p>Bibliografia.</p>
<blockquote>
<p>Elmar Eisemann and Frédo Durand. 2004. Flash photography enhancement via intrinsic relighting. ACM Trans. Graph. 23, 3 (August 2004), 673–678. <a target="_blank" href="https://doi.org/10.1145/1015706.1015778">https://doi.org/10.1145/1015706.1015778</a></p>
<p>C. Chen, Q. Chen, J. Xu and V. Koltun, "Learning to See in the Dark," in 2018 IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), Salt Lake City, UT, USA, 2018, pp. 3291-3300, doi: 10.1109/CVPR.2018.00347.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Fundamentos del Procesamiento de imágenes en el Dominio de la Frecuencia]]></title><description><![CDATA[La Transformada de Fourier es una de las herramientas más influyentes en las matemáticas, capaz de revelar los patrones fundamentales que componen una señal. Aunque su interpretación suele estar oculta tras ecuaciones complejas, en este artículo se o...]]></description><link>https://codigoenllamas.com/dominio-de-la-frecuencia</link><guid isPermaLink="true">https://codigoenllamas.com/dominio-de-la-frecuencia</guid><category><![CDATA[image processing]]></category><category><![CDATA[Computer Vision]]></category><category><![CDATA[opencv]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Mon, 30 Jun 2025 01:56:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/eybM9n4yrpE/upload/25cd27caf862bbbcba47aa84dfef4a40.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La <strong>Transformada de Fourier</strong> es una de las herramientas más influyentes en las matemáticas, capaz de revelar los patrones fundamentales que componen una señal. Aunque su interpretación suele estar oculta tras ecuaciones complejas, en este artículo se ofrece una revisión accesible e intuitiva, comenzando por la versión unidimensional y extendiéndola al caso bidimensional, clave en el análisis y procesamiento de imágenes digitales. Se explican también las técnicas necesarias para visualizar e interpretar el espectro de frecuencias, que permite pasar del dominio espacial —donde una imagen se ve como una distribución de píxeles— al dominio de la frecuencia, donde se revelan sus estructuras internas organizadas según su escala y variación.</p>
<blockquote>
<p>English version of this article. <a target="_blank" href="https://medium.com/imagecraft/frequency-domain-filtering-fundamentals-and-applications-36a29924c4df">Click here</a>.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751247513465/f459bb0f-bb01-4715-89ae-32e10aa2c054.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-transformada-de-fourier-1d">Transformada de Fourier 1D</h2>
<p>Para una señal continua <em>g(t)</em>, su transformada de Fourier está definida como:</p>
<p>$$G(f) = \int_{-\infty}^{\infty} g(t) \cdot e^{-j2\pi f t} , dt$$</p><p>donde:</p>
<ul>
<li><p><em>t</em> representa el tiempo (o la posición, en el caso de imágenes),</p>
</li>
<li><p><em>f</em> representa la frecuencia,</p>
</li>
<li><p><em>j</em> = √-1 es la unidad imaginaria.</p>
</li>
</ul>
<p>Este proceso equivale a medir <strong>cuánto "resuena" la señal <em>g(t)</em> con una onda sinusoidal</strong> de frecuencia <em>f</em>. Para cada frecuencia, el resultado <em>G(f)</em> es un número complejo que codifica tanto la magnitud como la fase de esa frecuencia dentro de la señal.</p>
<p>Aunque esta formulación puede parecer abstracta, se puede entender mejor aplicando la fórmula de Euler:</p>
<p>$$e^{−j2πft}=cos⁡(2πft)−jsin⁡(2πft)$$</p><p>Lo que nos permite ver la transformada como dos integrales:</p>
<p>$$G(f) = \int_{-\infty}^{\infty} g(t) \cos(2\pi f t) \, dt - j \int_{-\infty}^{\infty} g(t) \sin(2\pi f t) \, dt$$</p><p>Estas integrales separadas representan la contribución de las componentes <strong>pares</strong> y <strong>impares</strong> de la señal. Así, <em>G(f)</em> captura información tanto de la simetría como de la variabilidad de la señal en torno a cada frecuencia.</p>
<hr />
<h3 id="heading-transformada-inversa-y-reconstruccion">Transformada Inversa y Reconstrucción</h3>
<p>Una de las propiedades más importantes de la Transformada de Fourier es su <strong>reversibilidad</strong>: a partir de la representación en el dominio de la frecuencia es posible <strong>reconstruir exactamente</strong> la señal original, siempre que se cumplan ciertas condiciones suaves sobre la señal (como ser absolutamente integrable o cuadrado-integrable).</p>
<p>La <strong>transformada inversa</strong> se define de forma muy similar a la directa, con un pequeño cambio en el signo del exponente:</p>
<p>$$g(t) = \int_{-\infty}^{\infty} G(f) \cdot e^{j2\pi f t} \, df$$</p><p>Esta simetría entre la transformada directa e inversa resalta una idea clave: la Transformada de Fourier <strong>no pierde información</strong> (cuando se aplica correctamente), sino que simplemente reorganiza los datos de una forma distinta, enfocándose en las <strong>frecuencias presentes en la señal</strong> en lugar de sus valores locales en el tiempo o el espacio.</p>
<p>La posibilidad de <strong>reconstruir exactamente una señal</strong> a partir de su representación en el dominio de la frecuencia es lo que hace viable el filtrado en este espacio. Gracias a esta propiedad, es posible transformar una señal —como una imagen— al dominio frecuencial, modificar selectivamente ciertas frecuencias mediante un filtro, y luego aplicar la transformada inversa para regresar al dominio original. Este proceso garantiza que los cambios realizados se reflejen con precisión en la señal reconstruida, conservando la información relevante mientras se eliminan o atenúan componentes no deseados.</p>
<hr />
<h2 id="heading-transformada-de-fourier-2d">Transformada de Fourier 2D</h2>
<p>En el caso de imágenes, que pueden considerarse funciones bidimensionales <em>g(x, y)</em>, se utiliza la <strong>Transformada de Fourier 2D</strong>:</p>
<p>$$G(u, v) = \iint_{-\infty}^{\infty} g(x, y) \cdot e^{-j2\pi (ux + vy)} \, dx \, dy$$</p><p>Aquí:</p>
<ul>
<li><p><em>(x, y)</em> son las coordenadas espaciales,</p>
</li>
<li><p><em>(u, v)</em> son las coordenadas de frecuencia espacial,</p>
</li>
<li><p><em>G(u, v)</em> representa la contribución de la frecuencia $(u, v)$ a la imagen.</p>
</li>
</ul>
<p>Este cambio de representación permite analizar la imagen desde una nueva perspectiva: en lugar de observar el contenido local de la imagen, nos enfocamos en <strong>cómo varía globalmente la intensidad en diferentes escalas y direcciones</strong>.</p>
<hr />
<h2 id="heading-muestreo-y-aliasing">Muestreo y Aliasing</h2>
<p>En la práctica, la mayoría de las señales que se procesan digitalmente (audio, imágenes, sensores) no son continuas, sino <strong>discretas</strong>: se obtienen mediante <strong>muestreo</strong> de una señal continua a intervalos regulares. Esta operación transforma una señal continua <em>g(t)</em> en una secuencia discreta <em>g[n] = g(nT)</em>, donde <em>T</em> es el periodo de muestreo y <em>fₛ = 1/T</em> la frecuencia de muestreo.</p>
<p>Aquí entra en juego un principio fundamental: el <strong>teorema de muestreo de Nyquist-Shannon</strong>, que establece que:</p>
<blockquote>
<p>Una señal puede ser reconstruida perfectamente a partir de sus muestras si ha sido muestreada a una frecuencia <strong>mayor que el doble</strong> de su frecuencia más alta (frecuencia de Nyquist).</p>
</blockquote>
<p>Esto significa que si una señal contiene componentes hasta 500 Hz, debemos muestrearla a más de 1000 Hz para evitar pérdida de información. Una señal que cumple esta condición se denomina <strong>banda limitada</strong>, y cuando el muestreo se realiza adecuadamente, la señal original puede ser recuperada sin ambigüedades a partir de sus muestras.</p>
<hr />
<h3 id="heading-aliasing">Aliasing</h3>
<p>Cuando la frecuencia de muestreo es <strong>insuficiente</strong>, es decir, <strong>menor</strong> que el doble de la frecuencia más alta presente en la señal, ocurre un fenómeno llamado <strong>aliasing</strong>. En este caso, las frecuencias altas comienzan a <strong>disfrazarse</strong> como frecuencias más bajas, dando lugar a distorsiones irreversibles en el dominio de la frecuencia.</p>
<p>Matemáticamente, si una señal de frecuencia f es muestreada con una frecuencia <em>fₛ</em>, la frecuencia aparente o alias que se observa está dada por:</p>
<p>$$f' = \left| f_s \cdot n - f \right| \quad n = \text{round}\left(\frac{f}{f_s}\right)$$</p><p>Esto significa que la muestra tomada a una frecuencia incorrecta puede ser indistinguible de otra frecuencia completamente diferente. Por ejemplo, si <em>f = 1000 Hz</em> y <em>fₛ = 1250 Hz</em>, el alias observado será <em>f' = 250 Hz</em>. En otras palabras, la señal de 1 kHz parecerá una de 250 Hz, y no hay forma de saber cuál era la original. Este tipo de ambigüedad destruye la posibilidad de una reconstrucción precisa.</p>
<p><em>Este fenómeno no es solo una curiosidad matemática: se manifiesta de forma visual en situaciones como el</em> <strong><em>efecto de la rueda de carreta</em></strong>*, donde las ruedas parecen girar en sentido inverso cuando su velocidad supera la capacidad de muestreo de la cámara. Por esta razón, aliasing también es conocido como el* <strong><em>efecto wagon wheel</em></strong>*.*</p>
<p>Debido a este riesgo, en sistemas digitales se emplean <strong>filtros antialiasing</strong> antes del muestreo, los cuales eliminan las frecuencias que podrían provocar ambigüedad, asegurando que la señal resultante se mantenga dentro del rango permitido por la frecuencia de muestreo.</p>
<h3 id="heading-tipos-de-muestreo">Tipos de muestreo</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751247760816/bc4c6f75-c3a7-494d-a7cb-7a84b60fee59.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Sobremuestreo</strong>: fₛ ≫ 2fₘₐₓ – Alta fidelidad, mayor tamaño de datos.</p>
</li>
<li><p><strong>Muestreo crítico</strong>: fₛ = 2fₘₐₓ – Reconstrucción exacta, pero sin margen de seguridad.</p>
</li>
<li><p><strong>Submuestreo</strong>: fₛ &lt; 2fₘₐₓ – Ocurre aliasing, se pierde información.</p>
</li>
</ul>
<h2 id="heading-transformada-de-fourier-discreta-dft">Transformada de Fourier Discreta DFT</h2>
<p>La <strong>Transformada de Fourier Discreta (DFT)</strong>. permite representar señales digitales como combinaciones de frecuencias discretas, facilitando el análisis espectral, el filtrado y la compresión de datos. Además de su aplicabilidad, la DFT tiene la ventaja de que <strong>siempre existe</strong>, independientemente de las propiedades matemáticas de la señal, lo cual no ocurre con otras variantes como la serie de Fourier.</p>
<hr />
<h3 id="heading-transformada-discreta-1d">Transformada Discreta 1D</h3>
<p>Sea <em>g[x]</em> una señal unidimensional discreta compuesta por $w$ muestras. La DFT se define como:</p>
<p>$$G[k] = \sum_{x=0}^{w-1} g[x] \cdot e^{-j2\pi \frac{kx}{w}}$$</p><p>donde <em>k = 0, 1, ..., w-1</em> es el índice de frecuencia discreta. Esta fórmula reemplaza la integral de la versión continua por una suma finita, y convierte las frecuencias continuas en frecuencias discretas y periódicas.</p>
<p>Usando la <strong>fórmula de Euler</strong>, se puede reescribir en términos de funciones reales:</p>
<p>$$G[k] = \sum_{x=0}^{w-1} g[x] \cdot \left[\cos\left(\frac{2\pi kx}{w}\right) - j \sin\left(\frac{2\pi kx}{w}\right)\right]$$</p><p>Esto revela que cada valor <em>G[k]</em> representa cuánto de una sinusoide (frecuencia) de índice <em>k</em> está presente en la señal original.</p>
<hr />
<h3 id="heading-propiedades-clave">Propiedades clave</h3>
<ul>
<li><p>La señal de entrada <em>g[x]</em> típicamente es <strong>real</strong>, pero su DFT <em>G[k]</em> es <strong>compleja</strong> (parte real + imaginaria).</p>
</li>
<li><p>La DFT produce el <strong>mismo número de muestras</strong> que la señal original: <em>w</em> puntos en el dominio de la frecuencia.</p>
</li>
<li><p>Las frecuencias resultantes son <strong>periódicas</strong>, es decir, <em>G[k] = G[k + w]</em>. Esta periodicidad es clave en el análisis espectral.</p>
</li>
</ul>
<hr />
<h3 id="heading-transformada-inversa-1d">Transformada Inversa 1D</h3>
<p>La DFT es <strong>reversible</strong>, lo que significa que es posible recuperar la señal original a partir de sus coeficientes de frecuencia:</p>
<p>$$g[x] = \frac{1}{w} \sum_{k=0}^{w-1} G[k] \cdot e^{j2\pi \frac{kx}{w}}$$</p><p>Esto garantiza que ninguna información se pierde en la transformación, siempre que se conserve la totalidad de los coeficientes <em>G[k].</em></p>
<hr />
<h2 id="heading-transformada-de-fourier-discreta-en-2d">Transformada de Fourier Discreta en 2D</h2>
<p>En una función bidimensional sea <em>g[x, y]</em> como una imagen discreta de tamaño w × h, donde <em>x</em> y <em>y</em> representan las coordenadas espaciales horizontales y verticales respectivamente. La DFT 2D está definida como:</p>
<p>$$G[k_x, k_y] = \sum_{x=0}^{w-1} \sum_{y=0}^{h-1} g[x, y] \cdot e^{-j2\pi\left( \frac{k_x x}{w} + \frac{k_y y}{h} \right)}$$</p><p>donde kₓ y <em>k_y</em> son los índices de frecuencia discreta en las direcciones horizontal y vertical, respectivamente.</p>
<hr />
<h3 id="heading-forma-real-usando-la-formula-de-euler">Forma real usando la fórmula de Euler</h3>
<p>Usando la identidad de Euler, esta expresión puede descomponerse en términos reales e imaginarios:</p>
<p>$$G[k_x, k_y] = \sum_{x=0}^{w-1} \sum_{y=0}^{h-1} g[x, y] \cdot \left[\cos\left(2\pi\left( \frac{k_x x}{w} + \frac{k_y y}{h} \right)\right) - j \sin\left(2\pi\left( \frac{k_x x}{w} + \frac{k_y y}{h} \right)\right)\right]$$</p><p>Esto permite interpretar <em>G[k_x, k_y]</em> como una combinación de senos y cosenos bidimensionales, lo que resulta útil para analizar patrones periódicos en las imágenes, como texturas o bordes.</p>
<hr />
<h3 id="heading-propiedades-clave-de-la-dft-2d">Propiedades clave de la DFT 2D</h3>
<ul>
<li><p>La <strong>imagen de entrada</strong> <em>g[x, y]</em> suele ser <strong>real</strong>, pero su DFT <em>G[kₓ , k_y]</em> es <strong>compleja</strong>, conteniendo información de <strong>magnitud</strong> y <strong>fase</strong>.</p>
</li>
<li><p>La DFT 2D produce una imagen en el dominio de la frecuencia del mismo tamaño que la original: w × h coeficientes complejos.</p>
</li>
<li><p>El espectro de frecuencias es <strong>periódico</strong> tanto en kₓ como en k_y, con periodo w y h respectivamente.</p>
</li>
<li><p>Se puede calcular como una composición de dos DFTs unidimensionales:</p>
</li>
</ul>
<p>$$\text{DFT}_{2D}[g(x,y)] = \text{DFT}_x(\text{DFT}_y(g(x,y)))$$</p><p>Esto permite reutilizar implementaciones eficientes de la DFT 1D.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">FourierTransform2D</span>(<span class="hljs-params">image</span>):</span>
    <span class="hljs-string">"""
    Computes the 2D Fourier Transform of an image step by step
    using two 1D FFTs: first along rows, then along columns.

    Parameters:
    - image: Input image (2D numpy array).

    Returns:
    - 2D Fourier Transform of the image.
    """</span>
    <span class="hljs-comment"># First, apply FFT along the rows (axis=1)</span>
    fft_rows = np.fft.fft(image, axis=<span class="hljs-number">1</span>)

    <span class="hljs-comment"># Then, apply FFT along the columns (axis=0)</span>
    fft2d = np.fft.fft(fft_rows, axis=<span class="hljs-number">0</span>)

    <span class="hljs-keyword">return</span> fft2d
</code></pre>
<hr />
<h3 id="heading-transformada-inversa-2d">Transformada Inversa 2D</h3>
<p>Al igual que en el caso unidimensional, la DFT 2D es reversible. La <strong>Transformada Inversa Discreta de Fourier 2D</strong> permite reconstruir exactamente la imagen original:</p>
<p>$$g[x, y] = \frac{1}{wh} \sum_{k_x=0}^{w-1} \sum_{k_y=0}^{h-1} G[k_x, k_y] \cdot e^{j2\pi\left( \frac{k_x x}{w} + \frac{k_y y}{h} \right)}$$</p><p>Esto garantiza que no se pierde información si se conserva el espectro completo G[kₓ , k_y].</p>
<hr />
<h2 id="heading-visualizacion-de-la-dft-2d">Visualización de la DFT 2D</h2>
<p>La Transformada Discreta de Fourier bidimensional (DFT 2D) produce una representación compleja de la frecuencia espacial de una imagen. Sin embargo, esta información no es directamente interpretable por el ojo humano, por lo que es necesario realizar ciertas transformaciones para poder visualizarla de forma útil.</p>
<hr />
<h2 id="heading-magnitud-y-fase">Magnitud y fase</h2>
<p>El primer paso consiste en descomponer la DFT compleja en dos componentes reales: la <strong>magnitud</strong> y la <strong>fase</strong>.</p>
<ul>
<li><p>La <strong>fase</strong> contiene la información sobre la alineación de las ondas sinusoidales en la reconstrucción de la imagen. Puede visualizarse escalándola linealmente al rango [0, 255] para mostrarla como una imagen en escala de grises.</p>
</li>
<li><p>La <strong>magnitud</strong>, que representa la intensidad de cada frecuencia espacial presente, es más complicada de visualizar. Si se muestra directamente, se suele obtener una imagen casi negra con un único punto brillante en la esquina superior izquierda. Este punto representa la <strong>componente DC</strong>, que normalmente tiene un valor muy alto en comparación con las demás frecuencias.</p>
</li>
</ul>
<p>Este comportamiento se debe a que el módulo de la DFT posee un rango dinámico extremadamente amplio: unas pocas frecuencias dominan en magnitud mientras que la mayoría de las demás tienen valores mucho menores. Así, si se visualiza sin ningún tipo de ajuste, los valores más pequeños quedan prácticamente invisibles.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">ComputeFourierSpectra</span>(<span class="hljs-params">image</span>):</span>
    <span class="hljs-string">"""
    Computes and returns the magnitude and phase spectra of a 2D Fourier Transform.

    This function takes a grayscale image as input, applies the 2D Fourier Transform,
    and returns both the magnitude and phase spectra. The magnitude spectrum is 
    log-scaled and normalized to enhance visibility, while the phase spectrum is also 
    normalized to the 0–255 range for visualization purposes.

    Parameters:
    ----------
    image : np.ndarray
        2D numpy array representing a grayscale image (real-valued input).

    Returns:
    -------
    magnitude : np.ndarray
        2D array (uint8) representing the normalized log-magnitude spectrum.

    phase : np.ndarray
        2D array (uint8) representing the normalized phase spectrum in the range [0, 255].

    Notes:
    -----
    - The function assumes the input is a 2D image.
    - Zero-frequency components are shifted to the center using `np.fft.fftshift`.
    - The Fourier transform is computed using a custom implementation `FourierTransform2D`.
    """</span>
    dft = FourierTransform2D(image)

    <span class="hljs-comment"># Compute magnitude and apply log-scaling</span>
    magnitude = np.abs(dft)
    magnitude = np.log1p(magnitude)  <span class="hljs-comment"># log(1 + |F(u,v)|)</span>
    magnitude = np.fft.fftshift(magnitude)
    magnitude = (magnitude / np.max(magnitude) * <span class="hljs-number">255</span>).astype(np.uint8)

    <span class="hljs-comment"># Compute and normalize phase</span>
    phase = np.angle(dft)
    phase = np.fft.fftshift(phase)
    phase = (phase + np.pi) / (<span class="hljs-number">2</span> * np.pi) * <span class="hljs-number">255</span>  <span class="hljs-comment"># Normalize to [0, 255]</span>
    phase = phase.astype(np.uint8)

    <span class="hljs-keyword">return</span> magnitude, phase
</code></pre>
<hr />
<h2 id="heading-uso-del-logaritmo-y-desplazamiento-del-espectro">Uso del logaritmo y desplazamiento del espectro</h2>
<p>Para hacer visible la magnitud, se aplican dos transformaciones:</p>
<ol>
<li><p><strong>Escala logarítmica</strong>:</p>
<p> $$\log(1 + |G(k_x, k_y)|)$$</p>
<p> Esto reduce las diferencias extremas entre las frecuencias fuertes y débiles, haciendo visibles las frecuencias de menor energía. El uso del logaritmo en este contexto es análogo a la percepción del sonido por parte del oído humano: no responde linealmente a la intensidad, sino logarítmicamente. De este modo, se preservan los detalles de baja frecuencia sin saturar las regiones dominadas por componentes de gran magnitud.</p>
</li>
<li><p><strong>Centrar la componente DC</strong>:</p>
<p> Por defecto, la componente DC (frecuencia cero) está en la esquina superior izquierda del espectro. Para una visualización más simétrica e intuitiva, se reorganizan los cuadrantes del espectro:</p>
<ul>
<li><p>A = superior izquierda</p>
</li>
<li><p>B = superior derecha</p>
</li>
<li><p>C = inferior izquierda</p>
</li>
<li><p>D = inferior derecha</p>
</li>
</ul>
</li>
</ol>
<p>    El reordenamiento <strong>D–C–B–A</strong> lleva la componente DC al centro de la imagen, colocando las bajas frecuencias en el centro y las altas en los bordes. Esta representación resulta más natural para el análisis visual, ya que alinea la estructura radial de la frecuencia con la disposición visual esperada.</p>
<hr />
<p>Una forma matemática equivalente a este reordenamiento consiste en <strong>multiplicar la imagen original por (-1)ˣ⁺ʸ</strong> antes de aplicar la DFT:</p>
<p>$$\text{DFT} \left[ g(x, y)(-1)^{x+y} \right] = G\left(k_x - \frac{w}{2}, k_y - \frac{h}{2} \right)$$</p><p>donde w y h son el ancho y el alto de la imagen. Esta modulación espacial alterna el signo de los píxeles formando un patrón similar a un tablero de ajedrez, lo que tiene como efecto el desplazamiento del espectro de frecuencia. El resultado es una traslación del espectro tal que las bajas frecuencias se ubican en el centro de la imagen transformada, facilitando así su análisis visual y cuantitativo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751246576435/ff2b83e4-ea99-471b-a0e6-2bff00388b7f.png" alt /></p>
<hr />
<h2 id="heading-interpretacion-periodica-y-replicacion">Interpretación periódica y replicación</h2>
<p>La DFT asume que la imagen de entrada es <strong>periódica</strong> en ambas direcciones. Esto implica que su espectro también es periódico.</p>
<p>Si replicamos la imagen original cuatro veces (en una disposición 2×2) y aplicamos la DFT a esta imagen extendida, obtendremos una réplica del espectro en cada cuadrante. El cuadrante central (resultante del reordenamiento D–C–B–A) corresponde al espectro con la <strong>componente DC centrada</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751246582920/ebd38a2f-a34f-4523-a2de-64f03258d579.png" alt class="image--center mx-auto" /></p>
<p>Esta observación justifica conceptualmente por qué es válido y útil reordenar los cuadrantes del espectro para facilitar su interpretación: al centrar el origen de la frecuencia, se obtiene una vista más coherente y balanceada del contenido frecuencial de la imagen.</p>
<hr />
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751246595275/532fbf45-b4fb-411a-b348-9ce392afbe4e.png" alt /></p>
<h2 id="heading-observaciones-sobre-la-distribucion-de-energia">Observaciones sobre la distribución de energía</h2>
<p>Este tipo de visualización revela patrones importantes. Por ejemplo, es común observar que las frecuencias ubicadas en el <strong>centro del espectro</strong> (bajas frecuencias) contienen la mayor parte de la energía, mientras que las frecuencias ubicadas hacia los bordes (altas frecuencias) tienen menor intensidad. Esto concuerda con el hecho de que la mayoría de las imágenes naturales presentan transiciones suaves y pocas variaciones abruptas.</p>
<p>En consecuencia:</p>
<ul>
<li><p>El centro del espectro suele <strong>brillar más</strong> debido a la mayor concentración de energía en frecuencias bajas.</p>
</li>
<li><p>Las regiones periféricas contienen información sobre <strong>bordes, texturas y detalles finos</strong> de la imagen original.</p>
</li>
<li><p>Esta distribución espectral refleja el contenido estructural de la imagen en el dominio de la frecuencia, permitiendo tanto su análisis cualitativo como su manipulación mediante filtrado.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751246804009/b0686d4c-c01e-4c7c-9411-cc98faf1ac5c.png" alt class="image--center mx-auto" /></p>
<p>La Transformada de Fourier constituye una de las herramientas más poderosas en el procesamiento de señales, ya sea en audio, imágenes o datos multidimensionales. Su capacidad para descomponer una señal en sus componentes frecuenciales permite revelar estructuras ocultas y operar sobre ellas de manera precisa. Comprender cómo se representa una imagen en el dominio de la frecuencia —y las condiciones necesarias para que esta representación sea fiel, como lo establece el teorema de muestreo de Nyquist-Shannon— es fundamental antes de aplicar cualquier técnica de filtrado.</p>
<p>En este artículo nos enfocamos en los fundamentos teóricos y computacionales que permiten transformar una imagen desde el dominio espacial al dominio frecuencial. También se abordó el fenómeno del aliasing, una distorsión crítica que puede surgir cuando las señales no se muestrean adecuadamente. En artículos posteriores, exploraremos con mayor profundidad el <strong>filtrado en el dominio de la frecuencia</strong>, analizando distintos tipos de filtros (ideales, Butterworth, gaussianos, etc.), su implementación práctica, y sus efectos visuales y espectrales en imágenes reales. Esta base conceptual será clave para comprender por qué y cómo aplicar estos filtros de forma eficaz en aplicaciones reales de procesamiento digital de imágenes.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item><item><title><![CDATA[Graphics pipeline en OpenGL]]></title><description><![CDATA[En gráficos por computadora 3D al proceso de transformar los datos de una escena tridimensional en una representación 2D que se pueda visualizar en pantalla se le conoce como graphics pipeline o pipeline gráfico, y está compuesto por una serie de eta...]]></description><link>https://codigoenllamas.com/graphics-pipeline-en-opengl</link><guid isPermaLink="true">https://codigoenllamas.com/graphics-pipeline-en-opengl</guid><category><![CDATA[computer graphics]]></category><category><![CDATA[openGL]]></category><category><![CDATA[C++]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Fri, 20 Jun 2025 06:03:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/d2w-_1LJioQ/upload/08e5e017278473dfb8df97aed1ca06c4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En gráficos por computadora 3D al proceso de transformar los datos de una escena tridimensional en una representación 2D que se pueda visualizar en pantalla se le conoce como <em>graphics pipeline</em> o <em>pipeline gráfico</em>, y está compuesto por una serie de etapas que transforman y procesan la información de la escena hasta producir los píxeles finales que serán renderizados.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750382183199/4c07889a-ee12-4519-96d2-b6b4ec7d3068.png" alt class="image--center mx-auto" /></p>
<p>Podemos imaginar al pipeline gráfico como una máquina de procesamiento en la que introducimos nuestros datos más básicos —como los vértices— y, a medida que avanzan por cada etapa, estos datos se transforman según los comandos e instrucciones que hemos definido en nuestro programa. Al final del recorrido, obtenemos una imagen 2D lista para mostrarse en pantalla.</p>
<blockquote>
<p>English version of this article. <a target="_blank" href="https://medium.com/gitconnected/graphics-pipeline-in-opengl-9db1c264f374">Click here</a></p>
</blockquote>
<h1 id="heading-graphics-pipeline">Graphics pipeline</h1>
<p>Para comprender mejor estos conceptos, exploraremos cómo funciona cada etapa del proceso, acompañándola con el código necesario en C++, OpenGL y GLSL.</p>
<h2 id="heading-vertex-array">vertex array</h2>
<p><em>Aunque no es técnicamente parte del pipeline de renderizado, el proceso de obtención de los vértices que definen un objeto 3D es muy importante. Estos vértices suelen obtenerse mediante modelado 3D, donde se manipulan nubes de puntos para crear mallas poligonales (típicamente trianguladas).</em></p>
<p>En ejemplos posteriores trabajaremos con modelos 3D complejos, pero para esta demostración usaremos una geometría simple: un cuadrado construido con dos triángulos</p>
<pre><code class="lang-cpp">    <span class="hljs-comment">// Array de vértices para un cuadrado (dos triángulos)</span>
    <span class="hljs-keyword">float</span> vertexPositions[] = {
        <span class="hljs-comment">// Primer triángulo</span>
        <span class="hljs-number">-0.5f</span>, <span class="hljs-number">-0.5f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-comment">// esquina inferior izquierda</span>
        <span class="hljs-number">0.5f</span>, <span class="hljs-number">-0.5f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-comment">// esquina inferior derecha</span>
        <span class="hljs-number">0.5f</span>,  <span class="hljs-number">0.5f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-comment">// esquina superior derecha</span>

        <span class="hljs-comment">// Segundo triángulo</span>
        <span class="hljs-number">-0.5f</span>, <span class="hljs-number">-0.5f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-comment">// esquina inferior izquierda</span>
        <span class="hljs-number">0.5f</span>,  <span class="hljs-number">0.5f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-comment">// esquina superior derecha</span>
        <span class="hljs-number">-0.5f</span>,  <span class="hljs-number">0.5f</span>, <span class="hljs-number">0.0f</span>  <span class="hljs-comment">// esquina superior izquierda</span>
    };
</code></pre>
<h2 id="heading-vertex-shader">Vertex Shader</h2>
<p>El <em>vertex shader</em> es la <strong>primera etapa programable del pipeline gráfico</strong> de OpenGL y también la única obligatoria: Su función principal es transformar las posiciones de los vértices (de coordenadas del mundo a coordenadas de pantalla) y generar información que será utilizada por las siguientes etapas del pipeline.</p>
<h3 id="heading-vertex-pulling">Vertex Pulling</h3>
<p>Antes de que se ejecute el vertex shader, OpenGL realiza automáticamente una etapa fija conocida como <strong>vertex pulling</strong>. Esta etapa <strong>no es programable por el usuario</strong>, y su tarea es <strong>extraer los datos de los vértices desde la memoria (almacenados en buffers)</strong> y proporcionarlos al shader como entradas.</p>
<p>Estos datos suelen residir en objetos llamados <strong>VBOs</strong> (<em>Vertex Buffer Objects</em>), que contienen atributos como posiciones, normales o coordenadas de textura y la forma en que estos atributos se organizan y enlazan se encapsula en un <strong>VAO</strong> (<em>Vertex Array Object</em>).</p>
<h3 id="heading-configuracion-en-c">Configuración en C++</h3>
<p>A continuación se muestra cómo se configura configura el <strong>VBO</strong> y <strong>VAO</strong></p>
<pre><code class="lang-cpp">glGenVertexArrays(numVAOs, vao);                 <span class="hljs-comment">// Genera un VAO (almacena la configuración de atributos)</span>
glBindVertexArray(vao[<span class="hljs-number">0</span>]);                       <span class="hljs-comment">// Activa el VAO</span>

glGenBuffers(numVBOs, vbo);                      <span class="hljs-comment">// Genera un VBO (almacena datos de vértices)</span>
glBindBuffer(GL_ARRAY_BUFFER, vbo[<span class="hljs-number">0</span>]);           <span class="hljs-comment">// Lo enlaza como buffer de tipo GL_ARRAY_BUFFER</span>
glBufferData(GL_ARRAY_BUFFER, <span class="hljs-keyword">sizeof</span>(vertexPositions), vertexPositions, GL_STATIC_DRAW);  
<span class="hljs-comment">// Carga los datos de los vértices al buffer</span>

glEnableVertexAttribArray(<span class="hljs-number">0</span>);                    <span class="hljs-comment">// Habilita el atributo de vértice en la ubicación 0</span>
glVertexAttribPointer(<span class="hljs-number">0</span>, <span class="hljs-number">3</span>, GL_FLOAT, GL_FALSE, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>); 
<span class="hljs-comment">// Define cómo deben interpretarse los datos del VBO: </span>
<span class="hljs-comment">// - índice 0 (layout location)</span>
<span class="hljs-comment">// - 3 componentes por vértice (x, y, z)</span>
<span class="hljs-comment">// - tipo GL_FLOAT</span>
<span class="hljs-comment">// - sin normalizar</span>
<span class="hljs-comment">// - sin separación entre atributos (stride = 0)</span>
<span class="hljs-comment">// - sin desplazamiento inicial (offset = 0)</span>
</code></pre>
<h3 id="heading-correspondencia-en-el-shader">Correspondencia en el Shader</h3>
<p>En el vertex shader, los atributos de vértice se declaran con el calificador <code>in</code>, que indica que los datos llegarán desde fuera del shader (desde la etapa de vertex pulling)</p>
<pre><code class="lang-cpp">    <span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *vshaderSource =
        <span class="hljs-string">"#version 450\n"</span>
        <span class="hljs-string">"layout(location=0) in vec3 position;\n"</span>
        <span class="hljs-string">"void main(void) {\n"</span>
        <span class="hljs-string">"    gl_Position = vec4(position, 1.0);\n"</span>
        <span class="hljs-string">"}\n"</span>;
</code></pre>
<ul>
<li><p><code>in</code> se usa para <strong>recibir atributos de entrada</strong>. En el vertex shader, suele usarse para atributos como posición, color o normales.</p>
</li>
<li><p><code>out</code> se usa para <strong>enviar información a la siguiente etapa</strong>, como un fragment shader o geometry shader.</p>
</li>
<li><p><code>layout(location=0)</code> indica que este atributo corresponde al índice 0, el mismo que configuramos desde C++ con <code>glVertexAttribPointer</code>.</p>
</li>
<li><p><code>in vec3 position;</code> declara la variable que recibirá la posición de cada vértice.</p>
</li>
<li><p>En el cuerpo de <code>main()</code>, esa posición se convierte en un <code>vec4</code> y se asigna a <code>gl_Position</code>, la variable especial que representa la posición final del vértice.</p>
</li>
</ul>
<p>Esta rutina de entrada y salida es la forma principal de <strong>transmitir información personalizada entre las distintas etapas del pipeline gráfico en OpenGL</strong>. A través de los calificadores <code>in</code> y <code>out</code>, no solo es posible llevar posiciones de vértices desde la aplicación al vertex shader, sino también <strong>pasar datos adicionales</strong> desde fuera como colores, normales, coordenadas de textura o cualquier otro atributo necesario para el procesamiento gráfico.</p>
<h2 id="heading-primitivas-y-ensamblaje">Primitivas y ensamblaje</h2>
<p>Una vez que el <em>vertex shader</em> ha transformado las posiciones de los vértices y ha enviado sus datos a las etapas posteriores, el pipeline gráfico necesita agrupar esos vértices en unidades geométricas llamadas <strong>primitivas</strong>. Estas primitivas son la base sobre la cual OpenGL genera la geometría visible en pantalla. Las más comunes incluyen puntos, líneas y triángulos, siendo estos últimos la forma más utilizada en el renderizado 3D moderno.</p>
<p>En nuestro ejemplo, hemos definido un arreglo de vértices que representa dos triángulos, los cuales juntos forman un cuadrado. Estos vértices se almacenan en un VBO y son interpretados por OpenGL según el modo de dibujo especificado en la función <code>glDrawArrays</code>:</p>
<pre><code class="lang-cpp">glDrawArrays(GL_TRIANGLES, <span class="hljs-number">0</span>, <span class="hljs-number">6</span>);
</code></pre>
<p>El primer parámetro (<code>GL_TRIANGLES</code>) le indica a OpenGL que cada grupo de tres vértices consecutivos debe interpretarse como un triángulo independiente. Esto implica que los vértices serán consumidos de la siguiente manera:</p>
<ul>
<li><p>Vértices 0, 1 y 2 → primer triángulo</p>
</li>
<li><p>Vértices 3, 4 y 5 → segundo triángulo</p>
</li>
</ul>
<h2 id="heading-rasterizacion">Rasterización</h2>
<p>Una vez que las primitivas han sido correctamente ensambladas a partir de los vértices procesados por el <em>vertex shader</em>, el pipeline gráfico de OpenGL continúa con una serie de etapas cruciales que determinan qué partes de esas primitivas realmente se dibujarán en pantalla. Este conjunto de pasos intermedios asegura que solo los fragmentos visibles y válidos de la geometría lleguen al <em>fragment shader</em>.</p>
<h3 id="heading-clipping">Clipping</h3>
<p>La primera de estas etapas es el <strong>clipping</strong>, cuyo objetivo es eliminar las partes de las primitivas que quedan fuera del espacio visible de la escena. Este espacio visible es definido por el sistema de coordenadas de recorte (<em>clip space</em>), que abarca desde <code>-1</code> hasta <code>1</code> en cada eje después de la transformación por la matriz de proyección.</p>
<p>Por ejemplo, si una primitiva cruza los límites de este volumen, OpenGL la recorta automáticamente y conserva solo la porción que permanece dentro del espacio visible. Este proceso es automático y no requiere intervención del programador.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750382778891/4de2f84d-e53e-42c4-b21f-a6de745a9b74.png" alt="Diagram showing a red triangular shape partially inside and outside a rectangle on the left. An arrow labeled &quot;Clip&quot; points to a second rectangle on the right, where the triangle is clipped to fit within the boundaries." /></p>
<blockquote>
<p>🛈 El clipping puede modificar las primitivas originales, generando nuevos vértices en los bordes del volumen de recorte. Exploraremos el proceso con más detalle en futuros programas.</p>
</blockquote>
<h3 id="heading-transformacion-al-viewport">Transformación al viewport</h3>
<p>Luego del clipping, los vértices que han sobrevivido son transformados desde coordenadas normalizadas (NDC, <em>Normalized Device Coordinates</em>) al sistema de coordenadas de pantalla mediante la <strong>transformación de viewport</strong>. Esta etapa adapta la escena para que se dibuje en una región específica de la ventana definida por el usuario (el <em>viewport</em>), usualmente con <code>glViewport()</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750382807062/5bbd1176-61f7-4526-aa60-f5b07a64e4f9.jpeg" alt="Diagram illustrating world and device coordinates. On the left, a larger triangle and rectangle show the &quot;Window&quot; in world coordinates with labeled axes and min/max points. On the right, a smaller version in &quot;ViewPort&quot; shows the device coordinates. Axes and limits are also labeled for both." /></p>
<p>Este paso toma en cuenta el tamaño real de la ventana y convierte las coordenadas flotantes en coordenadas absolutas de píxeles, permitiendo que las primitivas se alineen correctamente en la pantalla.</p>
<blockquote>
<p>🛈 Aunque aquí se menciona brevemente, el <em>viewport</em> y su rol en el pipeline serán explorados con mayor profundidad más adelante.</p>
</blockquote>
<hr />
<h3 id="heading-culling-descartado-de-caras">Culling (descartado de caras)</h3>
<p>Antes de que las primitivas lleguen a la rasterización, OpenGL puede realizar una etapa llamada <strong>culling</strong> para descartar aquellas caras que no deberían ser visibles desde la posición actual de la cámara. Esto se basa en la orientación de los vértices (conocido como <em>winding order</em>): si los vértices se ordenan en sentido horario o antihorario.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750382829059/e0b7dde9-fdea-4d33-9a2d-7c5301731b94.png" alt="Diagram illustrating a view frustum with concepts of view-frustum culling, back-face culling, and occlusion culling. A visible object is shown inside the frustum, while other objects are outside, representing different culling techniques." /></p>
<p>Esta técnica es útil para mejorar el rendimiento, ya que evita renderizar superficies ocultas, como la parte trasera de un objeto sólido.</p>
<blockquote>
<p>🛈 El culling también se tratará de forma detallada en un artículo posterior, incluyendo su configuración con <code>glEnable(GL_CULL_FACE)</code>.</p>
</blockquote>
<hr />
<h3 id="heading-rasterizacion-de-primitivas">Rasterización de primitivas</h3>
<p>Finalmente, las primitivas visibles pasan por la <strong>etapa de rasterización</strong>, donde se convierten en fragmentos. Aquí, OpenGL determina <strong>qué píxeles de la pantalla están cubiertos por la primitiva</strong> y genera un fragmento por cada uno de ellos. Estos fragmentos contienen información interpolada de los vértices originales, como color, coordenadas de textura o normales.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750382866536/085f8529-7341-4ec8-a337-0e2124516ab4.jpeg" alt /></p>
<p>Cada fragmento generado será enviado al <strong>fragment shader</strong>, que se encargará de calcular su color final (y otras propiedades) antes de ser escrito al framebuffer.</p>
<p>La rasterización marca la transición del espacio continuo de coordenadas al mundo discreto de los píxeles, y es una de las etapas más intensivas del pipeline.</p>
<h2 id="heading-fragment-shader">Fragment Shader</h2>
<p>Una vez que la <strong>rasterización</strong> ha generado todos los fragmentos (uno por cada píxel cubierto por una primitiva), la siguiente etapa programable del pipeline gráfico es el <strong>fragment shader</strong>. Su tarea principal es <strong>calcular el color de cada fragmento individual</strong>, y potencialmente otras propiedades como la profundidad (<em>depth</em>), transparencia o coordenadas para mapas de texturas.</p>
<p>A diferencia del vertex shader, que opera por cada vértice, el fragment shader se ejecuta <strong>una vez por fragmento</strong>, lo que puede traducirse en millones de ejecuciones por cuadro en una escena compleja. Es aquí donde se definen los detalles visuales más importantes como: iluminación, texturas, reflejos, sombreado, etc.</p>
<p>este fragment shader asigna a cada fragmento un color rojo puro:</p>
<pre><code class="lang-cpp"><span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *fshaderSource =
    <span class="hljs-string">"#version 450\n"</span>
    <span class="hljs-string">"out vec4 color;\n"</span>
    <span class="hljs-string">"void main(void) {\n"</span>
    <span class="hljs-string">"    color = vec4(1.0, 0.0, 0.0, 1.0);\n"</span> <span class="hljs-comment">// Rojo</span>
    <span class="hljs-string">"}\n"</span>;
</code></pre>
<ul>
<li><p><code>out vec4 color;</code>: Declara una variable de salida llamada <code>color</code>. Esta variable será el valor final que se escriba en el framebuffer para cada fragmento.</p>
</li>
<li><p><code>vec4(1.0, 0.0, 0.0, 1.0)</code>: se asigna el color rojo en a cada fragmento.</p>
</li>
</ul>
<p>En este caso, como no hay atributos adicionales, el fragment shader simplemente actúa como un generador de color constante.</p>
<h2 id="heading-etapas-finales-pruebas-y-mezcla-de-fragmentos">Etapas finales: pruebas y mezcla de fragmentos</h2>
<p>OpenGL realiza una serie de pruebas conocidas como <strong>etapas finales</strong>. Estas pruebas permiten controlar <strong>si el fragmento será realmente escrito en el framebuffer</strong> y cómo debe combinarse con los valores ya presentes en él. Estas etapas, aunque son parte del pipeline, pueden habilitarse o configurarse según el comportamiento deseado.</p>
<h3 id="heading-pruebas-de-fragmentos-final-fragment-tests">Pruebas de fragmentos (Final Fragment Tests)</h3>
<p>Las pruebas más comunes son:</p>
<ul>
<li><p><strong>Prueba de profundidad (<em>Depth Test</em>)</strong><br />  Determina si un fragmento debe escribirse o no, comparando su valor de profundidad (<code>gl_FragDepth</code>) con el que ya está almacenado en el <strong>depth buffer</strong>. Si la prueba falla, el fragmento es descartado.<br />  Se activa con:</p>
<pre><code class="lang-c++">  glEnable(GL_DEPTH_TEST);
</code></pre>
</li>
<li><p><strong>Prueba de stencil (<em>Stencil Test</em>)</strong><br />  Permite crear máscaras complejas en pantalla, útiles para efectos como espejos o reflejos. Se activa con:</p>
<pre><code class="lang-c++">  glEnable(GL_STENCIL_TEST);
</code></pre>
</li>
<li><p><strong>Prueba de scissor (<em>Scissor Test</em>)</strong><br />  Limita el área de la ventana donde se puede dibujar. Es útil para renderizar solo una región específica de la pantalla.</p>
<pre><code class="lang-c++">  glEnable(GL_SCISSOR_TEST);
  glScissor(x, y, width, height);
</code></pre>
</li>
</ul>
<p>Estas pruebas se ejecutan en orden, y si alguna de ellas falla, el fragmento se descarta, ahorrando cómputo y evitando escribir resultados innecesarios.</p>
<hr />
<h3 id="heading-blending-combinando-colores-en-pantalla">Blending: combinando colores en pantalla</h3>
<p>Si un fragmento pasa todas las pruebas, entonces OpenGL puede combinar su color con el color que ya se encuentra en el framebuffer. Este proceso se llama <strong>blending</strong> (mezcla), y es esencial para representar <strong>transparencia, efectos de iluminación suaves, partículas, humo, etc.</strong></p>
<blockquote>
<p>🛈 Todas esta pruebas y configuraciones sera tratará de forma detallada en programas posteriores.</p>
</blockquote>
<hr />
<h2 id="heading-resultados">Resultados</h2>
<p>Finalmente, si el fragmento pasa las pruebas y el blending ha sido aplicado (si está activo), el resultado final se escribe en el framebuffer. Este es el paso que realmente <strong>altera los píxeles de la ventana visible</strong>, concluyendo así el procesamiento de la imagen para ese cuadro.</p>
<h3 id="heading-ejemplo-1">Ejemplo 1</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750396178808/e922b973-594c-4d99-b6f9-63adf6f1fe62.png" alt="A bright red rectangle centered against a black background." class="image--center mx-auto" /></p>
<h3 id="heading-ejemplo-2">Ejemplo 2</h3>
<p>puedes usar otras primitivas como las lineas para dibujar la geometría.</p>
<pre><code class="lang-cpp">    glDrawArrays(GL_LINE_LOOP, <span class="hljs-number">0</span>, <span class="hljs-number">6</span>); <span class="hljs-comment">// dibujar el cuadrado como un loop de lineas</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750396448870/91df7e28-9eb6-442a-b48f-9c4518aecfa8.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-ejemplo-3"><strong>Ejemplo 3</strong></h3>
<p>Con una pequeña modificación en el <em>vertex shader</em>, es posible asignar un color específico a cada vértice y transmitir esa información al <em>fragment shader</em> utilizando las variables <code>out</code> y <code>in</code>, que permiten compartir datos entre etapas del pipeline.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750397432761/d33a3007-5b78-4dd2-9744-f0cf53f6b902.png" alt="Gradient with smooth transitions between various colors including blue, green, red, and yellow, framed by a black background." class="image--center mx-auto" /></p>
<pre><code class="lang-cpp">    <span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *vshaderSource =
        <span class="hljs-string">"#version 450\n"</span>
        <span class="hljs-string">"layout(location=0) in vec3 position;\n"</span>
        <span class="hljs-string">"out vec4 color_;\n"</span>
        <span class="hljs-string">"void main(void) {\n"</span>
        <span class="hljs-string">"    gl_Position = vec4(position, 1.0);\n"</span>
        <span class="hljs-string">"    if(gl_VertexID == 0 || gl_VertexID == 3) {\n"</span>
        <span class="hljs-string">"        color_ = vec4(1.0, 0.0, 0.0, 1.0); // Rojo para los vértices 0 y 3\n"</span>
        <span class="hljs-string">"    } else if(gl_VertexID == 1 || gl_VertexID == 5) {\n"</span>
        <span class="hljs-string">"        color_ = vec4(0.0, 0.0, 1.0, 1.0); // Verde para los vértices 1 y 4\n"</span>
        <span class="hljs-string">"    } else {\n"</span>
        <span class="hljs-string">"        color_ = vec4(0.0, 1.0, 0.0, 1.0); // Azul para los vértices 2 y 5\n"</span>
        <span class="hljs-string">"    }\n"</span>
        <span class="hljs-string">"}\n"</span>;
</code></pre>
<h4 id="heading-que-hace-este-shader">¿Qué hace este shader?</h4>
<ul>
<li><p>Primero, se posiciona cada vértice en el espacio de clip (<code>gl_Position</code>) usando los datos recibidos desde el VBO (<code>vec3 position</code>).</p>
</li>
<li><p>Luego, se utiliza la variable incorporada <code>gl_VertexID</code> para asignar un color específico a ciertos vértices. Esta variable representa el índice del vértice que se está procesando en ese momento.</p>
</li>
<li><p>Dependiendo del valor de <code>gl_VertexID</code>, se asigna un color diferente a <code>color_</code>, que es una variable <code>out</code>. Esta variable será enviada a la siguiente etapa del pipeline: el fragment shader.</p>
</li>
</ul>
<p>Esto permite definir colores por vértice <strong>sin necesidad de pasar un segundo atributo en el VBO</strong>, utilizando solo la lógica dentro del shader.</p>
<pre><code class="lang-cpp">
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *fshaderSource =
        <span class="hljs-string">"#version 450\n"</span>
        <span class="hljs-string">"out vec4 color;\n"</span>
        <span class="hljs-string">"in vec4 color_;\n"</span>
        <span class="hljs-string">"void main(void) {\n"</span>
        <span class="hljs-string">"    color =  color_;\n"</span> 
        <span class="hljs-string">"}\n"</span>;
</code></pre>
<ul>
<li><p>Aquí, el fragment shader <strong>recibe</strong> el color generado por el vertex shader a través de la variable <code>in color_</code>.</p>
</li>
<li><p>El valor recibido no es exactamente el que se escribió en el vertex shader, sino <strong>una versión interpolada</strong> que OpenGL calcula automáticamente <strong>en la etapa de rasterización</strong>, dependiendo de la posición del fragmento dentro del triángulo.</p>
</li>
<li><p>Finalmente, este color se asigna a la salida final del fragment shader, la variable <code>out color</code>, que se convierte en el color visible del píxel.</p>
</li>
</ul>
<p>Cada etapa del pipeline gráfico cumple una función específica y secuencial en la transformación de los datos de la escena, preparando y refinando la información hasta convertirla en fragmentos listos para mostrarse en pantalla. En esta sección hemos realizado un recorrido general por cada una de estas fases, entendiendo su papel dentro del flujo de renderizado. Más adelante, exploraremos cómo estas etapas pueden ser controladas y modificadas para construir escenas más complejas, dinámicas e interesantes.</p>
<p><strong>Puedes encontrar el código fuente en el siguiente repositorio</strong></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Computer-Graphics-Programming">https://github.com/Nobody-1321/Computer-Graphics-Programming</a></div>
]]></content:encoded></item><item><title><![CDATA[Introducción a OpenGL: Tutorial para Principiantes]]></title><description><![CDATA[En los inicios de la programación gráfica, los desarrolladores debían adaptar su código a cada tipo de hardware o sistema operativo, lo que dificultaba la portabilidad y el mantenimiento. Para resolver este problema surgieron bibliotecas como OpenGL ...]]></description><link>https://codigoenllamas.com/introduccion-a-opengl-tutorial-para-principiantes</link><guid isPermaLink="true">https://codigoenllamas.com/introduccion-a-opengl-tutorial-para-principiantes</guid><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Mon, 16 Jun 2025 06:08:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/bTx3m6e0wX4/upload/ca625029e9356765dc6828013ff54c8d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En los inicios de la programación gráfica, los desarrolladores debían adaptar su código a cada tipo de hardware o sistema operativo, lo que dificultaba la portabilidad y el mantenimiento. Para resolver este problema surgieron bibliotecas como <strong>OpenGL (Open Graphics Library)</strong>, una de las más influyentes y duraderas, que proporciona una interfaz multiplataforma estandarizada para crear gráficos 2D y 3D. Su función principal es actuar como una capa intermedia entre las aplicaciones y la GPU, ocultando las particularidades del hardware y permitiendo que el mismo código funcione en distintos entornos.</p>
<blockquote>
<p><strong>English version of this article</strong></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://medium.com/p/aeb0def49e5a">https://medium.com/p/aeb0def49e5a</a></div>
<p> </p>
</blockquote>
<p>Este enfoque facilita aprovechar el paralelismo de las GPU modernas: operaciones como transformar vértices, aplicar texturas o calcular colores de píxeles pueden ejecutarse de forma masiva y simultánea, lo que permite alcanzar un rendimiento elevado en aplicaciones interactivas y en tiempo real.</p>
<p>Sin embargo, el avance del hardware y la necesidad de un control más preciso han impulsado el desarrollo de APIs de bajo nivel como <strong>Vulkan</strong>, que ofrecen acceso directo a la GPU y una personalización detallada de cada etapa del proceso gráfico. Aunque <strong>OpenGL</strong> sigue siendo ampliamente utilizado —especialmente por su simplicidad y su curva de aprendizaje más amigable—, en contextos donde se busca la máxima eficiencia y control, <strong>Vulkan</strong> se está convirtiendo en la opción preferida.</p>
<p>Ahora que entendemos el papel de OpenGL en la programación gráfica y por qué sigue siendo una herramienta fundamental para iniciarse en este campo, es momento de preparar el entorno necesario para comenzar a escribir nuestros primeros programas. Esto implica integrar correctamente las bibliotecas complementarias, gestionar dependencias y establecer una estructura de proyecto robusta que facilite el desarrollo a medida que avanzamos.</p>
<h1 id="heading-configuracion-y-estructura-del-entorno">Configuración y Estructura del Entorno</h1>
<p>Antes de comenzar a escribir código, es necesario contar con un entorno de desarrollo que nos permita compilar y ejecutar nuestras aplicaciones.</p>
<p>Para trabajar con OpenGL de forma cómoda necesitamos integrar otras bibliotecas —como GLFW para el manejo de ventanas y contextos, GLAD para la carga de funciones—GLM para el manejo del algebra lineal entre otras que estaremos usando a lo largo de esta serie, para lograr la inclusion correcta utilizaremos <strong>CMake</strong>, que nos permiten coordinar todos estos componentes de forma ordenada.</p>
<p>En esta sección te explico cómo está estructurado el entorno de desarrollo, Si bien configurar todo esto puede requerir algo de experiencia, ya tienes el proyecto preparado en el repositorio principal de esta serie, así que puedes empezar directamente. si ya dominas estas herramientas, también tienes la libertad de montar tu propia configuración desde cero.</p>
<h2 id="heading-estructura-del-proyecto">Estructura del Proyecto</h2>
<p>El proyecto está organizado en carpetas específicas que separan claramente el código fuente, las librerías, los binarios y los shaders (la distribución del proyecto cambiara a lo largo de la serie).</p>
<p>A continuación, se muestra una visión general de la estructura:</p>
<pre><code class="lang-bash">graphics/
├── src/                <span class="hljs-comment"># Archivos fuente .cpp, uno por cada programa</span>
│   └── shaders/        <span class="hljs-comment"># Shaders GLSL utilizados por los programas</span>
├── libs/               <span class="hljs-comment"># Librerías locales como GLAD</span>
├── build/              <span class="hljs-comment"># Carpeta generada por CMake para la compilación</span>
├── CMakeLists.txt      <span class="hljs-comment"># Script principal de configuración del proyecto</span>
├── conanfile.py        <span class="hljs-comment"># Script de Conan para gestión de dependencias</span>
├── buildLinux.py       <span class="hljs-comment"># Script Python para compilar en Linux</span>
├── buildWindows.py     <span class="hljs-comment"># Script Python para compilar en Windows</span>
└── README.md           <span class="hljs-comment"># Instrucciones y documentación del proyecto</span>
</code></pre>
<h2 id="heading-construccion-del-proyecto">Construcción del proyecto</h2>
<p><strong>CMake</strong> es una herramienta multiplataforma ampliamente utilizada en proyectos C++ para generar scripts de compilación adaptados a distintos entornos. En esta serie, nos enfocaremos principalmente en los sistemas operativos <strong>Windows y Linux</strong>, que son los más comunes entre los desarrolladores. Si bien CMake es sumamente poderosa y flexible, también puede resultar compleja para quienes no están familiarizados con ella. Por eso, aunque haremos uso de esta herramienta y puede que aparezcan algunas referencias a su funcionamiento, el enfoque de esta serie será mantener las cosas lo más simples posible, ya que el objetivo no es aprender CMake, sino centrarnos en la programación gráfica con OpenGL.</p>
<ul>
<li><p>Se requiere CMake versión 3.25 o superior.</p>
</li>
<li><p>Se configura el proyecto para usar <strong>C++23</strong>.</p>
</li>
<li><p>Se incluyen manualmente subdirectorios como <code>libs/glad</code>.</p>
</li>
<li><p>Se integran las dependencias externas <code>glfw</code>, <code>glm</code> y <code>OpenGL</code> mediante <code>find_package</code>, que son gestionadas por Conan.</p>
</li>
<li><p>Se detectan automáticamente todos los archivos <code>.cpp</code> en la carpeta <code>src/</code> y se compilan como ejecutables individuales.</p>
</li>
<li><p>Los ejecutables se colocan en <code>build/programs/</code>.</p>
</li>
<li><p>Los shaders (<code>.glsl</code>) se copian automáticamente a <code>build/programs/shaders/</code>, garantizando que estén disponibles junto al ejecutable al momento de la ejecución.</p>
</li>
<li><pre><code class="lang-bash">    file(GLOB SOURCE_FILES <span class="hljs-variable">${SOURCE_MAIN_DIR}</span>/*.cpp)

    foreach(SOURCE_FILE <span class="hljs-variable">${SOURCE_FILES}</span>)
        get_filename_component(SOURCE_FILE_NAME <span class="hljs-variable">${SOURCE_FILE}</span> NAME_WE)

        add_executable(<span class="hljs-variable">${SOURCE_FILE_NAME}</span> <span class="hljs-variable">${SOURCE_FILE}</span>)
        target_link_libraries(<span class="hljs-variable">${SOURCE_FILE_NAME}</span> PRIVATE glad glfw glm::glm <span class="hljs-variable">${OPENGL_LIBRARIES}</span>)

        set_target_properties(<span class="hljs-variable">${SOURCE_FILE_NAME}</span> PROPERTIES
            RUNTIME_OUTPUT_DIRECTORY <span class="hljs-variable">${CMAKE_CURRENT_BINARY_DIR}</span>/programs
        )
    endforeach()
</code></pre>
<p>  Esto permite agregar nuevos programas simplemente añadiendo un nuevo archivo <code>.cpp</code> en <code>src/</code>, sin necesidad de modificar el archivo CMake.</p>
</li>
</ul>
<h2 id="heading-gestion-de-dependencias">Gestión de Dependencias</h2>
<p>En este caso, utilizamos <strong>Conan</strong>, un moderno gestor de paquetes para C/C++, para descargar e instalar las dependencias externas como GLFW y GLM. Esto elimina la necesidad de configurar rutas manualmente o instalar librerías a mano.</p>
<p>El archivo <a target="_blank" href="http://conanfile.py"><code>conanfile.py</code></a> especifica las librerías requeridas y sus versiones. Para preparar el entorno, basta con ejecutar</p>
<pre><code class="lang-bash">conan install . --build=missing
</code></pre>
<p>Esto descargará y configurará automáticamente todas las dependencias necesarias para que CMake pueda encontrarlas.</p>
<h2 id="heading-scripts-de-construccion">Scripts de Construcción</h2>
<p>Para facilitar el proceso de compilación en distintos sistemas operativos, el proyecto incluye scripts auxiliares:</p>
<ul>
<li><p><a target="_blank" href="http://buildLinux.py"><code>buildLinux.py</code></a>: Genera y compila el proyecto en sistemas Linux.</p>
</li>
<li><p><a target="_blank" href="http://buildWindows.py"><code>buildWindows.py</code></a>: Hace lo mismo en Windows, utilizando rutas y generadores adecuados para ese entorno.</p>
</li>
</ul>
<h1 id="heading-primer-programa-con-opengl">Primer Programa con OpenGL</h1>
<p>En este primer ejemplo, creamos una ventana a pantalla completa utilizando GLFW y limpiamos la pantalla con un color rojo utilizando OpenGL. Es una excelente forma de comprobar que nuestra configuración del entorno gráfico funciona correctamente.</p>
<p>A continuación se presenta el código fuente del programa y su análisis detallado:</p>
<h2 id="heading-hello-window">hello window</h2>
<pre><code class="lang-cpp"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glad/glad.h&gt;  // Carga de funciones OpenGL</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;GLFW/glfw3.h&gt; // Manejo de ventanas y entrada</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;     // Salida de texto a consola</span></span>
</code></pre>
<p>Estas tres cabeceras son fundamentales: <code>glad.h</code> para inicializar funciones OpenGL modernas, <code>glfw3.h</code> para crear y gestionar la ventana, y <code>&lt;iostream&gt;</code> para imprimir información del monitor.</p>
<h2 id="heading-inicializacion-y-funcion-de-dibujo">Inicialización y función de dibujo</h2>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">init</span><span class="hljs-params">(GLFWwindow* window)</span></span>{
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">display</span><span class="hljs-params">(GLFWwindow* window)</span></span>{
    glClear(GL_COLOR_BUFFER_BIT);       
    glClearColor(<span class="hljs-number">1.0f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-number">0.0f</span>, <span class="hljs-number">1.0f</span>);
}
</code></pre>
<ul>
<li><p>La función <code>init</code> está preparada para futuras inicializaciones, como la carga de shaders o configuración de buffers.</p>
</li>
<li><p>La función <code>display</code> establece el color de fondo (rojo) y limpia el <em>color buffer</em>, lo cual borra la pantalla cada vez que se dibuja un nuevo frame.</p>
</li>
</ul>
<h2 id="heading-funcion-principal">Función principal</h2>
<pre><code class="lang-cpp"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{

    <span class="hljs-comment">// Initialize GLFW, terminate program if failed</span>
    <span class="hljs-keyword">if</span> (!glfwInit()) {<span class="hljs-built_in">exit</span>(EXIT_FAILURE);}

    <span class="hljs-comment">// Set window properties</span>
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, <span class="hljs-number">4</span>);                  
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, <span class="hljs-number">3</span>);                  
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
</code></pre>
<p>Se inicia GLFW. Si falla, el programa termina.</p>
<p>Estas instrucciones configuran una ventana con OpenGL versión 4.3 en modo <em>core</em>, lo cual exige el uso de funciones modernas (sin funciones obsoletas).</p>
<h2 id="heading-creacion-de-ventana">Creación de ventana</h2>
<pre><code class="lang-cpp">    <span class="hljs-keyword">const</span> GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor());
    GLFWwindow* window = glfwCreateWindow(mode-&gt;width, mode-&gt;height, <span class="hljs-string">" 01_hello_window "</span>, <span class="hljs-literal">nullptr</span>, <span class="hljs-literal">nullptr</span>);
</code></pre>
<p>Aquí se crea una ventana a <strong>pantalla completa</strong> con la resolución nativa del monitor principal. También se imprime información útil del modo de video:</p>
<pre><code class="lang-cpp">    glfwMakeContextCurrent(window);

    <span class="hljs-keyword">if</span>(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"failed to initialize GLAD "</span> &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;
        <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
    }
</code></pre>
<p>Se establece el contexto OpenGL actual y se inicializa GLAD, que permite usar todas las funciones de OpenGL que necesitemos (por ejemplo, <code>glClearColor</code>, <code>glGenBuffers</code>, etc.).</p>
<h2 id="heading-ciclo-principal">Ciclo principal</h2>
<pre><code class="lang-cpp">    <span class="hljs-keyword">while</span> (!glfwWindowShouldClose(window)) {
        display(window);          
        glfwPollEvents();        
        glfwSwapBuffers(window); 
    }
</code></pre>
<ul>
<li><p><code>display()</code> limpia la pantalla con color rojo.</p>
</li>
<li><p><code>glfwPollEvents()</code> procesa eventos del sistema como teclado o mouse.</p>
</li>
<li><p><code>glfwSwapBuffers()</code> intercambia los <em>framebuffers</em> para mostrar la imagen renderizada.</p>
</li>
</ul>
<h1 id="heading-resultado">Resultado</h1>
<p>Al ejecutar este programa, deberías ver una ventana a pantalla completa (o modo ventana si modificas el código) con un fondo completamente rojo. Si ves esto, ¡felicitaciones! Tu pipeline básico de OpenGL está funcionando correctamente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750053577330/f9cd7906-bc73-4138-b1df-f510df782efd.png" alt="A computer window titled &quot;01_hello_window&quot; displays a solid red background." class="image--right mx-auto mr-0" /></p>
<hr />
<p>Este primer acercamiento a OpenGL marca el inicio de un recorrido fascinante por el desarrollo gráfico en tiempo real. Hemos creado una ventana, establecido el contexto de OpenGL y generado un color de fondo simple, sentando así las bases sobre las que construiremos conceptos más avanzados como shaders, buffers y geometría 3D. A medida que avancemos, profundizaremos en el funcionamiento interno de la GPU y exploraremos técnicas modernas que nos permitirán aprovechar al máximo su poder de cómputo. Te invito a continuar con los siguientes tutoriales, donde desglosaremos el pipeline gráfico y daremos vida a nuestras primeras escenas interactivas.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Computer-Graphics-Programming">https://github.com/Nobody-1321/Computer-Graphics-Programming</a></div>
]]></content:encoded></item><item><title><![CDATA[Introducción al Procesamiento de Imágenes y Visión por Computadora]]></title><description><![CDATA[La visión es el más avanzado de nuestros sentidos, gracias a ella, somos capaces de orientarnos en entornos complejos, reconocer la diferencia entre un perro y un león o identificar el rostro de una persona conocida. Sin embargo, aunque todos estos e...]]></description><link>https://codigoenllamas.com/introduccion-al-procesamiento-de-imagenes-y-vision-por-computadora</link><guid isPermaLink="true">https://codigoenllamas.com/introduccion-al-procesamiento-de-imagenes-y-vision-por-computadora</guid><category><![CDATA[Computer Vision]]></category><category><![CDATA[Computer Science]]></category><category><![CDATA[image processing]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Francisco Zavala]]></dc:creator><pubDate>Sun, 08 Jun 2025 17:40:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/QRawWgV6gmo/upload/4d2d654121dd8509b73f3b25a31119e5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>La visión es el más avanzado de nuestros sentidos, gracias a ella, somos capaces de orientarnos en entornos complejos, reconocer la diferencia entre un perro y un león o identificar el rostro de una persona conocida. Sin embargo, aunque todos estos ejemplos involucran al sentido de la vista, la visión no actúa de manera aislada: también intervienen otras capacidades humanas, como la inteligencia, la memoria y el razonamiento. El interés por emular estas funciones humanas ha impulsado el desarrollo de campos como el procesamiento de imágenes y la vision por computadora.</p>
<blockquote>
<p><strong><em>English version of this article.</em></strong> <a target="_blank" href="https://medium.com/imagecraft/fundamentals-of-image-processing-and-computer-vision-6ba4bc8cc4b4"><strong><em>click here.</em></strong></a></p>
</blockquote>
<p>Durante muchos años, ingenieros y científicos han investigado los procesos de la visión con el objetivo de lograr que una computadora “vea”, lo cual no es una tarea sencilla. Mientras los seres humanos percibimos el mundo en tres dimensiones, los sistemas de visión artificial capturan la realidad a través de sensores como cámaras, obteniendo como resultado una representación bidimensional del entorno: una imagen.</p>
<blockquote>
<p><em>“Una imagen puede definirse como una función bidimensional f(x,y), donde x y y son coordenadas espaciales (del plano), y la amplitud de f en cualquier par de coordenadas (x,y) se denomina intensidad o nivel de gris de la imagen en ese punto.”</em><br />— <em>Gonzalez &amp; Woods, Digital Image Processing</em></p>
</blockquote>
<p>La imagen digital está compuesta por un número finito de elementos dispuestos en una matriz de tamaño <em>n × n</em>. Cada uno de estos elementos, conocidos como píxeles, posee una posición específica y un valor que representa la intensidad en ese punto.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749403638778/4d9a3c44-7600-4b8f-9ff4-37924e4574a3.jpeg" alt="Black and white image with three sections. Left: Pixelated face. Middle: Same image with grayscale values shown. Right: Grayscale values only, arranged in a grid." /></p>
<p>Sin embargo, la mera captura de una imagen no garantiza comprensión por sí misma. Para que pueda ser útil en tareas concretas es necesario transformar esa matriz de intensidades en información significativa. Es aquí donde entran en juego el procesamiento de imágenes y la visión por computadora, como etapas fundamentales dentro de un sistema que busca interpretar y actuar sobre el mundo visual.</p>
<p>Aunque no existe una frontera clara entre el procesamiento de imágenes y la visión por computadora —pues ambos comparten principios, métodos y herramientas—, una revisión de sus etapas puede ayudarnos a entender mejor cómo se relacionan y qué papel cumplen dentro de los sistemas de percepción artificial.</p>
<p>El punto de partida suele ser la adquisición y el procesamiento de los datos crudos capturados por sensores, típicamente cámaras. En esta fase inicial, conocida como <strong>procesamiento de imágenes</strong>, se trabaja directamente con la información capturada para mejorar su calidad visual o corregir defectos. El objetivo es transformarla en una versión más útil para análisis posteriores. Técnicas como el realce de contraste, la reducción de ruido, la corrección de distorsiones ópticas o la restauración de imágenes degradadas forman parte de esta etapa, en la que la salida es otra imagen, generalmente más clara o informativa que la original.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749403672524/a7d5aac1-c073-417d-9d81-b87c5c4d8970.png" alt="A collage of four black-and-white photos. The left column shows a woman with long hair wearing a broad-brimmed hat, looking over her shoulder. The right column shows a man in a coat peering through a camera on a tripod in an outdoor setting. The top two images are grainier, while the bottom two are clearer." /></p>
<p>Una vez procesada la imagen, se da paso a una segunda etapa: <strong>el análisis estructural</strong>, que busca extraer información significativa. Aquí se realizan operaciones como la segmentación de la imagen —es decir, dividirla en regiones u objetos de interés— y la extracción de características relevantes como contornos, formas, colores o texturas. Estas características se representan y describen de manera que puedan ser entendidas por un sistema automatizado. Este análisis proporciona una base estructurada sobre la cual se pueden realizar tareas más complejas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749403768801/7c9a67f8-19d5-4d23-985f-97d8c92c3b68.png" alt="Diagram showing image gradients on the left, with a circle highlighting various arrow directions within a grid. On the right, these gradients are transformed into a SIFT (Scale-Invariant Feature Transform) descriptor, represented by star-like patterns within a similar grid." /></p>
<p>Finalmente, entramos en el dominio de la <strong>visión por computadora</strong>, cuyo propósito es emular la capacidad humana de comprender e interpretar el entorno visual. A diferencia del procesamiento, que solo transforma la imagen, y del análisis, que organiza sus componentes, en esta etapa se busca comprender el contenido visual mediante el uso de algoritmos de inteligencia artificial y aprendizaje automático. La visión por computadora permite realizar tareas como la clasificación de objetos, el reconocimiento facial, la detección de movimiento y la toma de decisiones basada en lo que “ve” el sistema. En esencia, se trata de construir sistemas que no solo perciban, sino que también razonen e interactúen con su entorno de forma inteligente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749403814413/96a6a969-8789-4372-829d-f5de7a7ef603.webp" alt="Diagram of a neural network classifying an image of a dog. The image is flattened and processed through multiple layers of nodes connected by lines, resulting in a 95% probability for &quot;Dog&quot; and 5% for &quot;Cat&quot;." class="image--center mx-auto" /></p>
<h2 id="heading-aplicacion-practica-del-reconocimiento-de-imagenes">Aplicación Práctica del Reconocimiento de Imágenes</h2>
<p>Imagina que se te asigna la tarea de registrar la fecha de una gran cantidad de cheques. Hacerlo manualmente sería tedioso e implicaría una inversión enorme de tiempo. pero como eres seguidor de este blog has aprendido técnicas de procesamiento de imágenes y visión por computadora, por lo que decides automatizar la tarea mediante un sistema de reconocimiento de dígitos capaz de interpretar la fecha directamente a partir de la imagen de cada cheque.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749749929172/7127822b-c74a-4700-878c-d23501062f38.jpeg" alt="A Spanish bank cheque issued by Banco Santander with a value of 12.61 Euros, dated 27th December 2010. Various sections of the cheque, including the account number and signature, are obscured for privacy." class="image--center mx-auto" /></p>
<p>El primer paso consiste en adquirir la imagen del cheque mediante una cámara o escáner. Dado que el interés está centrado únicamente en la fecha —específicamente en los dígitos que la componen—, extraemos la región donde esta se encuentra. Este recorte permite enfocarse en el área de interés, lo que reduce la complejidad del proceso de segmentación y facilita que cada dígito pueda ser tratado como una imagen independiente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749749952066/bb445b9d-2b66-4680-892f-4f3b031f3797.png" alt class="image--center mx-auto" /></p>
<p>Antes de poder pasar estas imágenes a un modelo de clasificación, es necesario asegurarse de que cumplan con ciertos requisitos técnicos . Por ejemplo, los dígitos deben tener un tamaño uniforme (comúnmente 28x28 píxeles), un buen contraste entre el fondo y el número, ademas estar libres de ruido visual que pueda interferir con el análisis para la clasificacion, para lograrlo es necesario aplicar preprocesamiento como la binarización, la inversión de colores y operaciones morfológicas, que eliminan imperfecciones y realzan la estructura de los dígitos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749749964817/dd4384cb-3786-4020-a493-a96eb38db4d9.jpeg" alt="Multiple window panels showing pixelated representations of different digits, ranging from 0 to 7. Each window has navigation icons at the top and displays the digit centrally." class="image--center mx-auto" /></p>
<p>Con las imágenes ya limpias y normalizadas, se procede a la etapa más importante del sistema: el reconocimiento automático. La extracción de características se realiza mediante técnicas clásicas, utilizando algoritmos diseñados manualmente, como descriptores de contorno o transformadas matemáticas. No obstante, en este caso se emplea una solución moderna y considerablemente más potente: una red neuronal convolucional (CNN, por sus siglas en inglés).</p>
<p>Las CNN tienen la capacidad de aprender directamente de los datos, sin requerir la especificación explícita de qué buscar en la imagen. Lo hacen mediante una arquitectura en la que se aplican filtros convolucionales que recorren la imagen para detectar patrones locales, como bordes o líneas. Estas operaciones permiten que la red capture progresivamente, a través de distintas capas, características cada vez más complejas: desde trazos básicos hasta formas específicas asociadas a los números. A diferencia de los enfoques clásicos, este modelo aprende de manera automática qué rasgos son relevantes para distinguir un dígito del 0 al 9.</p>
<p>Una vez completado el entrenamiento del modelo, utilizando un conjunto de datos representativo (como el conocido dataset MNIST, que contiene miles de ejemplos de dígitos manuscritos), el sistema está listo para realizar predicciones. En este punto del proceso, se proporciona al modelo una imagen correspondiente a un dígito previamente segmentado del cheque. La red neuronal analiza sus características visuales y devuelve la clase que considera más probable, es decir, el número que dicho dígito representa</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749750065669/c416bc8b-5897-44f2-8237-414ec57f26bb.jpeg" alt class="image--center mx-auto" /></p>
<p>Al combinar las predicciones de todos los dígitos, el sistema reconstruye la fecha completa, formateándola en una representación estándar como "AAAA-MM-DD". De esta manera, hemos construido un flujo automatizado que, a partir de una imagen, es capaz de identificar con precisión la fecha de emisión de un cheque, replicando una tarea visual humana mediante una cadena integrada de procesamiento de imágenes, análisis estructural y visión por computadora.</p>
<h2 id="heading-principales-areas-de-aplicacion">Principales areas de aplicación</h2>
<p><strong>Inspección industrial.</strong><br />En la industria manufacturera, el procesamiento de imágenes permite automatizar la inspección visual de productos, detectando defectos con una precisión mucho mayor que la inspección humana. Estos sistemas son ampliamente usados en sectores como el automotriz, farmacéutico y de semiconductores, donde se comparan piezas reales con modelos de referencia para identificar imperfecciones, componentes faltantes o errores de ensamblaje.</p>
<p><strong>Análisis de documentos y reconocimiento óptico.</strong><br />Las tecnologías de reconocimiento óptico de caracteres (OCR) han transformado el manejo de documentos. Permiten desde la lectura automática de direcciones postales hasta el escaneo y verificación de billetes y documentos legales. También hacen posible la lectura de matrículas en sistemas de peaje o seguridad vial, así como la decodificación de códigos QR en entornos comerciales.</p>
<p><strong>Transporte inteligente.</strong><br />La visión por computadora ha revolucionado el sector del transporte. Cámaras instaladas en calles y vehículos analizan el flujo vehicular, detectan infracciones, miden la ocupación de carriles y ayudan a regular los semáforos. En los automóviles modernos, permiten funciones como la detección de peatones, el seguimiento de carriles y el aparcamiento autónomo, siendo una piedra angular de los sistemas de conducción asistida.</p>
<p><strong>Seguridad y vigilancia.</strong><br />En el ámbito de la seguridad, estas tecnologías permiten la identificación de personas mediante reconocimiento facial, la detección de objetos peligrosos en aeropuertos, y el monitoreo continuo de espacios públicos o privados. Los sistemas de video vigilancia inteligentes pueden incluso analizar patrones de movimiento o identificar comportamientos anómalos en tiempo real.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749404105476/38e127a8-4b30-41cf-bc22-7ace27804b9d.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>Teledetección.</strong><br />La observación remota del planeta a través de satélites y drones genera una enorme cantidad de datos visuales. El procesamiento de imágenes se utiliza aquí para monitorear la deforestación, estimar la humedad del suelo, localizar depósitos minerales o seguir el cambio climático. Gracias a estas imágenes multiespectrales, es posible estudiar el impacto humano sobre el entorno con gran detalle.</p>
<p><strong>Imágenes científicas y médicas.</strong><br />En medicina y ciencia, las imágenes son una herramienta clave para explorar lo invisible. Desde resonancias magnéticas y tomografías en hospitales hasta imágenes microscópicas en biología, estas técnicas permiten diagnosticar enfermedades, guiar cirugías o estudiar organismos a nivel celular. En muchos casos, los análisis se apoyan en algoritmos de segmentación, registro y clasificación automática de estructuras.</p>
<p><strong>Robótica y sistemas autónomos.</strong><br />Los robots industriales y móviles utilizan visión por computadora para interactuar con su entorno. Pueden identificar piezas, ensamblar componentes, navegar por un espacio o seguir a una persona. En entornos más complejos, como los vehículos autónomos, estas tecnologías son esenciales para mapear el entorno, evitar obstáculos y tomar decisiones en tiempo real.</p>
<hr />
<p>Más allá del análisis estructural y numérico, el procesamiento de imágenes también converge con aspectos estéticos y expresivos. Aunque muchas de sus técnicas fueron concebidas con fines científicos o industriales, hoy también son herramientas clave en aplicaciones creativas. Por ejemplo, el <strong>mejoramiento de imágenes</strong> no solo busca resaltar bordes o eliminar ruido, sino que puede utilizarse para realzar la belleza de un retrato mediante la <strong>detección y optimización de rasgos faciales</strong>. Asimismo, técnicas como la <strong>transferencia de estilo</strong> permiten fusionar el contenido de una imagen con la apariencia visual de una obra artística, logrando resultados visuales que combinan arte e inteligencia artificial.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749403881058/0e5735f9-c90f-4965-b969-b543ba39dd15.png" alt="A set of four images: A) A riverside view of colorful European-style buildings under a clear sky. B) The same scene with a turbulent sea painting style. C) The scene with Vincent van Gogh's &quot;Starry Night&quot; style, featuring swirling blues and yellows. D) The scene with Edvard Munch's &quot;The Scream&quot; style, showcasing dramatic reds and flowing lines." /></p>
<p>Además, estas capacidades visuales son fundamentales en entornos inmersivos como la <strong>realidad virtual</strong>, donde es necesario reconstruir y mejorar escenas visuales en tiempo real para generar experiencias visuales más envolventes y realistas. Este cruce entre visión computacional y creatividad demuestra que, lejos de ser un campo rígido, el procesamiento de imágenes también abre caminos hacia lo subjetivo, lo artístico y lo sensorial.</p>
<p>Procesar y comprender imágenes va mucho más allá de una simple tarea técnica; es una forma de acercarnos a cómo vemos y entendemos el mundo. Desde mejorar una imagen capturada por una cámara hasta permitir que una máquina reconozca patrones visuales o incluso genere arte, este campo une precisión matemática con creatividad. Sus aplicaciones abarcan desde la medicina hasta la realidad virtual, pasando por expresiones artísticas donde la estética y la inteligencia artificial se combinan. A medida que la tecnología avanza, también lo hacen nuestras posibilidades de crear sistemas que no solo procesen imágenes, sino que también las interpreten, las embellezcan y, en cierto sentido, las comprendan.</p>
<blockquote>
<p>Gonzalez, R. C., &amp; Woods, R. E. (2008). <em>Digital image processing</em> (4th ed.). Pearson.</p>
<p>Birchfield, S. (2016). <em>Image processing and analysis</em>. Cengage Learning.</p>
</blockquote>
<p>Puedes encontrar el código fuente en el siguiente repositorio.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/Nobody-1321/Learn-Image-Processing">https://github.com/Nobody-1321/Learn-Image-Processing</a></div>
]]></content:encoded></item></channel></rss>