Junio de 2019

Volumen 34, número 6

[ASP.NET Core 3.0]

Seguridad biométrica con inteligencia artificial en ASP.NET Core

Por Stefano Tempesta

En este artículo, que consta de dos partes, se presenta el modelo de autorización basado en directivas de ASP.NET Core 3, que pretende desacoplar la lógica de autorización de los roles de usuario subyacentes. Presenta un ejemplo específico de este proceso de autorización basado en información biométrica, como el reconocimiento facial o de voz. En este caso, el acceso a un edificio está restringido cuando se detecta una intrusión no autorizada. La gravedad de la intrusión se evalúa mediante un servicio de detección de anomalías integrado en Azure Machine Learning.

Acceso al sitio

El contexto es un sitio extremadamente seguro, como una zona militar, un hospital o un centro de datos. El acceso está restringido a las personas autorizadas, con algunas limitaciones. En los pasos siguientes se describe el flujo de seguridad que se exige en el acceso de cada edificio para el registro de personas:

  1. Una persona que solicita acceso a un edificio desliza su pase por el lector de tarjetas de la puerta.
  2. Las cámaras detectan movimiento y capturan la cara y el cuerpo de la persona; esto debería evitar el uso de una foto impresa, por ejemplo, para engañar a la cámara solo con reconocimiento facial.
  3. El lector de tarjetas y las cámaras están registrados como dispositivos de Internet de las cosas (IoT) y transmiten los datos grabados a Azure IoT Hub.
  4. Microsoft Cognitive Services realiza una comparación de la persona en una base de datos de personas autorizadas para acceder al edificio.
  5. Un flujo de autorización relaciona la información biométrica recopilada por los dispositivos IoT con la identidad de la persona del pase.
  6. Se invoca un servicio de Azure Machine Learning para evaluar el nivel de riesgo de la solicitud de acceso y si se trata de una intrusión no autorizada.
  7. La autorización se concede mediante una API web de ASP.NET Core a través de la comprobación de requisitos de directiva específicos que son propiedad del perfil definido en los pasos anteriores.

Si existe una discrepancia entre la identidad de la persona detectada y el pase, se impide inmediatamente el acceso al sitio. En caso contrario, el flujo continúa y comprueba si se ha detectado alguna de las anomalías siguientes:

  • Frecuencia inusual de acceso al edificio.
  • Si la persona ha salido del edificio antes (salida).
  • Número de accesos permitidos al día.
  • Si la persona que está de servicio.
  • Importancia crítica del edificio (es posible que no quiera restringir el acceso a un cantina, pero aplicar una directiva más estricta para el acceso a un centro de datos del servidor).
  • Si la persona trae a alguien o algo más consigo.
  • Casos anteriores de tipologías de accesos similares al mismo edificio.
  • Cambios en el nivel de riesgo medidos en el pasado.
  • Número de intrusiones detectadas en el pasado.

El servicio de detección de anomalías se ejecuta en Azure Machine Learning y devuelve una puntuación, expresada como una probabilidad de que el acceso sea una desviación del valor estándar. La puntuación se expresa en un intervalo entre 0 y 1, donde 0 significa "riesgo no detectado", todo correcto y plena confianza concedida; y 1 significa "alerta roja", bloquear el acceso inmediatamente. El nivel de riesgo de cada edificio determina el umbral que se considera aceptable para permitir el acceso al edificio con cualquier valor mayor que 0.

Autorización en ASP.NET Core

ASP.NET Core proporciona un rol declarativo de autorización simple y un modelo basado en directivas completo. La autorización se expresa en los requisitos y los controladores evalúan las notificaciones de un usuario con respecto a dichos requisitos. Con el fin de autorizar a los usuarios a acceder a un sitio, describiré cómo generar requisitos de directiva personalizados y el controlador de autorización correspondiente. Para obtener más información sobre el modelo de autorización en ASP.NET Core, consulte la documentación de bit.ly/2UYZaJh.

Como hemos visto, un mecanismo de autorización personalizada basado en directivas personalizado consta de requisitos y (normalmente) de un controlador de autorización. Conceder acceso a un edificio consiste en invocar una API que desbloquee la puerta de entrada. Los dispositivos IoT transmiten información biométrica a Azure IoT Hub, lo que a su vez desencadena el flujo de trabajo de verificación mediante la publicación del id. del sitio, un identificador único del sitio. El método POST de la API web simplemente devuelve un código HTTP 200 y un mensaje JSON con el id. de sitio y el nombre de usuario si la autorización se realiza correctamente. En caso contrario, produce el código de error HTTP 401 de acceso no autorizado. Pero vayamos por orden: Comienzo con la clase Startup de la API web, específicamente el método ConfigureServices, que contiene las instrucciones para configurar los servicios necesarios para ejecutar la aplicación de ASP.NET Core. Se agregan las directivas de autorización mediante una llamada al método AddAuthorization en el objeto de servicios. El método AddAuthorization acepta una colección de directivas que la función de API debe poseer cuando se invoca para autorizar su ejecución. Solo necesito una directiva en este caso, que denomino "AuthorizedUser". Sin embargo, esta directiva presenta varios requisitos que se deben cumplir y que reflejan las características biométricas de una persona que quiero verificar: cara, cuerpo y voz. Cada uno de los tres requisitos se representa mediante una clase específica que implementa la interfaz IAuthorizationRequirement, como se muestra en la Figura 1. Al enumerar los requisitos de la directiva AuthorizedUser, también especifico el nivel de confianza necesario para cumplir el requisito. Como he indicado anteriormente, este valor entre 0 y 1, expresa la precisión de la identificación del atributo biométrico correspondiente. Volveré a este punto más adelante al hablar del reconocimiento biométrico con Cognitive Services.

Figura 1 Configuración de los requisitos de autorización en la API web

public void ConfigureServices(IServiceCollection services)
{
  var authorizationRequirements = new List<IAuthorizationRequirement>
  {
    new FaceRecognitionRequirement(confidence: 0.9),
    new BodyRecognitionRequirement(confidence: 0.9),
    new VoiceRecognitionRequirement(confidence: 0.9)
  };
  services
    .AddAuthorization(options =>
    {
      options.AddPolicy("AuthorizedUser", policy => policy.Requirements =
        authorizationRequirements);
    })

La directiva de autorización AuthorizedUser contiene varios requisitos de autorización. Todos ellos deben cumplirse para que la evaluación de la directiva se realice correctamente. En otras palabras, varios requisitos de autorización agregados a una sola directiva de autorización se tratan de forma inclusiva.

Los tres requisitos de directiva que implementé en la solución son todas las clases que implementan la interfaz de IAuthorizationRequirement. Esta interfaz está vacía en realidad; es decir, no dictamina la implementación de ningún método. Para implementar los tres requisitos de manera coherente, especifiqué una propiedad ConfidenceScore pública para capturar el nivel previsto de confianza que la API de reconocimiento debe alcanzar para considerar que el requisito se cumplió correctamente. La clase FaceRecognitionRequirement tiene este aspecto:

public class FaceRecognitionRequirement : IAuthorizationRequirement
{
  public double ConfidenceScore { get; }
  public FaceRecognitionRequirement(double confidence) =>
    ConfidenceScore = confidence;
}

De forma similar, los demás requisitos de reconocimiento del cuerpo y la voz están implementados, respectivamente, en las clases BodyRecognitionRequirement y VoiceRecognitionRequirement.

La autorización para ejecutar una acción de la API web se controla mediante el atributo Authorize. En su forma más simple, la aplicación de AuthorizeAttribute a un controlador o una acción limita el acceso a ese controlador o acción a cualquier usuario autenticado. La API web que controla el acceso a un sitio expone un controlador de acceso único, que contiene solo la acción Post. Esta acción está autorizada si se cumplen todos los requisitos de la directiva "AuthorizedUser" especificada:

[ApiController]
public class AccessController : ControllerBase
{
  [HttpPost]
  [Authorize(Policy = "AuthorizedUser")]
  public IActionResult Post([FromBody] string siteId)
  {
    var response = new
    {
      User = HttpContext.User.Identity.Name,
      SiteId = siteId
    };
    return new JsonResult(response);
  }
}

Cada requisito está administrado por un controlador de autorización, como el de la Figura 2, que es responsable de la evaluación de un requisito de directiva. Puede optar por tener un único controlador para todos los requisitos, o bien controladores independientes para cada requisito. Este último enfoque es más flexible, ya que permite configurar un degradado de los requisitos de autorización, que puede configurar fácilmente en la clase Startup. Los controladores de requisitos de cara, cuerpo y voz extienden la clase abstracta AuthorizationHandler<TRequirement>, donde TRequirement es el requisito que se debe controlar. Dado que quiero evaluar tres requisitos, debo escribir un controlador personalizado que extienda la clase AuthorizationHandler para FaceRecognitionRequirement, BodyRecognitionRequirement y VoiceRecognitionRequirement, respectivamente. En concreto, se cumple el método HandleRequirementAsync, que determina si se cumplió un requisito de autorización. Este método, ya que es asincrónico, no devuelve un valor real, excepto para indicar que se completó la tarea. El control de autorización consiste en marcar un requisito como "correcto" al invocar el método Succeed en el contexto del controlador de autorización. Esto se verifica realmente mediante un objeto "recognizer", que usa internamente la API de Cognitive Services (más información en la sección siguiente). La acción de reconocimiento, que realiza el método Recognize, obtiene el nombre de la persona identificada y devuelve un valor (puntuación) que expresa el nivel de confianza sobre la mayor (valor más cercano a 1) o menor (valor más cercano a 0) precisión de la identificación. En la configuración de la API se especificó un nivel inesperado. Puede ajustar este valor a cualquier umbral que considere adecuado para su solución.

Figura 2 Controlador de autorización personalizada

public class FaceRequirementHandler :
  AuthorizationHandler<FaceRecognitionRequirement>
{
  protected override Task HandleRequirementAsync(
    AuthorizationHandlerContext context,
      FaceRecognitionRequirement requirement)
  {
    string siteId =
      (context.Resource as HttpContext).Request.Query["siteId"];
    IRecognition recognizer = new FaceRecognition();
    if (recognizer.Recognize(siteId, out string name) >=
      requirement.ConfidenceScore)
    {
      context.User.AddIdentity(new ClaimsIdentity(
        new GenericIdentity(name)));
      context.Succeed(requirement);
    }
    return Task.CompletedTask;
  }
}

Además de evaluar el requisito específico, el controlador de autorización también agrega una notificación de identidad para el usuario actual. Cuando se crea una identidad, se le pueden asignar una o varias notificaciones emitidas por una entidad de confianza. Una notificación es un par de nombre-valor que representa el asunto. En este caso, asigno la notificación de identidad al usuario en contexto. Posteriormente, esta notificación se recupera en la acción Publicar del controlador de acceso y se devuelve como parte de la respuesta de la API.

El último paso que se debe llevar a cabo para habilitar este proceso de autorización personalizado es el registro del controlador en la API web. Los controladores se registran en la colección de servicios durante la configuración:

services.AddSingleton<IAuthorizationHandler, FaceRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, BodyRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, VoiceRequirementHandler>();

Este código registra cada controlador de requisito como un singleton con el marco de inserción de dependencias (DI) integrado de ASP.NET Core. Se creará una instancia del controlador cuando se inicie la aplicación y DI insertará la clase registrada en el objeto pertinente.

Identificación de caras

La solución utiliza Azure Cognitive Services para Vision API con la finalidad de identificar la cara y el cuerpo de una persona. Para obtener más información acerca de Cognitive Services y los detalles sobre la API, visite bit.ly/2sxsqry.

Vision API proporciona opciones de detección de atributos de cara y comprobación de caras. La detección de caras hace referencia a la capacidad de detectar caras humanas en una imagen. La API devuelve las coordenadas del rectángulo de la ubicación de la cara en la imagen procesada y, de manera opcional, puede extraer una serie de atributos relacionados con la cara, como los de postura de la cabeza, sexo, edad, emoción, vello facial y gafas. La comprobación de caras, en cambio, realiza una autenticación de una cara detectada con la cara previamente guardada de una persona. En la práctica, evalúa si dos caras pertenecen a la misma persona. Esta es la API específica que utilizo en este proyecto de seguridad. Para empezar, agregue el paquete NuGet siguiente a su solución de Visual Studio: Microsoft.Azure.Cognitive­Services.Vision.Face 2.2.0-preview

El paquete administrado de .NET está en versión preliminar, por lo que debe asegurarse de comprobar la opción "Incluir versión preliminar" al explorar NuGet, como se muestra en la Figura 3.

Paquete NuGet para Face API
Figura 3 Paquete NuGet para Face API

El paquete de .NET facilita el reconocimiento y la detección de caras. En términos generales, el reconocimiento facial describe el trabajo de comparar dos caras diferentes para determinar si son similares o si pertenecen a la misma persona. Las operaciones de reconocimiento usan principalmente las estructuras de datos que se enumeran en Figura 4.

Figura 4 Estructuras de datos para Face API

Nombre Descripción
DetectedFace Esta es una representación de una cara recuperada por la operación de detección de caras. Su identificador expira 24 horas después de haberse creado.
PersistedFace Cuando se agregan objetos DetectedFace a un grupo (por ejemplo, FaceList o Person), se convierten en objetos PersistedFace, que se pueden recuperar en cualquier momento y no expiran.
FaceList/LargeFaceList Esta es una lista surtida de objetos PersistedFace. Un objeto FaceList tiene un identificador único, una cadena de nombre y, de manera opcional, una cadena de datos de usuario.
Persona Se trata de una lista de objetos PersistedFace que pertenecen a la misma persona. Tiene un identificador único, una cadena de nombre y, de manera opcional, una cadena de datos de usuario.
PersonGroup/LargePersonGroup Esta es una lista surtida de objetos Person. Tiene un identificador único, una cadena de nombre y, de manera opcional, una cadena de datos de usuario. Un objeto PersonGroup debe entrenarse previamente para poder usarlo en operaciones de reconocimiento.

La operación de verificación toma un id. de cara de una lista de caras detectadas en una imagen (colección DetectedFace) y determina si las caras pertenecen a la misma persona mediante la comparación del identificador con una colección de caras persistentes (PersistedFace). Las imágenes de caras persistentes que tienen un id. único y un nombre identifican a una persona. De manera opcional, un grupo de personas puede agruparse en un objeto PersonGroup para mejorar el rendimiento del reconocimiento. Básicamente, una persona es una unidad básica de identidad y el objeto person puede tener una o varias caras conocidas registradas. Cada persona se define en un objeto PersonGroup concreto (una colección de personas) y la identificación se realiza en un objeto PersonGroup. El sistema de seguridad crearía uno o varios objetos PersonGroup y, a continuación, les asociaría personas. Una vez creado un grupo, la colección PersonGroup debe entrenarse para poder usarlo en una identificación. Además, tiene que volver a entrenarse después de agregar o quitar a cualquier persona, o si se edita la cara registrada de alguna persona. El entrenamiento se realiza mediante Train API de PersonGroup. Cuando se usa la biblioteca cliente, solo se requiere una llamada al método TrainPersonGroupAsync:

await faceServiceClient.TrainPersonGroupAsync(personGroupId);

El entrenamiento es un proceso asincrónico. Puede que no finalice incluso después de la devolución del método TrainPersonGroupAsync. Es posible que deba consultar el estado de entrenamiento con el método GetPersonGroupTrainingStatusAsync hasta que esté listo para continuar con la detección o la comprobación de caras.

Al realizar la comprobación de caras, Face API calcula la similitud de una cara detectada entre todas las caras de un grupo y devuelve las personas más similares a la cara de prueba. Esto se realiza a través del método IdentifyAsync de la biblioteca cliente. La cara de prueba debe detectarse mediante los pasos mencionados anteriormente y, a continuación, el id. de cara se pasa a Identify API como un segundo argumento. Se pueden identificar varios id. de cara a la vez. El resultado contendrá todos los resultados de Identify. De forma predeterminada, Identify solo devuelve una persona, que es la más parecida a la cara de prueba. Si lo prefiere, puede especificar el parámetro opcional maxNumOfCandidatesReturned para permitir que Identify devuelva más candidatos. El código de la Figura 5 muestra el proceso de identificación y verificación de una cara:

Figura 5 Proceso de reconocimiento facial

public class FaceRecognition : IRecognition
{
  public double Recognize(string siteId, out string name)
  {
    FaceClient faceClient = new FaceClient(
      new ApiKeyServiceClientCredentials("<Subscription Key>"))
    {
      Endpoint = "<API Endpoint>"
    };
    ReadImageStream(siteId, out Stream imageStream);
    // Detect faces in the image
    IList<DetectedFace> detectedFaces =
      faceClient.Face.DetectWithStreamAsync(imageStream).Result;
    // Too many faces detected
    if (detectedFaces.Count > 1)
    {
      name = string.Empty;
      return 0;
    }
    IList<Guid> faceIds = detectedFaces.Select(f => f.FaceId.Value).ToList();
    // Identify faces
    IList<IdentifyResult> identifiedFaces =
      faceClient.Face.IdentifyAsync(faceIds, "<Person Group ID>").Result;
    // No faces identified
    if (identifiedFaces.Count == 0)
    {
      name = string.Empty;
      return 0;
    }
    // Get the first candidate (candidates are ranked by confidence)
    IdentifyCandidate candidate =
      identifiedFaces.Single().Candidates.FirstOrDefault();
    // Find the person
    Person person =
      faceClient.PersonGroupPerson.GetAsync("", candidate.PersonId).Result;
    name = person.Name;
    return candidate.Confidence;
  }

En primer lugar, debe obtener un objeto de cliente para Face API. Para ello, pase su clave de suscripción y el punto de conexión de la API. Puede obtener ambos valores de la instancia de Azure Portal donde aprovisionó el servicio Face API. A continuación, debe detectar cualquier cara visible en una imagen, pasada el método DetectWithStreamAsync del objeto Face del cliente. El objeto Face implementa las operaciones de detección y verificación de Face API. De las caras detectadas, me aseguro de que solo una se detecte realmente y obtengo su id. (identificador único de la colección de caras registradas de todas las personas autorizadas a acceder al sitio). A continuación, el método IdentifyAsync realiza la identificación de la cara detectada en un objeto PersonGroup y devuelve una lista de las coincidencias o los candidatos mejores, ordenada por nivel de confianza. Con el id. de persona del primer candidato, recupero el nombre de la persona, que finalmente se devuelve a Access Web API. Se cumple el requisito de autorización de cara.

Reconocimiento de voz

Azure Cognitive Services Speaker Recognition API ofrece algoritmos para el reconocimiento y la identificación del hablante. Las voces tienen características únicas que pueden usarse para identificar una persona, del mismo modo que una huella digital. La solución de seguridad de este artículo utiliza la voz como señal de control de acceso, donde el sujeto pronuncia una frase de contraseña en un micrófono registrado como dispositivo IoT. Al igual que con el reconocimiento facial, el reconocimiento de voz también requiere una inscripción previa de las personas autorizadas. Speaker API denomina a una persona inscrita "Perfil". Al realizar la inscripción de un perfil, se graba la voz del hablante pronunciando una frase específica y, a continuación, se extraen diversas características y se reconoce la frase elegida. Juntas, las características extraídas y la frase elegida forman una firma de voz única. Durante la verificación, una voz de entrada y una frase se comparan con la firma de voz y la frase de la inscripción con el fin de comprobar si son de la misma persona y si la frase es correcta.

Al examinar la implementación del código, Speaker API no se beneficia de un paquete administrado en NuGet como Face API, así que el enfoque que usaré es invocar la API de REST directamente con un mecanismo de solicitud y respuesta de cliente HTTP. El primer paso es crear una instancia de HttpClient con los parámetros necesarios para la autenticación y el tipo de datos:

public VoiceRecognition()
{
  _httpClient = new HttpClient();
  _httpClient.BaseAddress = new Uri("<API Endpoint>");
  _httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key",
    "<Subscription Key>");
  _httpClient.DefaultRequestHeaders.Accept.Add(
     new MediaTypeWithQualityHeaderValue("application/json"));
}

El método Recognize de la Figura 6 realiza el desarrollo en varios pasos, como se indica a continuación: Después de obtener la secuencia de audio del dispositivo IoT en el sitio, intenta identificar el audio en una colección de perfiles inscritos. La identificación se codifica en el método IdentifyAsync. Este método asincrónico prepara un mensaje de solicitud de varias partes, que contiene la secuencia de audio y los id. de perfil de identificación, y envía una solicitud POST a un punto de conexión específico. Si la respuesta de la API es Código HTTP 202 (Aceptado), el valor devuelto es un URI de la operación que se ejecuta en segundo plano. La finalización de esta operación, en el URI identificado, se comprueba con el método Recognize cada 100 ms. Si se realiza correctamente, obtuvo el id. de perfil de la persona identificada. Con ese id., puede continuar con la comprobación de la secuencia de audio, que es la confirmación final de la pertenencia de la voz grabada a la persona identificada. Se implementa en el método VerifyAsync, que funciona de forma similar al método IdentifyAsync, excepto en que devuelve un objeto VoiceVerificationResponse que contiene el perfil de la persona y, por tanto, su nombre. La respuesta de verificación incluye un nivel de confianza, que también se devuelve a Access Web API, al igual que Face API.

Figura 6 Reconocimiento de voz

public double Recognize(string siteId, out string name)
{
  ReadAudioStream(siteId, out Stream audioStream);
  Guid[] enrolledProfileIds = GetEnrolledProfilesAsync();
  string operationUri =
    IdentifyAsync(audioStream, enrolledProfileIds).Result;
  IdentificationOperation status = null;
  do
  {
    status = CheckIdentificationStatusAsync(operationUri).Result;
    Thread.Sleep(100);
  } while (status == null);
  Guid profileId = status.ProcessingResult.IdentifiedProfileId;
  VoiceVerificationResponse verification =
    VerifyAsync(profileId, audioStream).Result;
  if (verification == null)
  {
    name = string.Empty;
    return 0;
  }
  Profile profile = GetProfileAsync(profileId).Result;
  name = profile.Name;
  return ToConfidenceScore(verification.Confidence);
}

Me gustaría agregar algunos comentarios adicionales sobre esta API, para indicar en qué se diferencia de Face API. La API de verificación de voz devuelve un objeto JSON que contiene el resultado general de la operación de verificación (aceptación o rechazo), un nivel de confianza (baja, normal o alta) y la frase reconocida:

{
  "result" : "Accept", // [Accept | Reject]
  "confidence" : "Normal", // [Low | Normal | High]
  "phrase": "recognized phrase"
}

Este objeto se asigna a la clase VoiceVerificationResponse de C# para facilitar la operación en el método VerifyAsync, pero su nivel de confianza se expresa como texto:

public class VoiceVerificationResponse
{
  [JsonConverter(typeof(StringEnumConverter))]
  public Result Result { get; set; }
  [JsonConverter(typeof(StringEnumConverter))]
  public Confidence Confidence { get; set; }
  public string Phrase { get; set; }
}

Access Web API, en su lugar, espera un valor decimal (tipo de datos doble) entre 0 y 1, por lo que especifiqué algunos valores numéricos para la enumeración Confidence:

public enum Confidence
{
  Low = 1,
  Normal = 50,
  High = 99
}

Luego, los convertí en dobles antes de volver a Access Web API:

private double ToConfidenceScore(Confidence confidence)
{
  return (double)confidence / 100.0d;
}

Resumen

Eso es todo para esta primera parte, en la que traté el flujo de seguridad general de acceso al sitio, y la implementación del mecanismo de autorización en la API web de ASP.NET Core mediante directivas y requisitos personalizados. A continuación, ilustré el reconocimiento facial y de voz mediante las API de Cognitive Services pertinentes, como un mecanismo para restringir el acceso en función de la información biométrica de perfiles de personas previamente autorizadas o inscritas. En la segunda parte de este artículo, explicaré el streaming de datos de dispositivos IoT como un punto desencadenador para solicitar acceso, y la confirmación final de Access API para desbloquear (o bloquear) la puerta de acceso. También hablaré de un servicio de detección de anomalías basado en Machine Learning, que se ejecutará con cualquier intento de acceso para identificar su riesgo.

El código fuente de esta parte inicial de la solución está disponible en GitHub en bit.ly/2IXPZCo.


Stefano Tempesta es director regional de Microsoft, MVP de inteligencia artificial y Business Applications y miembro de Blockchain Council. Los intereses de Tempesta, orador frecuente en conferencias de TI internacionales, como Microsoft Ignite y Tech Summit, se extienden a las tecnologías de cadena de bloques y relacionadas con la inteligencia artificial. Creó Blockchain Space (blogchain.space), un blog sobre tecnologías de cadena de bloques, escribe para MSDN Magazine y MS Dynamics World, y publica experimentos de Machine Learning en Azure AI Gallery (gallery.azure.ai).

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Barry Dorrans


Comente este artículo en el foro de MSDN Magazine