Câmara localizável

Antes de começar aqui, recomendamos que consulte o nosso artigo descrição geral da câmara localizável que contém informações de descrição geral e uma tabela com detalhes da câmara do HoloLens 1 e 2.

Utilizar MediaFrameReference

Estas instruções aplicam-se se estiver a utilizar a classe MediaFrameReference para ler fotogramas de imagens da câmara.

Cada frame de imagem (quer seja fotografia ou vídeo) inclui um SpatialCoordinateSystem rooting na câmara no momento da captura, que pode ser acedido com a propriedade CoordinateSystem da sua MediaFrameReference. Cada moldura contém uma descrição do modelo de lente da câmara, que pode ser encontrada na propriedade CameraIntrinsics . Em conjunto, estas transformações definem para cada pixel um raio em espaço 3D que representa o caminho seguido pelos fotões que produziram o pixel. Estes raios podem estar relacionados com outros conteúdos na aplicação ao obter a transformação do sistema de coordenadas da moldura para outro sistema de coordenadas (por exemplo, a partir de um quadro estacionário de referência).

Cada frame de imagem fornece o seguinte:

O exemplo holographicFaceTracking mostra a forma bastante simples de consultar a transformação entre o sistema de coordenadas da câmara e os seus próprios sistemas de coordenadas de aplicação.

Utilizar o Media Foundation

Se estiver a utilizar o Media Foundation diretamente para ler fotogramas de imagens da câmara, pode utilizar o atributo MFSampleExtension_CameraExtrinsics de cada frame e MFSampleExtension_PinholeCameraIntrinsics atributo para localizar molduras de câmara relativamente a outros sistemas de coordenadas da sua aplicação, conforme mostrado neste código de exemplo:

#include <winrt/windows.perception.spatial.preview.h>
#include <mfapi.h>
#include <mfidl.h>
 
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Numerics;
using namespace winrt::Windows::Perception;
using namespace winrt::Windows::Perception::Spatial;
using namespace winrt::Windows::Perception::Spatial::Preview;
 
class CameraFrameLocator
{
public:
    struct CameraFrameLocation
    {
        SpatialCoordinateSystem CoordinateSystem;
        float4x4 CameraViewToCoordinateSystemTransform;
        MFPinholeCameraIntrinsics Intrinsics;
    };
 
    std::optional<CameraFrameLocation> TryLocateCameraFrame(IMFSample* pSample)
    {
        MFCameraExtrinsics cameraExtrinsics;
        MFPinholeCameraIntrinsics cameraIntrinsics;
        UINT32 sizeCameraExtrinsics = 0;
        UINT32 sizeCameraIntrinsics = 0;
        UINT64 sampleTimeHns = 0;
 
        // query sample for calibration and validate
        if (FAILED(pSample->GetUINT64(MFSampleExtension_DeviceTimestamp, &sampleTimeHns)) ||
            FAILED(pSample->GetBlob(MFSampleExtension_CameraExtrinsics, (UINT8*)& cameraExtrinsics, sizeof(cameraExtrinsics), &sizeCameraExtrinsics)) ||
            FAILED(pSample->GetBlob(MFSampleExtension_PinholeCameraIntrinsics, (UINT8*)& cameraIntrinsics, sizeof(cameraIntrinsics), &sizeCameraIntrinsics)) ||
            (sizeCameraExtrinsics != sizeof(cameraExtrinsics)) ||
            (sizeCameraIntrinsics != sizeof(cameraIntrinsics)) ||
            (cameraExtrinsics.TransformCount == 0))
        {
            return std::nullopt;
        }
 
        // compute extrinsic transform
        const auto& calibratedTransform = cameraExtrinsics.CalibratedTransforms[0];
        const GUID& dynamicNodeId = calibratedTransform.CalibrationId;
        const float4x4 cameraToDynamicNode =
            make_float4x4_from_quaternion(quaternion{ calibratedTransform.Orientation.x, calibratedTransform.Orientation.y, calibratedTransform.Orientation.z, calibratedTransform.Orientation.w }) *
            make_float4x4_translation(calibratedTransform.Position.x, calibratedTransform.Position.y, calibratedTransform.Position.z);
 
        // update locator cache for dynamic node
        if (dynamicNodeId != m_currentDynamicNodeId || !m_locator)
        {
            m_locator = SpatialGraphInteropPreview::CreateLocatorForNode(dynamicNodeId);
            if (!m_locator)
            {
                return std::nullopt;
            }
 
            m_frameOfReference = m_locator.CreateAttachedFrameOfReferenceAtCurrentHeading();
            m_currentDynamicNodeId = dynamicNodeId;
        }
 
        // locate dynamic node
        auto timestamp = PerceptionTimestampHelper::FromSystemRelativeTargetTime(TimeSpan{ sampleTimeHns });
        auto coordinateSystem = m_frameOfReference.GetStationaryCoordinateSystemAtTimestamp(timestamp);
        auto location = m_locator.TryLocateAtTimestamp(timestamp, coordinateSystem);
        if (!location)
        {
            return std::nullopt;
        }
 
        const float4x4 dynamicNodeToCoordinateSystem = make_float4x4_from_quaternion(location.Orientation()) * make_float4x4_translation(location.Position());
 
        return CameraFrameLocation{ coordinateSystem, cameraToDynamicNode * dynamicNodeToCoordinateSystem, cameraIntrinsics };
    }

private:
    GUID m_currentDynamicNodeId{ GUID_NULL };
    SpatialLocator m_locator{ nullptr };
    SpatialLocatorAttachedFrameOfReference m_frameOfReference{ nullptr };
};

Cenários de Utilização da Câmara Localizável

Mostrar uma fotografia ou vídeo no mundo onde foi capturado

Os fotogramas da Câmara do Dispositivo vêm com uma transformação "Câmara para Mundo", que pode ser utilizada para mostrar exatamente onde estava o dispositivo quando a imagem foi tirada. Por exemplo, pode posicionar um pequeno ícone holográfico nesta localização (CameraToWorld.MultiplyPoint(Vector3.zero)) e até desenhar uma pequena seta na direção em que a câmara estava virada (CameraToWorld.MultiplyVector(Vector3.forward)).

Taxa de Fotogramas

Manter uma taxa de fotogramas de aplicações interativa é fundamental, especialmente ao lidar com algoritmos de reconhecimento de imagens de execução prolongada. Por este motivo, utilizamos normalmente o seguinte padrão:

  1. Thread Principal: gere o objeto da câmara
  2. Thread Principal: pede novos frames (assíncrono)
  3. Thread Principal: transmitir novos frames para controlar o thread
  4. Thread de Controlo: processa a imagem para recolher pontos-chave
  5. Thread Principal: move o modelo virtual para corresponder aos pontos-chave encontrados
  6. Thread Principal: repetir a partir do passo 2

Alguns sistemas de marcadores de imagem fornecem apenas uma única localização de píxeis (outros fornecem a transformação completa, caso em que esta secção não será necessária), o que equivale a um raio de possíveis localizações. Para chegar a uma única terceira localização, podemos tirar partido de vários raios e encontrar o resultado final pela respetiva interseção aproximada. Para tal, terá de:

  1. Obter um ciclo a recolher várias imagens da câmara
  2. Localizar os pontos de funcionalidades associados e os seus raios mundiais
  3. Quando tem um dicionário de funcionalidades, cada um com vários raios mundiais, pode utilizar o seguinte código para resolver a interseção desses raios:
public static Vector3 ClosestPointBetweenRays(
   Vector3 point1, Vector3 normalizedDirection1,
   Vector3 point2, Vector3 normalizedDirection2) {
   float directionProjection = Vector3.Dot(normalizedDirection1, normalizedDirection2);
   if (directionProjection == 1) {
     return point1; // parallel lines
   }
   float projection1 = Vector3.Dot(point2 - point1, normalizedDirection1);
   float projection2 = Vector3.Dot(point2 - point1, normalizedDirection2);
   float distanceAlongLine1 = (projection1 - directionProjection * projection2) / (1 - directionProjection * directionProjection);
   float distanceAlongLine2 = (projection2 - directionProjection * projection1) / (directionProjection * directionProjection - 1);
   Vector3 pointOnLine1 = point1 + distanceAlongLine1 * normalizedDirection1;
   Vector3 pointOnLine2 = point2 + distanceAlongLine2 * normalizedDirection2;
   return Vector3.Lerp(pointOnLine2, pointOnLine1, 0.5f);
 }

Posicionar uma cena modelada

Tendo em conta duas ou mais localizações de etiquetas controladas, pode posicionar uma cena modelada para se ajustar ao cenário atual do utilizador. Se não conseguir assumir a gravidade, precisará de três localizações de etiquetas. Em muitos casos, utilizamos um esquema de cores em que as esferas brancas representam localizações de etiquetas registadas em tempo real e as esferas azuis representam localizações de etiquetas modeladas. Isto permite que o utilizador avalie visualmente a qualidade do alinhamento. Assumimos a seguinte configuração em todas as nossas aplicações:

  • Duas ou mais localizações de etiquetas modeladas
  • Um "espaço de calibragem", que na cena é o elemento principal das etiquetas
  • Identificador de funcionalidades da câmara
  • Comportamento, que move o espaço de calibragem para alinhar as etiquetas modeladas com as etiquetas em tempo real (temos o cuidado de mover o espaço principal e não os próprios marcadores modelados, porque outras ligações são posições relativas às mesmas).
// In the two tags case:
 Vector3 idealDelta = (realTags[1].EstimatedWorldPos - realTags[0].EstimatedWorldPos);
 Vector3 curDelta = (modelledTags[1].transform.position - modelledTags[0].transform.position);
 if (IsAssumeGravity) {
   idealDelta.y = 0;
   curDelta.y = 0;
 }
 Quaternion deltaRot = Quaternion.FromToRotation(curDelta, idealDelta);
 trans.rotation = Quaternion.LookRotation(deltaRot * trans.forward, trans.up);
 trans.position += realTags[0].EstimatedWorldPos - modelledTags[0].transform.position;

Controlar ou Identificar objetos/rostos do mundo real com LEDs ou outras bibliotecas de reconhecedores ou objetos do mundo real em movimento

Exemplos:

  • Robôs industriais com LEDs (ou códigos QR para objetos em movimento mais lentos)
  • Identificar e reconhecer objetos na sala
  • Identificar e reconhecer pessoas na sala, por exemplo, colocar cartões de contacto holográficos sobre rostos

Ver também