Leer un QR desde Windows con la webcam

Si buscamos como hacernos nuestro lector de códigos QR aparecen muchas entradas de como hacerlo en dispositivos móviles, pero si lo queremos hacer desde Windows con nuestra webcam por el motivo que sea, aparte de drivers propietarios no hay mucho donde consultar.

 

Por eso, después de unas horillas de prueba/error, dejo para compartir esto:

 

http://windowqr.codeplex.com/

 

Una prueba de concepto donde uno la librería de visión por computador AForge con el decodificador  de QRs ZXING, todo ello orquestado en una aplicación WPF ( y sin memory leacks)

La clase CamControl es la que nos va a permitir gestionar las webcams de nuestro sistema.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using AForge.Video.DirectShow;
   6:   
   7:  namespace QRWindowsClient
   8:  {
   9:      /// <summary>
  10:      /// Class to control the cameras of the system.
  11:      /// </summary>
  12:      class CamControl
  13:      {
  14:          private int selectedFrameSize;
  15:          private int selectedFps;
  16:          private VideoCaptureDevice selectedDevice;
  17:          public FilterInfoCollection Devices { get; private set; }
  18:          public Dictionary<int, string> FrameSizes { get; private set; }
  19:          public List<int> Fps;
  20:   
  21:          /// <summary>
  22:          /// Gets the selected device.
  23:          /// </summary>
  24:          public VideoCaptureDevice SelectedDevice
  25:          {
  26:              get
  27:              {
  28:                  return selectedDevice;
  29:              }
  30:              private set
  31:              {
  32:                  selectedDevice = value;
  33:                  RefreshFrameSize();
  34:              }
  35:          }
  36:   
  37:          /// <summary>
  38:          /// Initializes a new instance of the <see cref="CamControl"/> class.
  39:          /// </summary>
  40:          public CamControl()
  41:          {
  42:              Devices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
  43:   
  44:              // by default select the first one
  45:              SetCamera(0);
  46:          }
  47:   
  48:          /// <summary>
  49:          /// Sets the active camera.
  50:          /// </summary>
  51:          /// <param name="index">The index of the camera</param>
  52:          public void SetCamera(int index)
  53:          {
  54:              if (Devices.Count < index)
  55:              {
  56:                  throw new IndexOutOfRangeException("There is no device with index " + index);
  57:              }
  58:   
  59:              SelectedDevice = new VideoCaptureDevice(Devices[index].MonikerString);
  60:          }
  61:   
  62:          /// <summary>
  63:          /// Sets the size of the frame.
  64:          /// </summary>
  65:          /// <param name="index">The index of the available fps</param>
  66:          public void SetFrameSize(int index)
  67:          {
  68:              if (FrameSizes.Count < index)
  69:              {
  70:                  throw new IndexOutOfRangeException("There is no framesize with index " + index);
  71:              }
  72:   
  73:              selectedFrameSize = index;
  74:              RefreshFps();
  75:              ConfigureCamera();
  76:          }
  77:   
  78:          /// <summary>
  79:          /// Sets the FPS of the active camera.
  80:          /// </summary>
  81:          /// <param name="fps">The FPS</param>
  82:          public void SetFps(int fps)
  83:          {
  84:              if (!Fps.Contains(fps))
  85:              {
  86:                  throw new IndexOutOfRangeException("There is no fps like " + fps);
  87:              }
  88:   
  89:              selectedFps = fps;
  90:              ConfigureCamera();
  91:          }
  92:   
  93:          /// <summary>
  94:          /// Refreshes the size of the frame.
  95:          /// </summary>
  96:          private void RefreshFrameSize()
  97:          {
  98:              this.FrameSizes = new Dictionary<int, string>();
  99:              int i = 0;
 100:              foreach (VideoCapabilities set in SelectedDevice.VideoCapabilities)
 101:              {
 102:                  this.FrameSizes.Add(i, String.Format("{0} x {1}", set.FrameSize.Width, set.FrameSize.Height));
 103:                  i++;
 104:              }
 105:   
 106:              selectedFrameSize = 0;
 107:              RefreshFps();
 108:          }
 109:   
 110:          /// <summary>
 111:          /// Refreshes the FPS.
 112:          /// </summary>
 113:          private void RefreshFps()
 114:          {
 115:              int MaxFramerate = selectedDevice.VideoCapabilities[selectedFrameSize].FrameRate;
 116:              Fps = new List<int>();
 117:              for (int i = 1; i < MaxFramerate; i++)
 118:              {
 119:                  if (i % 5 == 0)
 120:                  {
 121:                      Fps.Add(i);
 122:                  }
 123:              }
 124:   
 125:              selectedFps = Fps.Min();
 126:          }
 127:   
 128:          /// <summary>
 129:          /// Configures the camera.
 130:          /// </summary>
 131:          private void ConfigureCamera()
 132:          {
 133:              SelectedDevice.DesiredFrameSize = SelectedDevice.VideoCapabilities[selectedFrameSize].FrameSize;
 134:              SelectedDevice.DesiredFrameRate = selectedFps;
 135:          }
 136:      }
 137:  }

 
 

La clase QRProcess es la que se encarga de llamar al código de zxing para descifrar el QR conforme puede, ya que recibiremos más imagenes que capacidad de procesamiento tenemos. Hace uso de Monitor para hilar fino ademas de ofrecer un evento para cuando encontremos la imagen que contiene un QR válido.

 
   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Drawing;
   6:  using com.google.zxing.qrcode;
   7:  using com.google.zxing;
   8:  using com.google.zxing.common;
   9:  using System.Threading;
  10:   
  11:  namespace QRWindowsClient
  12:  {
  13:      /// <summary>
  14:      /// Class to decode qr bitmaps in a separated thread.
  15:      /// </summary>
  16:      public class QRProcess
  17:      {
  18:          public QRCodeReader reader = new QRCodeReader();
  19:          public Bitmap CurrentBitmap { get; private set; }
  20:          public event EventHandler<ProcessArgs> ResultFound;
  21:   
  22:          public bool Run { get; set; }
  23:          public bool newBitmap { get; set; }
  24:   
  25:          /// <summary>
  26:          /// When a new bitmap is available, call this method to prepare the bitmap for be consumed.
  27:          /// </summary>
  28:          /// <param name="newBitmap">The bitmap to be consumed.</param>
  29:          public void NewBitmap(Bitmap newBitmap)
  30:          {
  31:              Monitor.Enter(this);
  32:              if (this.newBitmap == false)
  33:              {
  34:                  this.CurrentBitmap = (Bitmap)newBitmap.Clone();
  35:                  this.newBitmap = true;
  36:                  Monitor.PulseAll(this);
  37:              }
  38:              Monitor.Exit(this);
  39:          }
  40:   
  41:          /// <summary>
  42:          /// Stops this instance.
  43:          /// </summary>
  44:          public void Stop()
  45:          {
  46:              Monitor.Enter(this);
  47:              this.Run = false;
  48:              Monitor.PulseAll(this);
  49:              Monitor.Exit(this);
  50:          }
  51:   
  52:          /// <summary>
  53:          /// Starts this instance.
  54:          /// </summary>
  55:          public void Start()
  56:          {
  57:              this.Run = true;
  58:   
  59:              ThreadStart start = new ThreadStart(delegate
  60:              {
  61:                  while (this.Run)
  62:                  {
  63:                      Monitor.Enter(this);
  64:                      while (this.newBitmap == false)
  65:                      {
  66:                          Monitor.Wait(this, 1000);
  67:                      }
  68:                      using (Bitmap bitmap = (Bitmap)this.CurrentBitmap.Clone())
  69:                      {
  70:                          Monitor.Exit(this);
  71:                          var result = this.Process(bitmap);
  72:                          if (!string.IsNullOrWhiteSpace(result))
  73:                          {
  74:                              if (this.ResultFound != null)
  75:                              {
  76:                                  ProcessArgs args = new ProcessArgs() { Result = result };
  77:                                  this.ResultFound(this, args);
  78:                                  this.Run = false;
  79:                              }
  80:                          }
  81:                      }
  82:   
  83:                      // the class is ready to accept a new bitmap to evaluate.
  84:                      this.newBitmap = false;
  85:                  }
  86:              });
  87:   
  88:              Thread t = new Thread(start);
  89:              t.Start();
  90:          }
  91:   
  92:   
  93:   
  94:          /// <summary>
  95:          /// Processes the specified bitmap.
  96:          /// </summary>
  97:          /// <param name="bitmap">The bitmap.</param>
  98:          /// <returns>The message decoded or string.empty</returns>
  99:          public string Process(Bitmap bitmap)
 100:          {
 101:              try
 102:              {
 103:                  com.google.zxing.LuminanceSource source = new RGBLuminanceSource(bitmap, bitmap.Width, bitmap.Height);
 104:                  var binarizer = new HybridBinarizer(source);
 105:                  var binBitmap = new BinaryBitmap(binarizer);
 106:                  return reader.decode(binBitmap).Text;
 107:              }
 108:              catch
 109:              {
 110:                  return string.Empty;
 111:              }
 112:          }
 113:      }
 114:   
 115:      /// <summary>
 116:      /// Class to pass arguments between QRProcess events.
 117:      /// </summary>
 118:      public class ProcessArgs : EventArgs
 119:      {
 120:          /// <summary>
 121:          /// Gets or sets the result.
 122:          /// </summary>
 123:          /// <value>
 124:          /// The result.
 125:          /// </value>
 126:          public string Result { get; set; }
 127:      }
 128:  }

 

Y para unir las dos clases y que se vea en pantalla lo que vamos capturando por la cam el siguiente método. Aquí lo unico que hay que destacar es el tener cuidado cuando trabajamos con punteros (variable hBitMap), que luego hay que destruirlos llamando al método DeleteObject.

 

   1:  //Reference the GDI DeleteObject method.
   2:          [System.Runtime.InteropServices.DllImport("GDI32.dll")]
   3:          public static extern bool DeleteObject(IntPtr objectHandle);
   4:   
   5:          void captureDevice_NewFrame(object sender, NewFrameEventArgs eventArgs)
   6:          {
   7:              using (UnmanagedImage uimage = UnmanagedImage.FromManagedImage(eventArgs.Frame))
   8:              {
   9:                  try
  10:                  {
  11:                      using (Bitmap image = uimage.ToManagedImage())
  12:                      {
  13:                          IntPtr hBitMap = image.GetHbitmap();
  14:                          try
  15:                          {
  16:                              BitmapSource bmaps = Imaging.CreateBitmapSourceFromHBitmap(hBitMap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
  17:                              bmaps.Freeze();
  18:                              Dispatcher.Invoke((Action)(() =>
  19:                              {
  20:                                  pictureBoxMain.Source = bmaps;
  21:                              }), DispatcherPriority.Render, null);
  22:   
  23:                              process.NewBitmap(image);
  24:                          }
  25:                          finally
  26:                          {
  27:                              DeleteObject(hBitMap);
  28:                          }
  29:                      }
  30:                  }
  31:                  catch (Exception ex)
  32:                  {
  33:                      ex.ToString();
  34:                  }
  35:              }
  36:   
  37:          }

 

Y no tiene más misterio. Espero que le sea a alguien útil.