Cazando juega vivos que manejan por el hombro con auto-detección de placas

20 de marzo de 2017

#.Resumen (tl;dr;)

Desarrollé de un prototipo de aplicación para “cazar juega vivos” (denuncia pública) que manejan por el hombro que auto-detecte el número de placa y lo publique a Twitter, como se muestra a continuación:

Visualización del aplicativo móvil

Además publica en Twitter un status similar al siguiente:

La aplicación fue desarrollada con fines completamente experimentales y educativos. Si alguien desea más información sobre la aplicación me puede contactar a me@demogar.com o escribirme al Twitter.


#.La situación del “juega vivo”

Basta con manejar por la mañana por la Autopista Arraiján-Chorrera o el Corredor Norte para notar un especimen muy particular, que ocasiona mucho más tranque al que generalmente hay. Me refiero a estos animalitos de la creación que utilizan el hombro, un espacio obligatorio para las vías de alta velocidad y que son requeridos por si tienes algún desperfecto con el auto o simplemente para dar un poco de espacio si es requerido (como cuando las ambulancias requieren su paso).

Todo comenzó el viernes pasado (17 de Marzo) venía por el Corredor Norte. Noté una fila fatal. Veinte minutos después, el tráfico desaparecía: todo se debía a un auto que se encontraba con daños en el hombro y ocasionaba un embudo (con los animalitos ya mencionados) que se tiraban de vuelta a la vía sin importarle nada con nadie.

Entonces pensé, mientras escuchaba un podcast:

“Sería buenísimo tener una aplicación en el teléfono que me permitiera automáticamente reportar a esta gente, así como Inspector Ciudadano, el cual no tiene esta falta tipificada, pero que además detectara la placa de forma inmediata.”

Esa misma tarde/noche me dediqué a investigar un poco más y encontré que era posible hacerlo, así que creí sería bueno intentarlo y hacer mis propias denuncias ciudadanas… al final la tecnología está al alcance de todos.

#.Flujo esperado

Necesitaríamos que nuestro flujo de la implementación sea similar al siguiente:

Flujo de la aplicación
  1. El usuario, a través de una aplicación móvil (iOS por simplicidad), tome una fotografía del juega vivo.
  2. Esta fotografía, a través precisamente de esta aplicación, se comunicaría a través de un servicio web (ver abajo porque no hacer el procesamiento en el teléfono).
  3. El servicio web recibiría la consulta e iniciaría el procesamiento.
  4. El servicio web consultaría la placa al OpenALPR para intentar encontrarla. El ALPR contestaría con los resultados encontrados, el % de confianza en el resultado y las coordenadas de la placa encontrada.
  5. El servicio web analizaría los resultados de ALPR y los publicaría en una Base de Datos.
  6. El mismo servicio publicaría este juega vivo en Twitter. Para eso cree una cuenta llamada @ManejoAlHombro.
  7. Los resultados encontrados se enviarían de vuelta al cliente a través del servicio web y se desplegaría lo encontrado a través del aplicativo.

Para ello decidí utilizar un stack similar al siguiente (que voy a ir explicando por encima poco a poco):

  • OpenALPR como sistema de ALPR/ANPR.
  • Ruby para hacer una integración entre el servicio web y el ALPR.
  • Sinatra (Ruby) para el servicio web.
  • RMagick (Ruby) para dibujar el recuadro magenta.
  • DataMapper (Ruby) para la integración con la Base de Datos.
  • Twitter gem (Ruby) para la integración con Twitter.
  • Appcelerator (JavaScript) para la aplicación móvil en iOS (prototipo).

La intención era desarrollar una aplicación completamente funcional en menos de 4 horas del día sábado.

#.Conociendo ALPR y OpenALPR

Automatic number plate recognition (ANPR) (link) o Automatic Licence Plate Recognition (ALPR) es una tecnología que permite, a través de una fotografía o vídeo, el reconocimiento de las placas de los vehículos. Básicamente utilizan tecnología tipo Optical Character Recognition (OCR) (link), que permite a través de una imagen la conversión a texto.

Es muy utilizado para el cumplimiento de la ley en algunos países o para simplemente detectar movimientos de flotas, entre otros usos.

De todas las alternativas del mercado, encontré OpenALPR, una solución completamente libre y abierta que permite instalarlo en un servidor local.

OpenALPR is an open source Automatic License Plate Recognition library written in C++ with bindings in C#, Java, Node.js, Go, and Python. The library analyzes images and video streams to identify license plates. The output is the text representation of any license plate characters.

#.Instalando OpenALPR

Para las pruebas, decidí utilizar dos variantes: una con Docker y otra con Virtualbox + Ubuntu Server 16.04 LTS. Para el último caso fue muy sencilla la instalación y solo fue necesaria la ejecución de la siguiente línea desde la línea de comandos:

sudo apt-get update && sudo apt-get install -y openalpr openalpr-daemon openalpr-utils libopenalpr-dev

A nivel de desarrollo utilicé Docker, lo cual fue mucho más simple de implementar. Solo fue necesario abrir algunos puertos para el desarrollo en Sinatra.

#.Configurando OpenALPR para las placas de Panamá

OpenALPR tiene varias configuraciones de regiones (Estados Unidos, Australia, Unión Europea) y países (Estados Unidos, México, Brasil, etc.), pero Panamá no es una de ellas. Las placas de Panamá, en su confección (tamaños, etc.), son muy similares a las placas de México y algunos estados de Estados Unidos.

Primero era necesario determinar los distintos patrones de las placas de Panamá, de los cuales encontré los siguientes:

pa      @@#### // <- las placas nuevas, ej.: AB1234. Aplica también para Metro Bus, ej.: MB0000
pa      ###### // <- las placas anteriores, ej.: 123456
pa      @##### // <- las placas de Taxi, ej.: T12345
pa      #@@##### // <- las placas de Taxi de Ruta Interna, ej.: 8RI12345

Un archivo llamado pa.patterns debe ser creado en /usr/share/openalpr/runtime_data/postprocess con la configuración anteriormente descrita.

Adicionalmente hay que crear un archivo llamado pa.conf y ubicarlo en /usr/share/openalpr/runtime_data/config. Este archivo es la configuración física de la placa:

; 30-50, 40-60, 50-70, 60-80
char_analysis_min_pct = 0.30
char_analysis_height_range = 0.20
char_analysis_height_step_size = 0.10
char_analysis_height_num_steps = 4

segmentation_min_speckle_height_percent = 0.3
segmentation_min_box_width_px = 4
segmentation_min_charheight_percent = 0.5;
segmentation_max_segment_width_percent_vs_average = 1.35;

plate_width_mm = 304.8
plate_height_mm = 152.4

multiline = 0

char_height_mm = 70
char_width_mm = 35
char_whitespace_top_mm = 38
char_whitespace_bot_mm = 38

template_max_width_px = 120
template_max_height_px = 60

; Higher sensitivity means less lines
plateline_sensitivity_vertical = 25
plateline_sensitivity_horizontal = 45

; Regions smaller than this will be disqualified
min_plate_size_width_px = 70
min_plate_size_height_px = 35

; Results with fewer or more characters will be discarded
postprocess_min_characters = 5
postprocess_max_characters = 7

detector_file = us.xml

ocr_language = lus

; Override for postprocess letters/numbers regex.
postprocess_regex_letters = [A-Z]
postprocess_regex_numbers = [0-9]

; Whether the plate is always dark letters on light background, light letters on dark background, or both
; value can be either always, never, or auto
invert = auto

Rescatables de aquí solo los tamaños (304.8 x 152.4 mm, es decir 12”x6”). Utilizamos el mismo archivo de detección de Estados Unidos (detector_file = us.xml).

#.Creando una integración a OpenALPR desde Ruby

OpenALPR tiene integraciones con C#, Python, Node.js, Go y Java.

  • Java: él y yo no nos llevamos muy bien.
  • C#: no tengo Windows.
  • Python: me gusta, he trabajado con él, pero no he hecho nada del otro mundo. Creo que me costaría más del sábado y no era la intención.
  • Go: jamás lo he usado y no tengo interés de aprenderlo en un fin de semana.
  • Node.js: me gusta, pero luego de probarla no me funcionó muy bien la integración (un poco caótica para mis gustos).

Al final terminé haciendo una integración propia con Ruby, muy simple que pueden ver a continuación:

class OpenAlpr
  attr_reader :output, :command

  def initialize(file)
    @file = file
    @output = []

    begin
      @output = JSON.parse(processPhoto(file))
    rescue JSON::ParserError
      @output = nil
    end
  end

  private
  def processPhoto(file)
    @command = "alpr -j -n 10 -c pa -p pa #{Shellwords.shellescape file}"
    `#{@command}`
  end
end

Básicamente ejecuto el comando por línea de comandos, utilizando el flag -j para que regrese en JSON. Todo esto muy bien descrito en la guía de OpenALPR.

#.Haciendo un servicio web en Sinatra para la integración

Vamos a partir del hecho de que sería muchísimo mejor que pudiesemos hacer este cómputo desde el teléfono, pero quería solo invertir parte de mi sábado para esto. Al final solo es una prueba de concepto o un prototipo que me gustaría en un futuro mejorar y desplegar públicamente. Por ello que preferí hacer un servicio web en Sinatra que llame a la integración que ya hicimos arriba.

El servicio web se encargaría, según el flujo planteado, de:

  • Recibir la consulta que se envía a través del aplicativo móvil.
  • Consultar al OpenALPR utilizando la fotografía.
  • Analizar el resultado encontrado. Si es buen resultado, entonces lo enviaría a la DB (para poder luego compartir cualquier reporte a las autoridades).
  • Además publicaría esto a la cuenta de Twitter de @ManejoAlHombro.
  • Realizaría, con RMagick, un cuadro basándose en las coordenadas recibidas por OpenALPR.

Para evitar que este post no sea muy extenso y enfocarnos en los resultados, solo analizaremos el proceso de consulta a OpenALPR:

  # ...
  if params[:photo]
    # Get the filename
    @filename = params[:photo][:filename]

    # Write it locally
    File.open("public/photos/#{@filename}", "w") do |f|
      f.write(params[:photo][:tempfile].read)
    end

    # Analyze it with openalpr
    @search = OpenAlpr.new("public/photos/#{@filename}")
  end

  # Validate if openalpr returns values
  if @search && @search.output && @search.output['results'].size > 0
    # Get the better result
    @result = @search.output['results'].first

    # Get the coordinates for this result (the best)
    coordinates = @result['coordinates']
    first_coordinate = coordinates.first
    third_coordinate = coordinates[2]

  # ...

Nuevamente, esta es una parte pequeña del código y solo es la parte que consume la integación que ya se hizo. El código completo es corto (cerca de 250 líneas de código solamente).

#.Integración con Twitter

Adicionalmente, hice una implementación con Twitter. Una publicación final se vería como esta:

Integración con Twitter

Nota: Esta imagen era solo de muestra, este auto no circulaba por el hombro y por ende lo removí del Twitter.

Notamos que se publicaría la imagen ya procesada por OpenALPR y nuestro servicio hecho en Ruby, el cual automáticamente detecta el número de placa y le agregamos (con RMagick) además un recuadro magenta en la placa que encontró en el lugar ya encontrado. Una belleza ya que sigue la intención real de la aplicación: hacer una denuncia pública ciudadana, tal como lo hizo ENA con las personas que se llevaron las barreras para evadir el peaje.

#.Desarrollo de la aplicación móvil

Para la aplicación móvil decidí realizarla en Appcelerator. Tengo tres razones para escoger esta tecnología:

  • Para mi, era necesario proyectar a una integración multi-plataforma. Inicialmente decidí desarrollar en iOS ya que es mucho más rápido y sencillo de probar, pero la intención es mejorar el proyecto.
  • Como era un proyecto de fin de semana, no quería incursionar en nuevas tecnologías desconocidas para mi (como Ionic o NativeScript). He utilizado React Native y realmente me gsuta, pero siento Appcelerator aún un poco más maduro. Además, me siento cómodo con Appcelerator y consideré que no necesitaría más que una hora para desarrollar un prototipo funcional (y así fue).
  • Si me gustaría más una opción nativa (como Appcelerator o NativeScript) para en el futuro hacer la integración y procesamiento directo en el teléfono. Solo es hacer la migración a través de un módulo, algo que ya he hecho y es bastante simple.

La aplicación decidí que debía llevar dos pantallas / tabs sencillos:

  • Un Tab para realizar los reportes, con dos botones: para hacerlo a través de una nueva fotografía o para hacerlo a través de una fotografía de la galería.
  • Un Tab para configurar el endpoint o la ruta al servicio, útil para cambiar de ambientes.

El código que se encarga de utilizar una fotografía de la galería y publicarlo sería el siguiente:

function _openCamera() {
	Titanium.Media.showCamera({
		success : function(event) {
			if (event.mediaType == Ti.Media.MEDIA_TYPE_PHOTO) {
				_uploadReport(event.media);
			} else {
				alert("No es una imagen =" + event.mediaType);
			}
		},
		error : function(error) {
			var a = Titanium.UI.createAlertDialog({
				title : 'Camera'
			});
			a.setMessage('Error: ' + error.code);
			a.show();
		},
		saveToPhotoGallery : true,
		allowEditing : true,
		mediaTypes : [Ti.Media.MEDIA_TYPE_PHOTO]
	});
}

function _openGallery() {
	Ti.Media.openPhotoGallery({
		success : function(event) {
			if (event.mediaType == Ti.Media.MEDIA_TYPE_PHOTO) {
				_uploadReport(event.media);
			} else {
				alert("No es una imagen =" + event.mediaType);
			}
		},
		error : function(error) {
			var a = Titanium.UI.createAlertDialog({
				title : 'Camera'
			});
			a.setMessage('Unexpected error: ' + error.code);
			a.show();
		},
		allowEditing : true,
		allowMultiple : false,
		mediaTypes : [Ti.Media.MEDIA_TYPE_PHOTO]
	});
}

Funcionalmente el aplicativo se vería así:

Simulador en iOS

Nota: Esta imagen era solo de muestra, este auto no circulaba por el hombro y por ende lo removí del Twitter.

#.Comentarios y posibles mejoras a futuro

Todo este desarrollo lo hice en forma personal durante el fin de semana. No lo he hecho pensando en desarrollar un producto o una aplicación de consumo masivo por ahora. Creo que el producto aún no estaría production ready. Por ahora, estaré utilizándolo de forma personal y si alguien le interesa no dude en escribirme un coreo electrónico o por Twitter. Como tengo un iPhone que no estoy estaba utilizando creo que ya se para que lo puedo utilizar.

Como posibles mejoras del aplicativo estarían:

  • Lo ideal sería que la cámara auto-detectara el movimiento del auto o cuando encuentre una placa. Para esto sería necesario hacer la integración a nivel nativo e integrar OpenALPR nativamente (totalmente posible).
  • Sería buena idea que el aplicativo móvil funcionase en Android también. En Pixmat tenemos un laboratorio de pruebas, por lo que para mi sería bastante fácil migrarlo.
  • Quisiera agregar otras faltas y fallas y que el sistema permitiese reportar, como botar basura por el auto, estacionarse mal, no poner las direccionales, entre otras. Hay varias faltas que ya se pueden reportar por Inspector Ciudadano, pero otras que no se podrán reportar, por eso pienso que esta aplicación debe ser una denuncia pública.
  • Debo agregar más posibles mensajes / hashtags al Twitter. Esto es algo sencillo. Tampoco le agregué (aún) el mention a @ATTTPanama.
  • La aplicación aún tiene algunos problemas para detectar algunas fotografías / placas. No se determina si el porcentaje de confianza de la imagen es arriba del 50% ni tampoco pide un feedback al usuario.
  • El servicio recibe adicionalmente 3 otros parámetros que no fueron configurados aún: Latitud, Longitud y Comentarios. La intención sería que el reporte también utilice el GPS para ubicar a la persona.
  • Si la placa no se encuentra, no da opción a que la persona haga una retroalimentación como escribir manualmente la placa o simplemente corregirla.
  • Planeo en algún momento publicar todo el código fuente. Siento que, como es un trabajo hecho a modo de hacking, el resultado no es muy óptimo ni me siento satisfecho ya que me enfoqué en que funcionara.

A futuro, planeo hacer algunas pruebas con un Arduino y un RPi para ver si es posible hacer un sistema similar, pero totalmente autónomo (quizás hasta probar con un Dashcam). Definitivamente con los tráficos que a veces ocasionan estos individuos podría hacerlo incluso en el tranque.