Este artículo presenta una implementación de Python del algoritmo para reconocer fuentes de luz en mapas ambientales (LDR o HDR) usando una proyección equirrectangular. Sin embargo, después de realizar cambios menores, también se puede usar con imágenes de fondo simples o mapas cúbicos. Ejemplos de la posible aplicación del algoritmo: programas de trazado de rayos en los que es necesario reconocer fuentes de luz primarias para emitir rayos desde ellos; en renderizadores rasterizados, se puede usar para proyectar sombras usando un mapa de entorno; Además, el algoritmo también se puede utilizar en programas de eliminación de reflectores, como AR.
El algoritmo consta de los siguientes pasos:
- Disminución de la resolución de la imagen original, por ejemplo, a 1024.
- Convierta la imagen en brillo (luminancia), si es necesario, con imagen borrosa.
- Aplicación del método cuasi-Monte Carlo.
- Transformación de coordenadas esféricas a coordenadas equidistantes.
- Filtrado de muestras en función del brillo de un vecino.
- Ordenar muestras en función de su brillo.
- Filtrado de muestras basado en la métrica euclidiana.
- Fusionar muestras con el algoritmo de Bresenham.
- Cálculo de la posición del grupo de iluminación en función de su brillo.
Existen muchos algoritmos para reducir la resolución de las imágenes. El filtrado bilineal es el más rápido o fácil de implementar y, además, es el más adecuado en la mayoría de los casos. Para convertir el brillo en imágenes LDR y HDR, puede usar la fórmula estándar:
lum = img[:, :, 0] * 0.2126 + img[:, :, 1] * 0.7152 + img[:, :, 2] * 0.0722
Además, puede aplicar un ligero desenfoque a la imagen de brillo, por ejemplo, 1-2 píxeles para una imagen con una resolución de 1024, para eliminar todos los detalles de alta frecuencia (en particular, causados por una disminución de la resolución).
Proyección Equidistante
La proyección más común en los mapas del entorno es la proyección equidistante
3 . Mi algoritmo puede funcionar con otras proyecciones, por ejemplo, con mapas panorámicos y cúbicos, sin embargo, en el artículo consideraremos solo una proyección igualmente espaciada. Primero necesitas normalizar las coordenadas de la imagen:
pos[0] = x / width pos[1] = y / height
Luego necesitamos convertir desde y hacia coordenadas cartesianas usando coordenadas esféricas, es decir θ y φ, donde θ = x * 2π, y φ = y * π.
def sphereToEquirectangular(pos): invAngles = (0.1591, 0.3183) xy = (math.atan2(pos[1], pos[0]), math.asin(pos[2])) xy = (xy[0] * invAngles[0], xy[1] * invAngles[1]) return (xy[0] + 0.5, xy[1] + 0.5) def equirectangularToSphere(pos): angles = (1 / 0.1591, 1 / 0.3183) thetaPhi = (pos[0] - 0.5, pos[1] - 0.5) thetaPhi = (thetaPhi[0] * angles[0], thetaPhi[1] * angles[1]) length = math.cos(thetaPhi[1]) return (math.cos(thetaPhi[0]) * length, math.sin(thetaPhi[0]) * length, math.sin(thetaPhi[1]))
Muestreo Hammersley
El siguiente paso será aplicar el método cuasi-Monte Carlo, por ejemplo, el muestreo Hammersley
2 :
Puede usar otros métodos de muestreo, como Holton
4 , pero Hammersley es más rápido y proporciona una buena distribución de muestras en toda la esfera. Holton sería una buena opción para muestras de avión si se usa una imagen simple en lugar del mapa del entorno. Un requisito obligatorio para el muestreo de Hammersley es la inversión de las raíces (fila) de van der Corpute; para más detalles, consulte los enlaces
2 . Aquí está su implementación rápida:
def vdcSequence(bits): bits = (bits << 16) | (bits >> 16) bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1) bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2) bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4) bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8) return float(bits) * 2.3283064365386963e-10
Luego usamos superposición uniforme en la esfera:
def sphereSample(u, v): PI = 3.14159265358979 phi = v * 2.0 * PI cosTheta = 2.0 * u - 1.0
Para muestrear Hammersley, utilizamos un número fijo de muestras, dependiendo de la resolución de la imagen, y las convertimos de coordenadas esféricas a cartesianas, y luego a equidistantes:
samplesMultiplier = 0.006 samples = int(samplesMultiplier * width * height) samplesList = []
Esto nos dará una buena distribución de muestras que serán verificadas por la presencia de fuentes de luz:
Filtrando fuentes de luz
En el primer paso del filtrado, ignoramos todas las muestras que no exceden el umbral de brillo (para tarjetas HDR, puede ser más alto) y luego clasificamos todas las muestras por su brillo:
localSize = int(float(12) * (width / 1024.0)) + 1 samplesList = []
La próxima pasada se filtrará según la métrica euclidiana y la distancia umbral entre píxeles (dependiendo de la resolución de la imagen): esta es una estructura de datos espaciales que se puede utilizar para eliminar la complejidad O (N
2 ):
euclideanThreshold = int(float(euclideanThresholdPixel) * (width / 2048.0))
Las muestras resultantes pasan por la etapa de fusión para reducir aún más el número de fuentes de luz:
Fusionar fuentes de luz
En la última etapa, se realiza la fusión de muestras que pertenecen al mismo grupo de luces. Para hacer esto, podemos usar el algoritmo de Bresenham y comenzar con las muestras con el brillo más alto, porque ya están ordenadas. Cuando encontramos una fuente de luz que satisface la prueba de Bresenham, usamos su posición para cambiar la posición de la fuente en función del peso de la ejecución:
La función Bresenham busca una línea continua que tenga el mismo brillo. Si el delta en el píxel actual excede un cierto umbral, la verificación falla:
def bresenhamCheck(lum, imageSize, x0, y0, x1, y1): dX = int(x1 - x0) stepX = int((dX > 0) - (dX < 0)) dX = abs(dX) << 1 dY = int(y1 - y0) stepY = int((dY > 0) - (dY < 0)) dY = abs(dY) << 1 luminanceThreshold = 0.15 prevLum = lum[x0][y0] sumLum = 0.0 c = 0 if (dX >= dY):
Cabe señalar que, si es necesario, se pueden realizar mejoras en la prueba de Bresenham, lo que conducirá a una mejor fusión de las muestras, por ejemplo, puede tener en cuenta la transferencia horizontal de las fuentes de luz ubicadas en los bordes de la imagen. Además, la función se puede ampliar fácilmente para aproximar el área de las fuentes de luz. Otra mejora: puede agregar un umbral de distancia para no combinar muestras que estén demasiado lejos. Resultados finales
El azul denota los máximos locales de los grupos de luces, el azul denota las posiciones finales de las fuentes de luz y el rojo denota muestras que son parte del mismo grupo de luces y están conectadas por líneas.
Otros ejemplos de resultados:
- Detección de fuentes de luz en fotografías digitales por Maciej Laskowski
- Puntos Hammersley en el hemisferio por Holger Dammertz
- Proyección Equirectangular por Paul Reed
- Muestreo con Hammersley y Halton Points por Tien-Tsin Wong