(EDIT: included all source files)
So, for this console application I've been attempting to read .ppm files as input in either ASCII or binary format and when given an output type either ASCII or binary would create a copy in said format. This functionality works just fine, but there is also an optional command line argument for image operations like flipX, flipY, rotateCW, etc. For reference here is my header file with my image struct and pixel typedef along with the read and write functions from my main file, additionally my memory allocation and freeing functions in case there is something up with it too.
.ppm images
The input .ppm images are input as such with either a P3 or P6 value in the first line corresponding to if the file is in ASCII or binary format:
P3
# CREATOR: GIMP PNM Filter Version 1.1
# This has multiple comments to
# test that you handle them.
735 486
255
91
158
247
88
155
244
87
154
241
For this project the output file if this were to remain ASCII would look like so for easier readability of which pixel values are red, green, and blue.
P3
# CREATOR: GIMP PNM Filter Version 1.1
# This has multiple comments to
# test that you handle them.
735 486
255
91 158 247
88 155 244
87 154 241
The image I have been using for this function is this (yes it's a bit questionable but an image is an image)
When calling my function using the command prompt "thpe11.exe --flipX --ascii skeletonburn skeletonburn.ppm" it results in the following which is wildly different then expected.
The rows seemingly appear to still flip across the x axis, but the rest of the image is wildly sheared and ripped.
Below I have the code that pertains to the issue:
netPBM.h
This is for a school assignment hence why the comments state that I cannot change the image struct.
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
#ifndef __NETPBM__H__
#define __NETPBM__H__
typedef unsigned char pixel;
struct image
{ // you may not add other fields to this
// structure.
string magicNumber;
string comment;
int rows;
int cols;
pixel **redGray; // handles red channel or grayscale
pixel **green;
pixel **blue;
};
// place your function prototypes here
bool isValidOperation( const char *operation );
bool readImage( ifstream &fin, image &img );
void writeImage( ofstream &fout, image &img );
bool openImageInput( ifstream &fin, char *fileName );
void closeImageInput( ifstream &fin );
bool openImageOutput( ofstream &fout, char *fileName );
void closeImageOutput( ofstream &fout );
bool allocate2DArray( pixel **&pixels, int rows, int cols );
void free2DArray( pixel **&pixels, int rows );
void flipX( image &img );
void flipY( image &img );
void rotateCW( image &img );
#endif
thpe11.cpp
main file
#include "netPBM.h"
const char *operationList[] = { "--flipX", "--flipY", "--rotateCW", "--rotateCCW", "--grayscale", "--sepia" };
int main( int argc, char *argv[] )
{
ifstream fin;
ofstream fout;
image img;
char *foutName;
int offset;
if ( argc == 5 )
{
offset = 0;
}
else if ( argc == 4 )
{
offset = 1;
}
else
{
// incorrect number of arguments error
cout << "Usage: thpe11.exe [operation] --[ascii | binary] basename image" << endl;
cout << endl;
cout << " basename filename for the new image without extension" << endl;
cout << " image a color ppm to be opened and manipulated" << endl;
cout << endl;
cout << "Output Type (required)" << endl;
cout << " --ascii image data will be written out in using ascii numbers." << endl;
cout << " --binary image data will be written out in raw format." << endl;
cout << endl;
cout << "Operations" << endl;
cout << " --flipX swap the contents along the X axis" << endl;
cout << " --flipY swap the contents along the Y axis" << endl;
cout << " --rotateCW rotate the image clockwise" << endl;
cout << " --rotateCCW rotate the image counter clockwise" << endl;
cout << " --grayscale convert a color image to grayscale" << endl;
cout << " --sepia antique a color image" << endl;
exit( 0 );
}
// invalid output type error
if ( !( strcmp( argv [ 2 - offset ], "--ascii" ) == 0 || strcmp( argv [ 2 - offset ], "--binary" ) == 0 ) )
{
cout << "Invalid Output Type Specified" << endl;
cout << "Usage: thpe11.exe [operation] --[ascii | binary] basename image" << endl;
cout << endl;
cout << " basename filename for the new image without extension" << endl;
cout << " image a color ppm to be opened and manipulated" << endl;
cout << endl;
cout << "Output Type (required)" << endl;
cout << " --ascii image data will be written out in using ascii numbers." << endl;
cout << " --binary image data will be written out in raw format." << endl;
cout << endl;
cout << "Operations" << endl;
cout << " --flipX swap the contents along the X axis" << endl;
cout << " --flipY swap the contents along the Y axis" << endl;
cout << " --rotateCW rotate the image clockwise" << endl;
cout << " --rotateCCW rotate the image counter clockwise" << endl;
cout << " --grayscale convert a color image to grayscale" << endl;
cout << " --sepia antique a color image" << endl;
exit( 0 );
}
// invalid operation error
if ( offset == 0 && !isValidOperation( argv [ 1 ] ) )
{
cout << "Invalid Operation Given" << endl;
cout << "Usage: thpe11.exe [operation] --[ascii | binary] basename image" << endl;
cout << endl;
cout << " basename filename for the new image without extension" << endl;
cout << " image a color ppm to be opened and manipulated" << endl;
cout << endl;
cout << "Output Type (required)" << endl;
cout << " --ascii image data will be written out in using ascii numbers." << endl;
cout << " --binary image data will be written out in raw format." << endl;
cout << endl;
cout << "Operations" << endl;
cout << " --flipX swap the contents along the X axis" << endl;
cout << " --flipY swap the contents along the Y axis" << endl;
cout << " --rotateCW rotate the image clockwise" << endl;
cout << " --rotateCCW rotate the image counter clockwise" << endl;
cout << " --grayscale convert a color image to grayscale" << endl;
cout << " --sepia antique a color image" << endl;
exit( 0 );
}
// open input file
if ( !openImageInput( fin, argv [ 4 - offset ] ) )
{
cout << "Unable to open image file: " << argv [ 4 - offset ] << endl;
exit( 0 );
}
// read image data header and pixels
if ( !readImage( fin, img ) )
{
cout << "Unable to allocate memory for storage" << endl;
exit( 0 );
}
// change magicNumber and set output file extension
foutName = argv [ 3 - offset ];
if ( offset == 0 && strcmp( argv [ 1 ], "--grayscale" ) == 0 ) // grayscale case to change to P2 or P5
{
strcat( foutName, ".pgm" );
if ( strcmp( argv [ 2 ], "--ascii" ) == 0 )
{
img.magicNumber = "P2";
}
else if ( strcmp( argv [ 2 ], "--binary" ) == 0 )
{
img.magicNumber = "P5";
}
}
else // normal case to change to P3 to P6 if necessary
{
strcat( foutName, ".ppm" );
if ( img.magicNumber == "P3" && strcmp( argv [ 2 - offset ], "--binary" ) == 0 )
{
img.magicNumber = "P6";
}
else if ( img.magicNumber == "P6" && strcmp( argv [ 2 - offset ], "--ascii" ) == 0 )
{
img.magicNumber = "P3";
}
}
// open output file of corresponding file type with magicNumber
if ( !openImageOutput( fout, foutName ) )
{
cout << "Unable to open image file: " << foutName << endl;
exit( 0 );
}
// call edit operation "--flipX", "--flipY", "--rotateCW", "--rotateCCW", "--grayscale", "--sepia"
if ( offset == 0 )
{
if ( strcmp( argv [ 1 ], "--flipX" ) == 0 )
{
flipX( img );
}
else if ( strcmp( argv [ 1 ], "--flipY" ) == 0 )
{
flipY( img );
}
else if ( strcmp( argv [ 1 ], "--rotateCW" ) == 0 )
{
// call rotateCW
}
else if ( strcmp( argv [ 1 ], "--rotateCCW" ) == 0 )
{
// call rotateCCW
}
else if ( strcmp( argv [ 1 ], "--grayscale" ) == 0 )
{
// call grayscale
}
else if ( strcmp( argv [ 1 ], "--sepia" ) == 0 )
{
// call sepia
}
}
// write data to output image
writeImage( fout, img );
// call deallocate memory
free2DArray( img.redGray, img.rows );
free2DArray( img.green, img.rows );
free2DArray( img.blue, img.rows );
// call close file
closeImageInput( fin );
closeImageOutput( fout );
return 0;
}
bool isValidOperation( const char *operation )
{
for ( size_t i = 0; i < size( operationList ); i++ )
{
if ( strcmp( operation, operationList [ i ] ) == 0 )
{
// operation valid
return true;
}
}
// operation not valid
return false;
}
bool readImage( ifstream &fin, image &img )
{
string tempStr;
pixel value;
int i, j, k;
// read header
// read magic number
getline( fin, img.magicNumber );
// read the rest of the header
while ( true )
{
fin >> tempStr;
if ( tempStr == "#" )
{
getline( fin, tempStr );
if ( img.comment.empty() )
{
img.comment = "#" + tempStr;
}
else
{
img.comment += "\n";
img.comment += "#" + tempStr;
}
}
else
{
img.rows = stoi( tempStr );
fin >> tempStr;
img.cols = stoi( tempStr );
break;
}
}
// read maximum pixel size
fin.ignore( 1 );
getline( fin, tempStr );
if ( stoi( tempStr ) > 255 )
{
cout << "Image does not consist of 8-bit pixels" << endl;
exit( 0 );
}
// allocate all 3 pixel arrays
if ( !allocate2DArray( img.redGray, img.rows, img.cols ) )
{
return false;
}
if ( !allocate2DArray( img.green, img.rows, img.cols ) )
{
return false;
}
if ( !allocate2DArray( img.blue, img.rows, img.cols ) )
{
return false;
}
// verify magicNumber and then read pixels
if ( img.magicNumber == "P3" ) // ascii
{
// read in pixel values and store in pixel arrays
for ( i = 0; i < img.rows; i++ )
{
for ( j = 0; j < img.cols; j++ )
{
for ( k = 0; k < 3; k++ )
{
fin >> tempStr;
fin.ignore(); // ignore space/newline character
value = static_cast< char >( stoi( tempStr ) );
// assign pixel to appropriate color band
switch ( k % 3 )
{
case 0:
img.redGray [ i ][ j ] = value;
break;
case 1:
img.green [ i ][ j ] = value;
break;
case 2:
img.blue [ i ][ j ] = value;
break;
}
}
}
}
}
else if ( img.magicNumber == "P6" ) // binary
{
// read in pixel values and store in pixel arrays
for ( i = 0; i < img.rows; i++ )
{
for ( j = 0; j < img.cols; j++ )
{
for ( k = 0; k < 3; k++ )
{
fin.read( reinterpret_cast< char * >( &value ), sizeof( pixel ) );
// assign pixel to appropriate color band
switch ( k % 3 )
{
case 0:
img.redGray [ i ][ j ] = value;
break;
case 1:
img.green [ i ][ j ] = value;
break;
case 2:
img.blue [ i ][ j ] = value;
break;
}
}
}
}
}
else // error case if image is not of type P3 or P6
{
cout << "Not a valid netpbm image" << endl;
exit( 0 );
}
return true;
}
void writeImage( ofstream &fout, image &img )
{
pixel value;
int i, j, k;
// populate header
fout << img.magicNumber << '\n';
fout << img.comment << '\n';
fout << img.rows << ' ' << img.cols << '\n';
fout << 255 << '\n';
// verify magicNumber and then populate pixels
if ( img.magicNumber == "P3" ) // ascii
{
for ( i = 0; i < img.rows; i++ )
{
for ( j = 0; j < img.cols; j++ )
{
// assign pixels to appropriate color bands
fout << static_cast< int >( img.redGray [ i ][ j ] ) << " ";
fout << static_cast< int >( img.green [ i ][ j ] ) << " ";
fout << static_cast< int >( img.blue [ i ][ j ] ) << "\n";
}
}
}
else if ( img.magicNumber == "P2" ) // greyscale ascii
{
for ( i = 0; i < img.rows * img.cols; i++ )
{
for ( j = 0; j < img.cols; j++ )
{
// assign pixel to color band
fout << static_cast< int >( img.redGray [ i ][ j ] ) << "\n";
}
}
}
else if ( img.magicNumber == "P6" ) // binary
{
for ( i = 0; i < img.rows; i++ )
{
for ( j = 0; j < img.cols; j++ )
{
for ( k = 0; k < 3; k++ )
{
// assign pixel to appropriate color band
switch ( k % 3 )
{
case 0:
value = img.redGray [ i ][ j ];
break;
case 1:
value = img.green [ i ][ j ];
break;
case 2:
value = img.blue [ i ][ j ];
break;
}
fout.write( reinterpret_cast< char * >( &value ), sizeof( pixel ) );
}
}
}
}
else if ( img.magicNumber == "P5" ) // greyscale binary
{
for ( i = 0; i < img.rows; i++ )
{
for ( j = 0; j < img.cols; j++ )
{
// assign pixel to color band
value = img.redGray [ i ][ j ];
fout.write( reinterpret_cast< char * >( &value ), sizeof( pixel ) );
}
}
}
}
memory.cpp
#include "netPBM.h"
// allocate 2d array for color-band data
bool allocate2DArray( pixel **&pixels, int rows, int cols )
{
int i;
pixels = new ( nothrow ) pixel * [ rows ];
if ( pixels == nullptr )
{
return false;
}
for ( i = 0; i < rows; i++ )
{
pixels [ i ] = new ( nothrow ) pixel [ cols ];
if ( pixels [ i ] == nullptr )
{
free2DArray( pixels, i );
return false;
}
}
return true;
}
// free 2d array of color-band data
void free2DArray( pixel **&pixels, int rows )
{
int i;
if ( pixels == nullptr )
{
return;
}
for ( i = 0; i < rows; i++ )
{
delete[] pixels [ i ];
}
delete[] pixels;
pixels = nullptr;
}
imageOperations.cpp
#include "netPBM.h"
// flip on x-axis
void flipX( image &img )
{
pixel temp;
int i, j;
for ( i = 0; i < img.rows / 2; i++ )
{
for ( j = 0; j < img.cols; j++ )
{
temp = img.redGray [ i ][ j ];
img.redGray [ i ][ j ] = img.redGray [ img.rows - 1 - i ][ j ];
img.redGray [ img.rows - 1 - i ][ j ] = temp;
temp = img.green [ i ][ j ];
img.green [ i ][ j ] = img.green [ img.rows - 1 - i ][ j ];
img.green [ img.rows - 1 - i ][ j ] = temp;
temp = img.blue [ i ][ j ];
img.blue [ i ][ j ] = img.blue [ img.rows - 1 - i ][ j ];
img.blue [ img.rows - 1 - i ][ j ] = temp;
}
}
}
// flip on y-axis
void flipY( image &img )
{
int start = 0;
int end = img.rows - 1;
while ( start < end )
{
swap( img.redGray [ start ], img.redGray [ end ] );
swap( img.green [ start ], img.green [ end ] );
swap( img.blue [ start ], img.blue [ end ] );
start++;
end--;
}
}
// rotate clockwise
// rotate counter clockwise
// grayscale
// sepia
imageFileIO.cpp
#include "netPBM.h"
// open image input file
bool openImageInput( ifstream &fin, char *fileName )
{
fin.open( fileName, ios::in | ios::binary );
if ( !fin.is_open() )
{
return false;
}
return true;
}
// close image input file
void closeImageInput( ifstream &fin )
{
fin.close();
}
// open image output file
bool openImageOutput( ofstream &fout, char *fileName )
{
fout.open( fileName, ios::out | ios::trunc | ios::binary );
if ( !fout.is_open() )
{
return false;
}
return true;
}
// close image output file
void closeImageOutput( ofstream &fout )
{
fout.close();
}
I have spent countless hours attempting to rewrite my function for flipX by changing up whether rows and columns were i or j and using different algorithms I have seen searching the web. There was at one point that I had this working but that was before I had realized I was storing my pixel data wildly different and more complicated than I should've. Earlier I was storing each individual number to the 2nd index of the pixel arrays when I should've been storing them as ascii characters from the start. I know that simply reversing the columns/rows of a 2d array should be straightforward since the simplicity of the code should reflect that, so I must be missing something with how my data is stored and reinterpreting it incorrectly as opposed to how writeImage reads it to copy it. I figured I've spent a good amount of hours to warrant creating a post here so I can stop wasting more without coming to a proper conclusion.