Rotate PPM image by 90 degrees in C

Asked

Viewed 3,048 times

3

Well, I’m trying to spin 90° an image in PPM Nxm format in C language and I’m not finding a way to do that. The program reads the image of an external file and ends up creating another, both in PPM format. Previous data are stored in local variables and image pixels in a two-dimensional matrix of structs with 3 values. Below follows an example of the code.

 #include <stdio.h>
#include <stdlib.h>

typedef struct imagem IMAGEM;

struct imagem{
    int r,g,b;
};

IMAGEM **M;

int main(){
    FILE *arq;
    FILE *arq2;
    arq = fopen("imagem.ppm","r");
    int g, h, i, j, maxCor;
    char string[6];
    fscanf(arq,"%s",string);
    fscanf(arq,"%d",&g);
    fscanf(arq,"%d",&h);
    fscanf(arq,"%d",&maxCor);
    if (g >= h){
        M = malloc(g * sizeof(IMAGEM));
        for (i=0;i<g;i++)
            M[i] = malloc(g * sizeof(IMAGEM));
    } else {
        M = malloc(h * sizeof(IMAGEM));
        for (i=0;i<g;i++)
            M[i] = malloc(h * sizeof(IMAGEM));
    }

    for (i=0;i<g;i++){
        for(j=0;j<h;j++){
            fscanf(arq,"%d",&M[i][j].r);
            fscanf(arq,"%d",&M[i][j].g);
            fscanf(arq,"%d",&M[i][j].b);
        }
    }
    fclose(arq);
    arq2 = fopen("imagemsaida.ppm","w");
    fprintf(arq2,"%s\n",string);
    fprintf(arq2,"%d\n",g);
    fprintf(arq2,"%d\n",h); 
    fprintf(arq2,"%d\n",maxCor);
    for(i=0;i<g;i++){
        for(j=0;j<h;j++){
            fprintf(arq2,"%d\n",M[i][j].r);
            fprintf(arq2,"%d\n",M[i][j].g);
            fprintf(arq2,"%d\n",M[i][j].b);
        }
    }
    fclose(arq2);
    return 0;

}
  • In any case your matrix M represents a square (gg or hh). You have to do the first malloc with g (or h) and the second with h (or g).

1 answer

3


First, this struct that you called imagem and IMAGEM, actually it’s just a pixel. So let’s rename it:

typedef struct pixel PIXEL;

struct pixel {
    int r, g, b;
};

Better yet, match the struct and the typedef in one thing:

typedef struct PIXEL {
    int r, g, b;
} PIXEL;

Second, based on that, let’s create another struct image to better organize our data:

typedef struct IMAGEM {
    int largura;
    int altura;
    int maxcor;
    PIXEL *pixels;
} IMAGEM;

Third, to have all logic within the main is not very good as the code gets confused. The solution to this is to modularize your code by dividing it into functions.

So let’s separate a function to create an image:

IMAGEM *nova_imagem(int largura, int altura, int maxcor) {
    IMAGEM *imagem = (IMAGEM *) malloc(sizeof(IMAGEM));
    imagem->pixels = (PIXEL *) malloc(largura * altura * sizeof(PIXEL));
    imagem->largura = largura;
    imagem->altura = altura;
    imagem->maxcor = maxcor;
    return imagem;
}

Note that the created image is always sized largura * altura. Compare this to your previous code:

if (g >= h){
    M = malloc(g * sizeof(IMAGEM));
    for (i=0;i<g;i++)
        M[i] = malloc(g * sizeof(IMAGEM));
} else {
    M = malloc(h * sizeof(IMAGEM));
    for (i=0;i<g;i++)
        M[i] = malloc(h * sizeof(IMAGEM));
}

Note that in your previous code, it will create the image with the size h * h or g * g, being that you wanted or g * h or h * g. But as the two ties from there go from 0 to 0 g - 1 you will reserve an unnecessary area in case of else. Also, you are allocating a lot of memory, row by row and putting on several different pointers, when it is simpler and more efficient to put everything in one block only, just as my function above does.

Well, continuing with our program, we have to have a function that frees the memory of the created image:

void destruir_imagem(IMAGEM *imagem) {
    free(imagem->pixels);
    free(imagem);
}

In addition, we need a way to access the image pixels for both reading their values and writing. This is equivalent to your &M[i][j], but as we made a single allocation for all pixels, we have to use something smarter:

PIXEL *pixel_da_imagem(IMAGEM *imagem, int x, int y) {
    return &(imagem->pixels[y * imagem->largura + x]);
}

This formula y * imagem->largura + x it may seem strange, but it is because each line corresponds to a block of largura pixels, all lines being stored contiguously in memory in a single block. So, each position that varies in the y causes a variation of largura positions inside the block.

The advantage of this method is that we don’t need to allocate each line one by one in the loop for, which simplifies image allocation and misallocation, optimizes memory usage and also performance by ensuring that all image pixels are in the same memory region. The downside is that the formula for accessing pixels gets a little bit more complicated, but just a little bit.

With this, now let’s create a function to read the figure data of a file:

IMAGEM *ler_arquivo_ppm_p3(const char *nome_arquivo) {
    FILE *arq = fopen(nome_arquivo, "r");
    if (arq == NULL) return NULL;
    int largura, altura, maxcor, x, y;
    IMAGEM *imagem = NULL;
    char formato[6];
    fgets(formato, 6, arq);
    if (strcmp("P3\n", formato) == 0) {
        fscanf(arq, "%d", &largura);
        fscanf(arq, "%d", &altura);
        fscanf(arq, "%d", &maxcor);
        imagem = nova_imagem(largura, altura, maxcor);
        for (y = 0; y < altura; y++) {
            for (x = 0; x < largura; x++) {
                PIXEL *p = pixel_da_imagem(imagem, x, y);
                fscanf(arq, "%d", &(p->r));
                fscanf(arq, "%d", &(p->g));
                fscanf(arq, "%d", &(p->b));
            }
        }
    }
    fclose(arq);
    return imagem;
}

Note that I am using the fgets to read the format. The reason is that and so I do not run the risk of having a buffer overflow when reading the image format.

Also this function accepts files only in P3 sub-format of PPM format (no comment lines initiated by #), which I believe is the format you want. It checks if the file header starts with P3 and rejects the file if this is not the case, returning NULL. NULL is also returned if the file cannot be opened for some reason (the most common case is when the file does not exist).

However, the function does not accept comments in the file (lines started by #) and probably something bad will happen if she reads an incomplete, truncated, defective or poorly formatted file that still has the header "P3". Tidying up these details takes a little extra work, but it’s not that hard either. Idem to modify the function to accept images in other formats.

And now the function that writes the file:

void salvar_arquivo_ppm_p3(const char *nome_arquivo, IMAGEM *imagem) {
    FILE *arq = fopen(nome_arquivo, "w");
    int x, y;
    fprintf(arq, "P3\n%d %d\n%d", imagem->largura, imagem->altura, imagem->maxcor);
    for (y = 0; y < imagem->altura; y++) {
        for (x = 0; x < imagem->largura; x++) {
            PIXEL *p = pixel_da_imagem(imagem, x, y);
            fprintf(arq, "\n%d %d %d", p->r, p->g, p->b);
        }
    }
    fclose(arq);
}

Nothing too surprising here. But there are two important differences from your code. The first is the \n and the header space you forgot between the format name, the width, the height and the maxcor image. Without these characters, the generated file would be poorly formed.

The second difference is more cosmetic, I just matched some of your fprintffollowed in a smaller number of fprintfs.

Done this, now comes the legal part. Rotate the figure by 90 degrees. I could use a process where the original image is changed or where a copy with the changes is made and the original image is preserved. I find the second best alternative:

IMAGEM *rotacionar_90_graus_direita(IMAGEM *original) {
    int x, y;
    int h = original->altura, w = original->largura;
    IMAGEM *rotacionada = nova_imagem(h, w, original->maxcor);
    for (y = 0; y < h; y++) {
        for (x = 0; x < w; x++) {
            PIXEL *p1 = pixel_da_imagem(original, x, y);
            PIXEL *p2 = pixel_da_imagem(rotacionada, h - y - 1, x);
            p2->r = p1->r;
            p2->g = p1->g;
            p2->b = p1->b;
        }
    }
    return rotacionada;
}

Here it should be noted that to create the rotated image, I reversed the parameters of height and width. This is unintentional, since the rotated image has the height, width of the original and vice versa. The maxcol the rotated image is the same as the original.

The pixels of the rotated image are defined by means of the transformed/linear function T(x, y) = (y, -x), which is the linear right rotation transform at 90 degrees. Negative values are shifted so as not to leave the appropriate range of positions, so that -x becomes w - x - 1. Likewise -y would become h - y - 1. So here are the interesting transformation functions:

  • t(x, y) = (y, w - x - 1): Rotation 90 degrees left (counterclockwise).
  • t(x, y) = (h - y - 1, x): Rotation 90 degrees right (clockwise).
  • t(x, y) = (w - x - 1, h - y - 1): Rotation in 180 degrees.
  • t(x, y) = (w - x - 1, y): Horizontal reflection.
  • t(x, y) = (x, h - y - 1): Upright reflection.
  • t(x, y) = (y, x): Reflection on the main diagonal.
  • t(x, y) = (h - y - 1, w - x - 1): Reflection on the secondary diagonal.
  • t(x, y) = (x, y): No change is made.

Other types of transforms (shear, deformation, widening, trapezoid projections, cylindrical projections, rotations at arbitrary angles, etc.) require more complicated formulas.

Also note that pixel values are simply copied. But if you do something other than just copy the pixel values (especially if you also look at neighboring pixels or the statistical distribution of pixels in the image), you can make effects such as reversing colors, converting to black-and-white, highlighting colors, do color filtering, recoloring, noise elimination, brightness and contrast adjustment, edge detection, targeting and object recognition, etc.

Now you just need to combine all this to make your program to rotate images:

int main() {
    IMAGEM *original = ler_arquivo_ppm_p3("imagem.ppm");
    if (original == NULL) {
        printf("Arquivo nao eh PPM P3 ou nao existe");
        return 1;
    }
    IMAGEM *rotacionada = rotacionar_90_graus_direita(original);
    salvar_arquivo_ppm_p3("imagemsaida.ppm", rotacionada);
    destruir_imagem(original);
    destruir_imagem(rotacionada);
    return 0;
}

Look how cool! Yours main became extremely simple and intuitive to understand!

Put it all together, look how your code gets:

#include <stdio.h>
#include <stdlib.h>

typedef struct PIXEL {
    int r, g, b;
} PIXEL;

typedef struct IMAGEM {
    int largura;
    int altura;
    int maxcor;
    PIXEL *pixels;
} IMAGEM;

IMAGEM *nova_imagem(int largura, int altura, int maxcor) {
    IMAGEM *imagem = (IMAGEM *) malloc(sizeof(IMAGEM));
    imagem->pixels = (PIXEL *) malloc(largura * altura * sizeof(PIXEL));
    imagem->largura = largura;
    imagem->altura = altura;
    imagem->maxcor = maxcor;
    return imagem;
}

void destruir_imagem(IMAGEM *imagem) {
    free(imagem->pixels);
    free(imagem);
}

PIXEL *pixel_da_imagem(IMAGEM *imagem, int x, int y) {
    return &(imagem->pixels[y * imagem->largura + x]);
}

IMAGEM *ler_arquivo_ppm_p3(const char *nome_arquivo) {
    FILE *arq = fopen(nome_arquivo, "r");
    if (arq == NULL) return NULL;
    int largura, altura, maxcor, x, y;
    IMAGEM *imagem = NULL;
    char formato[6];
    fgets(formato, 6, arq);
    if (strcmp("P3\n", formato) == 0) {
        fscanf(arq, "%d", &largura);
        fscanf(arq, "%d", &altura);
        fscanf(arq, "%d", &maxcor);
        imagem = nova_imagem(largura, altura, maxcor);
        for (y = 0; y < altura; y++) {
            for (x = 0; x < largura; x++) {
                PIXEL *p = pixel_da_imagem(imagem, x, y);
                fscanf(arq, "%d", &(p->r));
                fscanf(arq, "%d", &(p->g));
                fscanf(arq, "%d", &(p->b));
            }
        }
    }
    fclose(arq);
    return imagem;
}

void salvar_arquivo_ppm_p3(const char *nome_arquivo, IMAGEM *imagem) {
    FILE *arq = fopen(nome_arquivo, "w");
    int x, y;
    fprintf(arq, "P3\n%d %d\n%d", imagem->largura, imagem->altura, imagem->maxcor);
    for (y = 0; y < imagem->altura; y++) {
        for (x = 0; x < imagem->largura; x++) {
            PIXEL *p = pixel_da_imagem(imagem, x, y);
            fprintf(arq, "\n%d %d %d", p->r, p->g, p->b);
        }
    }
    fclose(arq);
}

IMAGEM *rotacionar_90_graus_direita(IMAGEM *original) {
    int x, y;
    int h = original->altura, w = original->largura;
    IMAGEM *rotacionada = nova_imagem(h, w, original->maxcor);
    for (y = 0; y < h; y++) {
        for (x = 0; x < w; x++) {
            PIXEL *p1 = pixel_da_imagem(original, x, y);
            PIXEL *p2 = pixel_da_imagem(rotacionada, h - y - 1, x);
            p2->r = p1->r;
            p2->g = p1->g;
            p2->b = p1->b;
        }
    }
    return rotacionada;
}

int main() {
    IMAGEM *original = ler_arquivo_ppm_p3("imagem.ppm");
    if (original == NULL) {
        printf("Arquivo nao eh PPM P3 ou nao existe");
        return 1;
    }
    IMAGEM *rotacionada = rotacionar_90_graus_direita(original);
    salvar_arquivo_ppm_p3("imagemsaida.ppm", rotacionada);
    destruir_imagem(original);
    destruir_imagem(rotacionada);
    return 0;
}

Let’s test all that. Here’s a very small and simple P3 PPM image:

P3
4 2
255
255 255 255   255   0   0     0 255   0     0   0 255
  0   0   0     0 255 255   255   0 255   255 255   0

Here is the result after running the program:

P3
2 4
255
0 0 0
255 255 255
0 255 255
255 0 0
255 0 255
0 255 0
255 255 0
0 0 255

Shall we visualize? Here are the two images side by side (increased with a 1000% zoom, since each of them has only 8 pixels and would be too small to see right):

  • Original image: Original - zoom 1000%

  • Image rotated 90 degrees to the right: Rotacionada - zoom 1000%

  • I thank you for the answer, cleared my doubt. However, I got another curiosity: How do I "blur the image"?

  • 1

    @DJM_JM A simple way is to take the value of the colors of the pixel, add to the value of the neighboring pixels, average them and put the resulting value in the pixel of the new image. To blur further, it can be not only of neighboring pixels, but of all pixels within a circle of radius r centered on the source pixel.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.