Reconocimiento de fuentes de luz en mapas ambientales.

imagen

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:

  1. Disminución de la resolución de la imagen original, por ejemplo, a 1024.
  2. Convierta la imagen en brillo (luminancia), si es necesario, con imagen borrosa.
  3. Aplicación del método cuasi-Monte Carlo.
  4. Transformación de coordenadas esféricas a coordenadas equidistantes.
  5. Filtrado de muestras en función del brillo de un vecino.
  6. Ordenar muestras en función de su brillo.
  7. Filtrado de muestras basado en la métrica euclidiana.
  8. Fusionar muestras con el algoritmo de Bresenham.
  9. 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 # / 0x100000000 def hammersleySequence(i, N): return (float(i) / float(N), vdcSequence(i)) 

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 # map to -1,1 sinTheta = math.sqrt(1.0 - cosTheta * cosTheta); return (math.cos(phi) * sinTheta, math.sin(phi) * sinTheta, cosTheta) 

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 = [] # apply hammersley sampling for i in range(0, samples): xi = hammersleySequence(i, samples) xyz = sphereSample(xi[0], xi[1]) # to cartesian imagePos = sphereToEquirectangular(xyz) luminance = lum[imagePos[0] * width, imagePos[1] * height] 

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 = [] # apply hammersley sampling for i in range(0, samples): xi = hammersleySequence(i, samples) xyz = sphereSample(xi[0], xi[1]) # to cartesian imagePos = sphereToEquirectangular(xyz) luminance = lum[imagePos [0] * width, imagePos [1] * height] sample = Sample(luminance, imagePos , xyz) luminanceThreshold = 0.8 #do a neighbour search for the maximum luminance nLum = computeNeighborLuminance(lum, width, height, sample.imagePos, localSize) if nLum > luminanceThreshold: samplesList.append(sample) samplesList = sorted(samplesList, key=lambda obj: obj.luminance, reverse=True) 

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)) # filter based euclidian distance filteredCount = len(samplesList) localIndices = np.empty(filteredCount); localIndices.fill(-1) for i in range(0, filteredCount): cpos = samplesList[i].pos if localIndices[i] == -1: localIndices[i] = i for j in range(0, filteredCount): if i != j and localIndices[j] == -1 and distance2d(cpos, samplesList[j].pos) < euclideanThreshold: localIndices[j] = i 

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:

  # apply bresenham check and compute position of the light clusters lights = [] finalIndices = np.empty(filteredCount); finalIndices.fill(-1) for i in localIndices: sample = samplesList[i] startPos = sample.pos if finalIndices[i] == -1: finalIndices[i] = i light = Light() light.originalPos = np.array(sample.pos) # position of the local maxima light.worldPos = np.array(sample.worldPos) light.pos = np.array(sample.pos) light.luminance = sample.luminance for j in localIndices: if i != j and finalIndices[j] == -1: endPos = samplesList[j].pos if bresenhamCheck(lum, width, height, startPos[0], startPos[1], endPos[0], endPos[1]): finalIndices[j] = i # compute final position of the light source sampleWeight = samplesList[j].luminance / sample.luminance light.pos = light.pos + np.array(endPos) * sampleWeight light.pos = light.pos / (1.0 + sampleWeight) imagePos = light.pos * np.array([1.0 / width, 1.0 / height) light.worldPos = equirectangularToSphere(imagePos) lights.append(light) 

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): # delta may go below zero delta = int (dY - (dX >> 1)) while (x0 != x1): # reduce delta, while taking into account the corner case of delta == 0 if ((delta > 0) or (delta == 0 and (stepX > 0))): delta -= dX y0 += stepY delta += dY x0 += stepX sumLum = sumLum + min(lum[x0][y0], 1.25) c = c + 1 if(abs(sumLum / c - prevLum) > luminanceThreshold and (sumLum / c) < 1.0): return 0 else: # delta may go below zero delta = int(dX - (dY >> 1)) while (y0 != y1): # reduce delta, while taking into account the corner case of delta == 0 if ((delta > 0) or (delta == 0 and (stepY > 0))): delta -= dY x0 += stepX delta += dX y0 += stepY sumLum = sumLum + min(lum[x0][y0], 1.25) c = c + 1 if(abs(sumLum / c - prevLum) > luminanceThreshold and (sumLum / c) < 1.0): return 0 return 1 

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:




  1. Detección de fuentes de luz en fotografías digitales por Maciej Laskowski
  2. Puntos Hammersley en el hemisferio por Holger Dammertz
  3. Proyección Equirectangular por Paul Reed
  4. Muestreo con Hammersley y Halton Points por Tien-Tsin Wong

Source: https://habr.com/ru/post/458598/


All Articles