Compartir a través de


Introducción a la representación de volúmenes

Para conocer los volúmenes médicos de resonancia magnética o ingeniería, consulte Representación de volúmenes en Wikipedia. Estas "imágenes volumétricas" contienen información enriquecida con opacidad y color en todo el volumen que no se puede expresar fácilmente como superficies como mallas poligonales.

Soluciones clave para mejorar el rendimiento

  1. BAD: Enfoque ingenuo: Mostrar todo el volumen, generalmente se ejecuta demasiado lentamente
  2. BUENO: Plano de corte: Mostrar solo un segmento del volumen
  3. BUENO: Corte de sub-volumen: Mostrar solo unas pocas capas del volumen
  4. BUENO: Reduzca la resolución de la representación de volumen (vea "Representación de escena de resolución mixta")

Solo hay una cierta cantidad de información que se puede transferir desde la aplicación a la pantalla en cualquier fotograma determinado, que es el ancho de banda de memoria total. Además, cualquier procesamiento (o "sombreado") necesario para transformar esos datos para la presentación requiere tiempo. Las principales consideraciones al realizar la representación de volúmenes son las siguientes:

  • Screen-Width * Screen-Height * Screen-Count * Volume-Layers-On-That-Pixel = Total-Volume-Samples-Per-Frame
  • 1028 * 720 * 2 * 256 = 378961920 (100%) (volumen de res completo: demasiadas muestras)
  • 1028 * 720 * 2 * 1 = 1480320 (0,3% de lleno) (segmento fino: 1 muestra por píxel, se ejecuta sin problemas)
  • 1028 * 720 * 2 * 10 = 14803200 (3,9 % del total) (segmento de subvolumen: 10 muestras por píxel, se ejecuta con bastante suavidad, parece 3d)
  • 200 * 200 * 2 * 256 = 20480000 (5 % de lleno) (volumen res inferior: menos píxeles, volumen completo, se ve 3d pero un poco borrosa)

Representación de texturas 3D

En la CPU:

public struct Int3 { public int X, Y, Z; /* ... */ }
 public class VolumeHeader  {
   public readonly Int3 Size;
   public VolumeHeader(Int3 size) { this.Size = size;  }
   public int CubicToLinearIndex(Int3 index) {
     return index.X + (index.Y * (Size.X)) + (index.Z * (Size.X * Size.Y));
   }
   public Int3 LinearToCubicIndex(int linearIndex)
   {
     return new Int3((linearIndex / 1) % Size.X,
       (linearIndex / Size.X) % Size.Y,
       (linearIndex / (Size.X * Size.Y)) % Size.Z);
   }
   /* ... */
 }
 public class VolumeBuffer<T> {
   public readonly VolumeHeader Header;
   public readonly T[] DataArray;
   public T GetVoxel(Int3 pos)        {
     return this.DataArray[this.Header.CubicToLinearIndex(pos)];
   }
   public void SetVoxel(Int3 pos, T val)        {
     this.DataArray[this.Header.CubicToLinearIndex(pos)] = val;
   }
   public T this[Int3 pos] {
     get { return this.GetVoxel(pos); }
     set { this.SetVoxel(pos, value); }
   }
   /* ... */
 }

En la GPU:

float3 _VolBufferSize;
 int3 UnitVolumeToIntVolume(float3 coord) {
   return (int3)( coord * _VolBufferSize.xyz );
 }
 int IntVolumeToLinearIndex(int3 coord, int3 size) {
   return coord.x + ( coord.y * size.x ) + ( coord.z * ( size.x * size.y ) );
 }
 uniform StructuredBuffer<float> _VolBuffer;
 float SampleVol(float3 coord3 ) {
   int3 intIndex3 = UnitVolumeToIntVolume( coord3 );
   int index1D = IntVolumeToLinearIndex( intIndex3, _VolBufferSize.xyz);
   return __VolBuffer[index1D];
 }

Sombreado y degradados

Cómo sombrear un volumen, como la RMN, para una visualización útil. El método principal consiste en tener una "ventana de intensidad" (un mínimo y un máximo) en la que quiera ver intensidades y simplemente escalar en ese espacio para ver la intensidad en blanco y negro. A continuación, se puede aplicar una "rampa de color" a los valores dentro de ese intervalo y almacenarse como textura, de modo que diferentes partes del espectro de intensidad se puedan sombrear con colores diferentes:

float4 ShadeVol( float intensity ) {
   float unitIntensity = saturate( intensity - IntensityMin / ( IntensityMax - IntensityMin ) );
   // Simple two point black and white intensity:
   color.rgba = unitIntensity;
   // Color ramp method:
   color.rgba = tex2d( ColorRampTexture, float2( unitIntensity, 0 ) );

En muchas de nuestras aplicaciones, almacenamos en nuestro volumen un valor de intensidad sin procesar y un "índice de segmentación" (para segmentar diferentes partes como la piel y el hueso; estos segmentos son creados por expertos en herramientas dedicadas). Esto se puede combinar con el enfoque anterior para colocar un color diferente, o incluso una rampa de color diferente para cada índice de segmento:

// Change color to match segment index (fade each segment towards black):
 color.rgb = SegmentColors[ segment_index ] * color.a; // brighter alpha gives brighter color

Segmentación de volumen en un sombreador

Un gran primer paso consiste en crear un "plano de segmentación" que pueda moverse por el volumen, "segmentarlo" y cómo se valora el examen en cada punto. Esto supone que hay un cubo "VolumeSpace", que representa dónde está el volumen en el espacio mundial, que se puede usar como referencia para colocar los puntos:

// In the vertex shader:
 float4 worldPos = mul(_Object2World, float4(input.vertex.xyz, 1));
 float4 volSpace = mul(_WorldToVolume, float4(worldPos, 1));
// In the pixel shader:
 float4 color = ShadeVol( SampleVol( volSpace ) );

Seguimiento de volúmenes en sombreadores

Cómo usar la GPU para realizar el seguimiento de subvolumen (recorre unos cuantos elementos voxels profundos y, a continuación, capas en los datos de atrás a delante):

float4 AlphaBlend(float4 dst, float4 src) {
   float4 res = (src * src.a) + (dst - dst * src.a);
   res.a = src.a + (dst.a - dst.a*src.a);
   return res;
 }
 float4 volTraceSubVolume(float3 objPosStart, float3 cameraPosVolSpace) {
   float maxDepth = 0.15; // depth in volume space, customize!!!
   float numLoops = 10; // can be 400 on nice PC
   float4 curColor = float4(0, 0, 0, 0);
   // Figure out front and back volume coords to walk through:
   float3 frontCoord = objPosStart;
   float3 backCoord = frontPos + (normalize(cameraPosVolSpace - objPosStart) * maxDepth);
   float3 stepCoord = (frontCoord - backCoord) / numLoops;
   float3 curCoord = backCoord;
   // Add per-pixel random offset, avoids layer aliasing:
   curCoord += stepCoord * RandomFromPositionFast(objPosStart);
   // Walk from back to front (to make front appear in-front of back):
   for (float i = 0; i < numLoops; i++) {
     float intensity = SampleVol(curCoord);
     float4 shaded = ShadeVol(intensity);
     curColor = AlphaBlend(curColor, shaded);
     curCoord += stepCoord;
   }
   return curColor;
 }
// In the vertex shader:
 float4 worldPos = mul(_Object2World, float4(input.vertex.xyz, 1));
 float4 volSpace = mul(_WorldToVolume, float4(worldPos.xyz, 1));
 float4 cameraInVolSpace = mul(_WorldToVolume, float4(_WorldSpaceCameraPos.xyz, 1));
// In the pixel shader:
 float4 color = volTraceSubVolume( volSpace, cameraInVolSpace );

Representación de volumen completo

Al modificar el código subvolumen anterior, se obtiene lo siguiente:

float4 volTraceSubVolume(float3 objPosStart, float3 cameraPosVolSpace) {
   float maxDepth = 1.73; // sqrt(3), max distance from point on cube to any other point on cube
   int maxSamples = 400; // just in case, keep this value within bounds
   // not shown: trim front and back positions to both be within the cube
   int distanceInVoxels = length(UnitVolumeToIntVolume(frontPos - backPos)); // measure distance in voxels
   int numLoops = min( distanceInVoxels, maxSamples ); // put a min on the voxels to sample

Representación de escenas de resolución mixta

Cómo representar una parte de la escena con una resolución baja y volver a colocarla en su lugar:

  1. Configure dos cámaras fuera de pantalla, una para seguir cada ojo que actualice cada fotograma.
  2. Configure dos destinos de representación de baja resolución (es decir, 200x200 cada uno) en los que se representan las cámaras.
  3. Configurar un quad que se mueva delante del usuario

Cada fotograma:

  1. Dibujar los destinos de representación para cada ojo a baja resolución (datos de volumen, sombreadores caros, etc.)
  2. Dibuje la escena normalmente como resolución completa (mallas, interfaz de usuario, etc.)
  3. Dibuje un cuadrángulo delante del usuario, sobre la escena y proyecte las representaciones de baja res en ese
  4. Resultado: combinación visual de elementos de resolución completa con datos de volumen de baja resolución pero alta densidad

Consulta también