He hecho una cosa: Obtener imágenes del sensor óptico de un ratón

Hace tiempo decubrí que los sensores de los ratones ópticos son cámaras gracias a este artículo. Lo puse en la lista de cosas pendientes hasta hoy que he encontrado un rato para investigar. He abierto algunos ratones hechos polvo que tengo guardados y ninguno tiene sensor ADNS-2051, pero si el ADNS-5020 que funciona de forma parecida. Así que he cogido un Logitech LX3 y lo he destripado.

El ADNS-5020 es un sensor óptico que toma imágenes de 15 x 15 píxeles en blanco y negro, y es capaz de calcular el movimiento en los ejes X e Y a partir de ellas. Tiene un interfaz SPI, así que he usado un Arduino Nano para conectarme a él. Pero antes hay que hacer algunas modificaciones al circuito para aislar al sensor.

Para ello hay que o bien retirar el CY7C63813 y la resistencia R2 o cortar las pistas a estos componentes. También hay conectar una resistencia pull-up de 10k entre +5v y NRESET para mantener el ADNS-5020 encendido. Los pins D5, D6 y D7 del Arduino se conectan a SCLK, SDIO y NCS respectivamente. EL circuito resultante debe quedar algo así:

El software consta de dos parte. El sketch que conecta con el sensor y manda las imágenes:

#include 

int SCLK = 5;
int SDIO = 6;
int NCS  = 7;

void setup() {
  Serial.begin(9600);
   
  pinMode(SCLK, OUTPUT);
  pinMode(SDIO, OUTPUT);
  pinMode(NCS, OUTPUT);
  
  mouse_reset();
  delay(10);
}

void loop() {
  char img[225];
  for (int i=0;i<225;i++){
      img[i]=readLoc(0x0b);
      img[i] &= 0x7F;
      img[i]+=1;
      Serial.print(img[i], DEC);
      Serial.print(",");
      delay(2);
  }  
  Serial.println();
  delay(500);
}

void mouse_reset(){
  // Initiate chip reset
  digitalWrite(NCS, LOW);
  pushbyte(0x3a);
  pushbyte(0x5a);
  digitalWrite(NCS, HIGH);
  delay(10);
  // Set 1000cpi resolution
  digitalWrite(NCS, LOW);
  pushbyte(0x0d);
  pushbyte(0x01);
  digitalWrite(NCS, HIGH);
}

unsigned int readLoc(uint8_t addr){
  unsigned int ret=0;
  digitalWrite(NCS, LOW);
  pushbyte(addr);
  ret=pullbyte();
  digitalWrite(NCS, HIGH);
  return(ret);
}

void pushbyte(uint8_t c){
  pinMode(SDIO, OUTPUT);
  for(unsigned int i=0x80;i;i=i>>1){
    digitalWrite(SCLK, LOW);
    digitalWrite(SDIO, c & i);
    digitalWrite(SCLK, HIGH);
  }
}

unsigned int pullbyte(){
  unsigned int ret=0;
  pinMode(SDIO, INPUT);
  for(unsigned int i=0x80; i>0; i>>=1) {
    digitalWrite(SCLK, LOW);
    ret |= i*digitalRead(SDIO);
    digitalWrite(SCLK, HIGH);
  }
  pinMode(SDIO, OUTPUT);
  return(ret);
}

Y el visor que recibe las imágenes por el puerto serie y las muestra en una ventana con PyQT5:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys, serial, threading
from PyQt5.QtWidgets import QApplication, QWidget, QLabel
from PyQt5.QtGui import QIcon, QPixmap
from PIL import Image, ImageDraw, ImageQt


class App(QWidget):
 
    def __init__(self):
      super().__init__()
      self.title = 'PyQt5 Mousecam ADNS 5020'
      self.left = 10
      self.top = 10
      self.width = 640
      self.height = 480
      self.initUI()
 
    def initUI(self):
      self.setWindowTitle(self.title)
      self.setGeometry(self.left, self.top, self.width, self.height)
      # Create widget
      self.label = QLabel(self)
      pixmap = QPixmap('fondo.jpg')
      self.label.setPixmap(pixmap)
      self.resize(pixmap.width(),pixmap.height())

      self.show()

    def mousecam(self):
      moo = serial.Serial('/dev/ttyUSB0', 9600)
      while True:
        msg = moo.readline().decode()
        print(msg)
        img = list(map(int,msg.strip()[:-1].split(',')))
        print(img)
        im=Image.new('RGB', (15,15), None)
        draw=ImageDraw.Draw(im)
        
        for i, value in enumerate(img):
          j = i // 15
          i = i - j * 15
          c = int(value * 2.4)
          
          draw.point((i,j),(c,c,c))
      
        im2 = im.resize((512,512))
        qim = ImageQt.ImageQt(im2)
        pix = QPixmap.fromImage(qim)
        self.label.setPixmap(pix)

if __name__ == '__main__':
  app = QApplication(sys.argv)
  ex = App()
  MouseCamThread = threading.Thread(target=ex.mousecam)
  MouseCamThread.daemon = True
  MouseCamThread.start()
  sys.exit(app.exec_())

Como resultado podemos ver las imágenes capturadas por el sensor. He probado con texto de 6 puntos y se diferencian las letras, pero seguro que puede hacerse algo con el contraste para que se vean mejor.


Existen sensores con mayor resolución y mejor fps para gaming como se puede ver en el catálogo del fabricante , es probable que con esos se consigan mejores resultados.

He subido el código y la documentación del sensor a un repositorio en Github por si alguien quiere experimentar y mejorar lo hecho hasta ahora.