Un principiante en las Artes Gráficas Una experiencia en un sector que nos envuelve… El blog personal de David Pinelo.

30Mar/100

Programando con Qt: Creando un widget sobre el que poder dibujar

Si algo distingue este blog, es el CAOS. Lo mismo hablo de AbanQ, que Qt, que Asterisk, que de la crisis, o me llevo dos meses sin escribir para de nuevo hacerlo sobre redes neuronales artificiales. Supongo que aquí plasmo un poco la realidad de mi día a día: Me enfrento a múltiples y diversas tecnologías para resolver múltiples y diversos problemas... pero la tecnología es sólo una pequeña parte de mi trabajo.

Y dentro de las líneas de caos, voy a dedicarme hoy a hablar un poquito de las Qt, mostrando un ejemplo; vamos a crear un widget muy sencillito: En él, y con el ratón pulsado, podremos dibujar, al más puro estilo Paint. La idea es utilizar este widget para capturar trazos de letras. Es decir, el usuario escribirá en él una letra. El widget esperará un tiempo prudencial, y avisará al código principal de que ya tiene la letra capturada. Este código está sacado de mi proyecto QtHandwriteRecog.

Ojo, la versión que pongo aquí NO ES LA IDEAL utilizando las posibilidades gráficas de Qt. Es una BURDA aproximación, pero que permite introducirse en algunos aspectos de las Qt sin llegar a un nivel de complejidad excesiva. Los que os estéis iniciando en las Qt lo entenderéis. Los expertos lo detestaréis. Pero a los primeros, os ayudará a ser los segundos, y detestarlo en un futuro. ;-)

El widget que crearemos será una extensión de QLabel. Es decir, dibujaremos en un QLabel. La razón, es la habilidad de este último para mostrar imágenes (QPixmap) de una forma muy sencilla. Lo mejor es ver la definición del widget e ir comentado particularidades

/***************************************************************************
 copyright            : (C) 2009 by David Pinelo
 email                : adpinelo@yahoo.es
***************************************************************************/
/***************************************************************************
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; version 2 of the License.               *
 ***************************************************************************/
/***************************************************************************
 Este  programa es software libre. Puede redistribuirlo y/o modificarlo
 bajo  los  trminos  de  la  Licencia  Pblica General de GNU   en  su
 versin 2, publicada  por  la  Free  Software Foundation.
 ***************************************************************************/
#ifndef PAINTWIDGET_H
#define PAINTWIDGET_H

#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QTimer>
#include <QDebug>

#define DELAY_BETWEEN_TRACES    500
#define IMG_WRITE_X    200
#define IMG_WRITE_Y    250
#define TRACE_WIDTH 5

class PaintWidget : public QLabel
{
Q_OBJECT
private:
 /** Buffer intermedio que contendrá una copia de trabajo de la imágen dibujada. */
 QImage *m_img;
 /** Chivato que indica si se está dibujando */
 bool m_draw;
 bool m_clear;
 /** Para conseguir un trazo continuo, guardamos la posicion
     del ultimo punto dibujado en el evento */
 QPoint m_lastPoint;
 /** Tras escribir un trazo, esperamos un tiempo prudencial por si
     se escribe otro trazo. Caso de no escribirse, se lanza la senial
     de trazo finalizado */
 QTimer *m_timer;
 /** Resolucion de la imagen */
 int m_xRes;
 /** Resolucion de la imagen */
 int m_yRes;

 void viewImage();
 void paintPixel ( const QPoint &pos );
 void drawLine ( const QPoint &orig, const QPoint &dest );
 QPoint scaledPoint ( const QPoint & point );
 void createBufferImg();
 void normalizeImage( QImage &imgNormalized );
 void deleteWhiteSpaces ( QImage &image );

public:
 PaintWidget(QWidget *parent = 0);
 ~ PaintWidget();

 void getNormImage( QImage &image );

 int getXRes();
 int getYRes();
 void setXRes(int x);
 void setYRes(int y);

signals:
 void traceFinish();

public slots:
 void clear();

private slots:
 void isTraceFinish();

protected:
 void mousePressEvent ( QMouseEvent * event );
 void mouseMoveEvent ( QMouseEvent * event );
 void mouseReleaseEvent ( QMouseEvent * event );
};

#endif // PAINTWIDGET_H

La secuencia de eventos para que el usuario escriba un trazo es sencilla:

  1. Sitúa el ratón encima del widget.
  2. Pulsa y deja pulsado el botón izquierdo del ratón todo el tiempo que quiera dibujar.
  3. Mueve el ratón por el widget.
  4. Se escribe el trazo.
  5. Suelta el ratón, y pasado un tiempo el widget indica que el trazo está finalizado. Este tiempo se establecer por
DELAY_BETWEEN_TRACES

Al realizar un trazo, encontramos algunas dificultades y presento algunas consideraciones:

  • Una de ellas es que el trazo es discontínuo por la "coordinación" entre el tiempo de refresco en pantalla y los eventos de captura del ratón. En este widget implementaremos un truco para conseguir un trazo contínuo: Entre evento y evento capturado de movimiento del ratón sobre la imágen, dibujaremos una línea que una los puntos que se pintan con cada evento. Esto proporcionará continuidad al trazo.
  • Para no hacer un trazo muy fino, todo el dibujo se realizará con un pincel mayor.
  • Para simplificar la lectura de la imagen y tratamiento de la misma, asocio al widget un "buffer" intermedio, una imagen a través del objeto QImage.

Con esto, la implementación en código de este widget sería como sigue:

#include "paintwidget.h"

PaintWidget::PaintWidget(QWidget *parent) : m_img(NULL),
    QLabel(parent)
{
    m_draw = false;
    m_clear = true;

    createBufferImg();

    m_timer = new QTimer(this);
    m_timer->setInterval(DELAY_BETWEEN_TRACES);

    connect(m_timer, SIGNAL(timeout()), this, SLOT(isTraceFinish()));
    viewImage();
}

PaintWidget::~PaintWidget()
{
    delete m_img;
    delete m_timer;
}

void PaintWidget::mousePressEvent ( QMouseEvent * event )
{
    /* Acciones cuando se pulsa el botón:
    -Se activa el chivato de dibujando.
    -Se para el timer que controla si el usuario ha dejado de pulsar
    -Se dibuja el primer punto en la pantalla
    */
    m_draw = true;
    m_timer->stop();
    paintPixel ( event->pos() );
    m_clear = true;
}

void PaintWidget::mouseReleaseEvent( QMouseEvent * event )
{
    /* Al soltar el botón: Se indica que se ha dejado de dibujar. Se inicia el conteo
    de tiempo para saber si el usuario ha terminado de escribir, o es una pequeña pausa
    para "ponerle el punto a la i", por ejemplo */
    m_draw = false;
    m_timer->start();
}

void PaintWidget::mouseMoveEvent( QMouseEvent *event )
{
    if ( !m_draw )
        return;

    /* Al mover el ratón, con el botón pulsado, se pinta los píxeles, y el trazo contínuo */
    paintPixel ( event->pos() );
    // Esto dara continuidad al trazo
    drawLine ( m_lastPoint, event->pos() );
    m_lastPoint = event->pos();
}

void PaintWidget::viewImage()
{
    this->setPixmap(QPixmap::fromImage(*m_img));
}

void PaintWidget::paintPixel ( const QPoint &pos )
{
	// Para que no haya problemas con el escalado, hacemos el trazo algo más grueso
	QPainter paint;
	paint.begin( m_img );
	paint.setBrush( Qt::black );
	paint.drawEllipse(pos, TRACE_WIDTH/2, TRACE_WIDTH/2);
	paint.end();
	viewImage();

}

void PaintWidget::drawLine ( const QPoint &orig, const QPoint &dest )
{
    // Esta funcion da algo de continuidad al trazo
    QPainter paint;
    if ( !orig.isNull() ) {
        paint.begin( m_img );
	for ( int i = 0 ; i < ( TRACE_WIDTH / 2 ) ; i++ ) {
             QPoint pO1 ( orig.x() - i, orig.y() - i );
             QPoint pD1 ( dest.x() - i, dest.y() - i );
             paint.drawLine( pO1, pD1 );
             QPoint pO2( orig.x() + i, orig.y() + i );
             QPoint pD2( dest.x() + i, dest.y() + i );
             paint.drawLine( pO2, pD2 );
        }
        paint.end();
    }
 } 

void PaintWidget::isTraceFinish()
{
    if ( !m_draw && m_clear ) {
        m_timer->stop();
        emit traceFinish();
    }
}

void PaintWidget::getNormImage( QImage ℑ )
{
	image = *m_img;
	normalizeImage ( image );
}

void PaintWidget::clear()
{
    QPoint pnull;
    m_img->fill(1);
    m_clear = true;
    m_lastPoint = pnull;
    viewImage();
}

int PaintWidget::getXRes()
{
	return m_xRes;
}

int PaintWidget::getYRes()
{
	return m_yRes;
}

void PaintWidget::setXRes(int x)
{
	m_xRes = x;
}

void PaintWidget::setYRes(int y)
{
	m_yRes = y;
}

/** \fn PaintWidget::createBufferImg()
  Crea el buffer de imagen intermedio que almacena la imagen
  */
void PaintWidget::createBufferImg()
{
	if ( m_img != NULL ) {
		delete m_img;
	}
	// Solo dos colores: Blanco y negro
	m_img = new QImage(IMG_WRITE_X, IMG_WRITE_Y, QImage::Format_Mono);
	m_img->fill(1);
}

/** \fn PaintWidget::normalizeImage( QImage &imgNormalized )
  Esta función elimina los espacios en blanco alrededor del carácter escrito, y escala
  la imagen a la resolución necesaria
  */
void PaintWidget::normalizeImage( QImage &imgNormalized )
{
	deleteWhiteSpaces ( imgNormalized );
	imgNormalized = imgNormalized.scaled(QSize( m_xRes, m_yRes ));
}

/** \fn PaintWidget::deleteWhiteSpace ( QImage ℑ )
  Esta función elimina los espacios en blanco alrededor del carácter escrito
  */
void PaintWidget::deleteWhiteSpaces ( QImage ℑ )
{
	// Recorremos la imagen, primero para encontrar la primera fila con el primer punto no blanco
	// Después la recorremos en sentido inverso para encontrar la última fila con el último punto no blanco
	// Se hace lo mismo, pero por columnas para la columna derecha e izquierda
	int firstRow = 0, lastRow = image.size().height(), firstCol = 0, lastCol = image.size().width();

	for ( int row = 0 ; row < image.size().height() ; row ++ ) {
		if ( firstRow == 0 ) {
			for ( int col = 0 ; col < image.size().width() ; col ++ ) {
                                if ( image.pixelIndex( col, row ) == 0 ) {
                                        firstRow = row;
                                        if ( firstRow > 0 )
						firstRow --;
					break;
				}
			}
		}
	}
	for ( int row = image.size().height() - 1 ; row > 0 ; row -- ) {
		if ( lastRow == image.size().height() ) {
			for ( int col = 0 ; col < image.size().width() ; col ++ ) {
				if ( image.pixelIndex( col, row ) == 0 ) {
					lastRow = row;
					if ( lastRow < image.size().height() - 1 )
						lastRow++;
					break;
				}
			}
		}
	}
	for ( int col = 0 ; col < image.size().width() ; col++ ) {
		if ( firstCol == 0 ) {
			for ( int row = 0 ; row < image.size().height() ; row ++ ) {
                              if ( image.pixelIndex( col, row ) == 0 ) {
                                    firstCol = col;
                                    if ( firstCol == 0 )
                                         firstCol--;
                                    break;
                              }
                       }
                }
        }
        for ( int col = image.size().width() - 1; col > 0  ; col-- ) {
		if ( lastCol == image.size().width() ) {
			for ( int row = 0 ; row < image.size().height() ; row ++ ) {
				if ( image.pixelIndex( col, row ) == 0 ) {
					lastCol = col;
					if ( lastCol < image.size().width() - 1 )
						lastCol++;
					break;
				}
			}
		}
	}
	image = image.copy ( firstCol, firstRow, (lastCol - firstCol), (lastRow - firstRow));

}

Básicamente, este sería el código. La imagen se normaliza, esto es, una cosa es el tamaño al que se dibuja y otra cómo se quiere obtener la imagen para tratarla (en el caso del programa para el que se escribió este widget, para que la imagen sirva de entrada a una red neuronal).

 /** Para conseguir un trazo continuo, guardamos la posicion
     del ultimo punto dibujado en el evento */
Comentarios (0) Trackbacks (0)

Sin comentarios por ahora.


Deja un comentario

Debes iniciar sesión para dejar un comentario.

Sin trackbacks por el momento.