Departamento de Ciência da Computação - IME - USP

MAC0122 Princípios de Desenvolvimento de Algoritmos

Segundo Semestre de 2012

Segundo Exercício Programa     

 

“As soon as we started programming, we found to our surprise that
it wasn’t as easy to get programs right as we had thought. Debugging had to be discovered.
I can remember the exact instant when I realized that a large part
of my life from then on was going to be spent in finding mistakes in my own programs.”

Maurice Wilkes discovers debugging, 1949

 

O objetivo desse segundo exercício-programa (EP) é treinar Recursão, Registros e structs, Alocação dinâmica de memória e Listas encadeadas.

Para isso, neste EP vocês escreverão um programa que de um arquivo de configuração que contém uma representação de uma imagem formada por "arranjo" de linhas verticais e horizontais. A partir dessa representação o programa

  1. cria uma "imagem" desse arranjo,
  2. segmenta essa imagem em "regiões conexas" usando uma função recursiva.
  3. armazena os "pixels" que formam cada região em uma lista ligada com cabeça.
Finalmente, o programa atribui cores a cada uma das regiões.


Mondrian

Resumo retirado da Wikipedia

Pieter Cornelis "Piet" Mondrian foi um pintor holandês.

Ele foi um importante contribuinte para o movimento artístico De Stijl, fundado por Theo van Doesburg. Ele desenvolveu uma forma não-representacional que ele chamou de neoplasticismo. Sua técnica consistia em pintar, sobre um fundo branco, uma grade de linhas verticais e horizontais pretas e as três cores primárias.

Para saber mais e ver alguns de seus quadros clique aqui.


Processamento de imagens

Para desenvolver o EP precisamos representar imagens no computador. Isto é feito através das chamadas imagens digitais.

Uma imagem digital ou simplesmente imagem é basicamente uma matriz, com, digamos, altura nL (número de Linhas) e largura nC (número de Colunas). Cada elemento da matriz é chamada de pixel (picture element), que possui uma "cor".

Na sua forma mais básica podemos representar um pixel como aceso ("branco"), ou apagado ("preto"). As imagens em que cada pixel possui apenas um dentre dois "níveis" possíveis são chamadas de imagens binárias. Imagens binárias podem ser representadas de maneira bastante compacta, basta reservarmos apenas 1 bit (= dígito binário) por pixel. Desta forma podemos representar até 8 pixels em cada byte do computador.

Em geral, no entanto, dois nívels são insuficientes para representar o que costumamos chamar de imagens em preto e branco, pois estas, em geral, possuem vários níveis ou tons de cinza. Uma forma comum de representar uma imagem com vários tons de cinza é reservando um byte (8 bits) para cada pixel. Desta forma podemos representar imagens com até 256 níveis de cinza por pixel. Chamamos as imagens em que cada pixel pode ter vários tons de cinza de imagens com nível de cinza.

Já uma imagem colorida requer ainda mais informação para cada pixel. A representação mais comum é obtida decompondo uma cor nas componentes básicas 'R' (red - vermelho), 'G' (green - verde) e 'B' (blue - azul). Vermelho, verde e azul são chamadas de cores primárias, já que com combinações dessas cores podemos representarmos um espéctro grande de cores. Para a maioria das aplicações essas três cores são suficientes, pois a visão humana é tricromática.

Para simplificar a representação de imagens nesse exercício programa e torná-la independente de plataforma, dispositivo e formato, decidimos considerar a seguinte definição para uma imagem:

typedef struct {
   int nL;         /* numero de linhas da imagem */
   int nC;         /* numero de colunas da imagem */
   float **pixel;  /* matriz com a imagem */
} Imagem;
onde nL e nC correspondem ao número de linhas e colunas de pixels da imagem, e pixel é um ponteiro para a matriz que contem as "cores" (níveis de cinza) dos pixels. O tipo float foi adotado para facilitar a manipulação das imagens.

 


Descrição do programa

 

Neste EP você escreverá um programa em C que cria imagens "Mondrian", ou simplesmente Mondrian, como saída. Dividiremos a sua tarefa nas seguintes partes:
  1. Manipulação de imagens;
  2. Desenho de linhas;
  3. Segmentação de regiões;
  4. Pintura colorida; e
  5. Programa principal.
Inicialmente copie o esqueleto disponibilizado.

Manipulação de imagens

Você deve utilizar, obrigatoriamente, o registro Imagem definido anteriormente, para implementar as funções com os sequintes protótipos:

Desenho de linhas

Considere a seguinte declaração de constantes e de um registro que será utilizado para representar uma linha/segmento horizontal ou vertical:
#define TIPO_HORIZONTAL 'H'
#define TIPO_VERTICAL   'V'

struct lineStruct {
  char tipo;          /* pode ser 'H'=horizontal ou 'V'=vertical */
  int pos, ini, fim;  /* define as posicoes dos pixels nos extremos da linha */
  struct lineStruct *prox;
};
typedef struct lineStruct Linha;
O campo tipo contém um caractere que indica o tipo da linha, 'H' para horizontal e 'V' para vertical. Os campo pos, ini e fim indicam índices de linhas ou colunas de uma imagem dependendo do conteúdo do campo tipo:

Você deve utilizar, obrigatoriamente, os registros Imagem e Linha definidos anteriormente, para implementar as funções de desenho a seguir.

Segmentação das regiões

Apesar de existirem outras soluções possíveis, nesse trabalho vamos implementar um algoritmo recursivo para segmentar as regiões da imagem. Para isso você deve implementar as funções descritas a seguir. Na descrição das funções fazemos referência às seguintes constantes
  #define COR_FUNDO 1.0
  #define COR_BORDA 0.0

Pintura com uma cor RGB

As funções definidas anteriormente para segmentação e desenho supõem que a imagem tem níveis de cinza entre 0.0 (0.0 = COR_BORDA) e 1.0 (1.0 = COR_FUNDO). Para colorir o Mondrian, vamos utilizar a representação de cor RGB onde uma cor é definida pela combinação das cores primárias R, G e B. Assim, a combinação RGB = (1.0, 0.0, 0.0) equivale a 100% de R e 0% de G e B e portanto o pixel é vermelho. O amarelo pode ser obtido pela combinação de R e G como (1.0, 1.0, 0.0), o preto como (0.0, 0.0, 0.0) e o branco como (1.0, 1.0, 1.0).

Ao invés de criarmos um novo registro para cada pixel de uma imagem RGB, utilizaremos uma imagem com nível de cinza para representar a intensidade ou ton de cada uma das cores primárias. Assim, teremos uma imagem com nível de cinza para R, outra para G e outra para B. Cada uma dessas imagens é chamada de um canal de cor.

Escreva uma função:

Programa principal (O que você deve fazer)

Você deve desenvolver um programa que cria um Mondrian a partir de um arquivo de configuração e salva o resultado em um arquivo ppm (portable pixmap format). O nome dos arquivos devem ser recebidos pela linha de comando da seguinte maneira:

Para usar esse programa digite:
ep2 <nomeArquivoDeEntrada> <nomeDaImagemDeSaida>

Exemplo:
ep2 entrada.txt saidaRGB

O arquivo de configuração determina o tamanho da imagem e o arranjo das linhas verticais e horizontais que compõem o Mondrian. A função leMondrian que faz a leitura desse arquivo é fornecida no esqueleto (ou seja, você não precisa implementá-la).

Após carregar as linhas, seu programa deve criar uma imagem onde as linhas devem ser desenhadas e uma borda deve ser desenhada ao redor da imagem.

Utilize a função segmentaRegioes para determinar o número de regiões e obter o agrupamento de pixels de cada região. Seu programa deve imprimir, para cada agrupamento conexo de pixels, o número de pixels contidos no agrupamento.

Deixamos a seu critério a forma para colorir cada região. Uma tabela com cores é fornecida mais adiante, mas você pode utilizar outras cores se desejar, ou utilizar apenas algumas delas (como só R, G e B, além do preto e branco). O seu programa, no entanto, precisa utilizar obrigatoriamente a função pintaRegiao e gerar uma imagem colorida RGB (na forma de 3 canais de nível de cinza). Por exemplo, você pode sortear uma cor da tabela para cada região.

A imagem RGB gerada deve ser salva utilizando a função salvaImagemRBG, também fornecida no esqueleto.


Funções fornecidas e que você não precisa implementar

O esqueleto fornecido contém as seguintes funções que você deve utilizar em seu programa:

Protótipos das funções que você deve implementar

 

float getPixel(Imagem *img, int x, int y);

void  setPixel(Imagem *img, int x, int y, float cor);

void  pintaImagem(Imagem *img, float cor);

void  copiaImagem (Imagem *destino, Imagem *origem);

Imagem *criaImagem(int nLins, int nCols);

void desenhaLinha(Imagem *img, Linha *lin, float cor);

void desenhaBorda(Imagem *img, float cor);

int  juntaPixels(Imagem *img, int x, int y, float corNova, celPixel *cabeca);

int  segmentaRegioes(Imagem *img, celRegiao cabecas[MAX_REGIOES]);

void pintaRegiao(celPixel *cab, Imagem *R, Imagem *G, Imagem *B, float cor[3]);

Exemplo de execução do programa

Arquivos de configuração de um Mondrian têm o seguinte formato: Desta forma, para o arquivo de configuração entrada.txt
R 480 640
V 170    0  479
V 430    0  479
V 320    0  200
V 250  200  350
V 350  200  350
V 300  350  479
H 180  430  639
H 300  430  639
H 200    0  430
H 350  170  430
a função leMondrian produz as mensagens:
Arquivo de entrada com a definicao do desenho: entrada.txt
Resolucao da Imagem: 480 linhas x 640 colunas 
Linha   1: Tipo V com pos =  170, inicio =    0 e fim  479
Linha   2: Tipo V com pos =  430, inicio =    0 e fim  479
Linha   3: Tipo V com pos =  320, inicio =    0 e fim  200
Linha   4: Tipo V com pos =  250, inicio =  200 e fim  350
Linha   5: Tipo V com pos =  350, inicio =  200 e fim  350
Linha   6: Tipo V com pos =  300, inicio =  350 e fim  479
Linha   7: Tipo H com pos =  180, inicio =  430 e fim  639
Linha   8: Tipo H com pos =  300, inicio =  430 e fim  639
Linha   9: Tipo H com pos =  200, inicio =    0 e fim  430
Linha  10: Tipo H com pos =  350, inicio =  170 e fim  430

A função leMondrian devolve essas 10 linhas em uma lista ligada com cabeça. O seu programa precisa apenas chamar essa função e percorrer a lista para desenhar cada linha.

Assim o seu programa deve gerar a seguinte imagem (observe que a origem do desenho fica no canto inferior esquerdo)

Linhas do Mondrian

A seguir o seu programa deve processar a imagem e segmentá-la em regiões, produzindo uma lista de regiões como a seguinte:

Grupos de pixeis conexos:
Grupo   0 tem  33631 pixels
Grupo   1 tem  29651 pixels
Grupo   2 tem  21691 pixels
Grupo   3 tem  37232 pixels
Grupo   4 tem  24752 pixels
Grupo   5 tem  46982 pixels
Grupo   6 tem  11771 pixels
Grupo   7 tem  14751 pixels
Grupo   8 tem  11771 pixels
Grupo   9 tem  37024 pixels
Grupo  10 tem  16512 pixels
Grupo  11 tem  16512 pixels

Por fim, seu programa deve associar uma cor a cada região, como por exemplo:

Mondrian colorido

No programa executável disponibilizado, uma cor é associada a cada região assim que a região é determinada, seguindo a ordem dada pela tabela abaixo:
float CORES[12][3]=
{
  {1.0, 0.0, 0.0}, /*  0 red     */
  {0.0, 1.0, 0.0}, /*  1 green   */
  {0.0, 0.0, 1.0}, /*  2 blue    */
  {1.0, 1.0, 0.0}, /*  3 yellow  */
  {1.0, 0.0, 1.0}, /*  4 magenta */
  {0.0, 0.0, 0.0}, /*  5 black   */
  {0.2, 0.7, 0.4}, /*  6 green 2 */
  {0.7, 0.4, 0.2}, /*  7 marrom  */
  {0.0, 1.0, 1.0}, /*  8 cyan    */
  {0.5, 0.5, 0.5}, /*  9 cinza   */
  {1.0, 1.0, 1.0}, /* 10 branco  */
  {0.0, 0.0, 0.0}  /* 11 preto   */
};

O seu programa pode utilizar cores diferentes e utilizar outros esquemas para associar uma cor a uma região.

Teste seu programa (sua função) com arquivos de entrada diferentes (invente os seus), assim como distribuição de cores diferentes. Inicialmente teste com imagens bem pequenas e desenhos bem simples.

O executável mondrian resolve o problema. Compare os resultados obtidos pelo seu programa com o do executável fornecido.

Observações


Last modified: Fri Aug 24 12:15:34 BRT 2012