, , , , , ,

[SOLVED] Project 2: image processing cop3503c: programming fundamentals ii

$25

File Name: Project_2:_image_processing_cop3503c:_programming_fundamentals_ii.zip
File Size: 612.3 KB

5/5 - (1 vote)

Lots of applications need to process images in some way. Load them, store them, write them back out to files, scale them, rotate them, adjust the color in part (or all) of the image, etc. The purpose of this assignment is to show you how you can perform some of these operations on a particular type of file. In this assignment, you will: • Read data from binary .tga files • Process the image data stored within files in a variety of ways • Write out the new .tga files in the same binary format • Develop a command-line interface to handle user input • Develop a Makefile to allow others to compile and run your project quickly and easily !This assignment is split into two milestones. To encourage you to split your work over several days, each milestone is due on a different day. • Milestone 1 covers manipulating image data. You will implement ten different manipulation tasks, each of which use different manipulation methods. Furthermore, you will write a basic Makefile to run your program automatically. Milestone 1 is due on Gradescope on Wednesday, March 20th at 11:59pm. • Milestone 2 covers creating a command-line interface to your program. Your program will no longer run the same ten tasks repeatedly, rather, the user will be able to specify the tasks they want to run themselves. You will also make a small modification to your Makefile that will run a few tasks using your new command-line interface. Milestone 2 is due on Gradescope on Wednesday, March 27th at 11:59pm.If you haven’t already, you will need to install an IDE and compiler for this project. This will allow you to write and run your code on your local computer, which can speed up development and give you access to more tools for working with your project. If you need to install an IDE and/or compiler, please visit this page on Canvas for instructions. 3 TGA Files What is a TGA, or .tga, file? It’s simply an image file – just like your standard PNG, JPEG, or GIF files. However, the file format supporting TGA files is much simpler and easier to work with, especially when modifying the images through code.In this assignment, you will learn this specification in order to read and create your own TGA files in C++! How awesome is that? It’s like your own little Photoshop! Some operating systems won’t let you open TGA files natively (looking at you, Windows), so you will need to install some sort of viewer for them. If you already have a tool installed that lets you open and view these, great. You can tell if you have a tool installed already if you try clicking on one of the .tga files provided and observing if a program is able to show you the file. It’s recommended to install at least one of the following programs if you do not already have a TGA viewer installed: • Visual Studio: If you have installed Visual Studio, you should be able to click on a .tga file and have the file open up in a new Visual Studio tab. • Photoshop: If you use the popular Photoshop image manipulation tool, you can use this program to view your outputted .tga files. • GNU Image Manipulation Program (GIMP): A popular free and open-source image editor, GIMP will be able to open your .tga files and is completely free. • TGAViewer: A small, cute little program whose job is just to view .tga files. How nice! 3 4 TGA FILE SPECIFICATION Note that not having a viewer installed on your computer will not prevent you from completing the project. However, it will prevent you from seeing the contents of your output images, which may make it more difficult to catch possible errors in your code. Although it is helpful to generate the images and check whether the output is correct visually, we acknowledge that not all people can see pictures correctly (ie, have an inability ability to perceive or distinguish certain color, or students with visual disabilities). This assignment has been developed with visually impaired students in mind, and can be completed entirely through code. As you move through the document and begin to work with TGA files, we recommend accomplishing your tasks using images with only a few pixels. For example, rather than testing your code using a 512×512 image, you could use an image of size 2×2 to check if your methods are implemented correctly at the channel level. This setup is more accommodating for screen readers and other assistive devices. 4 TGA File Specification 4.1 RGB and Pixels Before jumping into the format, we need to discuss how colors can be represented in computers. Without having a way to represent colors, you can’t represent pictures. For example, imagine trying to store an image of a sunflower on your computer. How do you store the golden color of the flower, or the pastel blue color of the sky, or the olive green color of the flower stem? One of the most common format for representing colors numerically is known as the RGB format. This format uses three numbers (0-255) to store the amount of red, green, and blue in the color. Figure 4.1 showcases some examples of this representation. R: 255 G: 0 B: 0 Red R: 0 G: 255 B: 0 Green R: 0 G: 0 B: 255 Blue R: 255 G: 0 B: 255 Magenta R: 255 G: 255 B: 255 White R: 0 G: 0 B: 0 Black R: 240 G: 255 B: 48 Yellow R: 61 G: 50 B: 161 Purple Figure 4.1: Various colors represented in RGB format. You can see that colors high in red, green, and blue, but devoid of other colors, are unsurprisingly red, green, and blue, respectively. White is made by setting red, green, and blue to the max. Black is made by setting all values to zero. Other colors are made by combining various values of these three elements. If you would like to create your own colors, you can use a color picker tool, found on your computer or online. An example of such tool can be found at https://colorpicker.me/. In computers, these three numbers are commonly stored as 3 bytes, each made up of 8 bits. Each byte is able to represent a number between 0 and 255. Note that some file formats store the bytes in the reverse order of blue, green, red, rather than the conventional red, green, blue. This includes TGA – but more on that later! You should now feel comfortable understanding how a single color can be represented numerically. But, a picture is made up of many colors – how do we represent all of these various colors? This is the concept of pixel. Digital photos are made up of a series of these pixels which create a unique image. The height and width of an image are determined by the number of these pixels in a column and row, respectively. For example, a 1920×1080 image, or 1080p image, has 1,920 pixels in each row, and 1,080 pixels in each column. The image has a height of 1080 pixels and a width of 1920 pixels. Each pixel has three color attributes: red, green, and blue. 4.2 File Format Since binary files are all about bytes, they are typically an unreadable mess to any program (or person) that doesn’t know exactly how the data is structured. In order to read them properly, you must have some sort of blueprint, schematic, or breakdown of how the information is stored. Without this description of the file format, you would just be reading random combinations of bytes attempting to get some useful information out of it—not the most productive process. The TGA file format is a relatively simple format, though it has some options which can get a bit complex in some cases. The purpose of this assignment is not make you a master of this particular image 4 4.2 File Format 4 TGA FILE SPECIFICATION format, so a few shortcuts will be taken (more on those later). First, let’s take a quick look at the file format, showcased in Table 4.1. Field Size Length Description Header ID Length 1 byte Denotes the length of the Image ID. Color Map Type 1 byte Specifies if a color map is present. Image Type 1 byte Indicates the type (e.g., grayscale, true color, compressed). Color Map Origin 2 bytes Starting index of the color map, usually 0. Color Map Length 2 bytes Length of the color map, usually 0. Color Map Depth 1 byte Bit depth of the color map, usually 0. X Origin 2 bytes Horizontal image origin, typically 0. Y Origin 2 bytes Vertical image origin, usually 0. Image Width 2 bytes Width of the image in pixels. Image Height 2 bytes Height of the image in pixels. Pixel Depth 1 byte Bit depth of each pixel, typically 24 for RGB. Image Descriptor 1 byte Provides additional details about the image. Data Image Data Variable Contains pixel data in BGR format. Starting from bottomleft to upper-right. Number of pixels is the product of width and height. Table 4.1: File specification for TGA files. So to start, there is a header. Every file format is potentially different, but in a TGA file the header data takes up 18 bytes total, across a number of variables, and this information describes the rest of the file. Depending on the specifics of the file (or your scenario), some of those variables may have a value of zero, or they may be non-zero. (In the case of the TGA format, some values in the header were once very important, but nowadays are not used—the format still has them for compatibility reasons.) From the header description, the image width and image height are at a 12 byte offset and a 14 byte offset, respectively, from the beginning of the file. You may find it helpful to read each piece of data in the header into a structure. Then, once the header has been completely read, you can go about using it for whatever purposes you have in mind. A possible structure for the header is shown in Listing 4.1. Listing 4.1: Example of using a C++ struct to hold header information. 1 struct Header { 2 char idLength; 3 char colorMapType; 4 char dataTypeCode; 5 short colorMapOrigin; 6 short colorMapLength; 7 char colorMapDepth; 8 short xOrigin; 9 short yOrigin; 10 short width; 11 short height; 12 char bitsPerPixel; 13 char imageDescriptor; 14 }; Note that if you used char in your program, when you print out the char or unsigned char variable, you get a symbol that corresponds to its numeric value, instead of the number itself. If you want to see the numeric value of a char variable instead of its symbol, you would have to cast it to an integer, as shown in Listing 4.2. 5 4.3 TGA Image Data 4 TGA FILE SPECIFICATION Listing 4.2: Showcasing various methods of printing information about a character in C++. 1 char someVariable = 65; 2 cout << someVariable; // Prints out ‘A’ instead of 65 3 cout << (int)someVariable; // Prints out 65 instead of ‘A’ We’ve covered reading headers into memory, but how do we construct and write a header for an entirely new image? Here’s the great news: as long as the images are the same width and height, you can simply steal the header from either image and use that in the resulting image. In this project, all source images that will be used have headers that are compatible with each other. Note that if you need to change the width and height of the resulting image, you will need to modify this property in the resulting image’s header. 4.3 TGA Image Data After the header comes the really important part, the image data itself. In a TGA file, the image data is stored in a contiguous block of pixels. The number of pixels in the block is equal to the image’s length multiplied by the image’s width. The contents of a single pixel can vary depending on the properties of the file, but for this assignment we are using images with 24-bit color. This means that each pixel contains: • 1 byte (8 bits) for blue data • 1 byte (8 bits) for green data • 1 byte (8 bits) for red data Notice that the order of the color data is BGR, not RGB. Each of those bytes will contain a value from 0-255, representing the intensity of that color in the pixel. Conveniently, an unsigned char is able to store values in the range of 0-255! So if a file had a size of 200×300, it would contain 60,000 pixels, each of which contains 3 bytes of data. Figure 4.2 shows the start of the image data of an example TGA file, where pixels are stored in series. example.tga: 247 B 22 G 0 R 132 B 51 G 102 R 255 B 75 G 218 R 0 B 198 G 176 R Pixel 0 Pixel 1 Pixel 2 Pixel 3 Figure 4.2: Series of pixels stored in the example.tga file. Pixels are stored in series, and each contain a B, G, and R value. To store these values in your program, you could make a 1-D array/vector with 180,000 entries, where there are three new entries in the array for every pixel (being the B, G, and R values for each pixel). Or, you could make a 2-D array/vector of 60,000 entries, with each entry being a vector storing three elements, the B, G, and R values of the specific pixel. The approach you take should depend on what you feel comfortable with. What about the order of the pixels themselves? In many image files (including TGA files), the first pixel in the file represents the bottom left corner of the image, while the last pixel represents the top right corner of the image, as shown in Figure 4.3. If you read, store, and write the pixel data in the same order, you don’t really have to worry too much about this. If you wanted to copy data into a particular part of the image, however… that can be a bit tricky. For example, to copy some 2×2 image into the top left corner would require you change pixels 16, 17, 24, and 25. 6 5 READING AND WRITING 0 8 16 24 1 9 17 25 2 10 18 26 3 11 19 27 4 12 20 28 5 13 21 29 6 14 22 30 7 15 23 31 First pixel you read/write Last pixel you read/write tinyaqua.tga Figure 4.3: Order in which pixels are retrieved and written to in a TGA file. So, to summarize: The file contains a header, which is 18 bytes in length. Stored within those 18 bytes are pieces of information describing the image content—the width and height of the image, how the color data is stored, and so on. All you need from the header is the width and the height. However, when writing a file, you should provide all the header data, whether you are using it or not. Because of this, you should store this data along with the image data itself. 5 Reading and Writing Great – you should now have a good understanding of TGA files. You should now have the skills to read and write to them. Now, we encourage you to try reading and writing one of the .tga files we give you. While this isn’t worth any points on its own, this part serves as good practice for the next section. Specifically, try: 1. Reading the header and pixel data from one of the TGA files provided to you. You can choose one of the files from the input/ folder provided to you in the assignment zip file. 2. Storing the information you read (image header, pixel content, etc.) in data structures. 3. Writing the contents of those data structures to another TGA file with a different name. 4. Verifying that the contents of the file you wrote and the file you read from are the same. For now, you can just visually inspect the two images. If you do not write the binary data in the correct format to the .tga file, your viewer will not be able to open the file. Try opening a file from the input/ folder to be sure your viewer is not broken. Once you feel confident reading and writing files, then you should be ready to move to Part 2. 5.1 Implementation Tips As you move throughout this project, we’ll offer some small bits of advice to help you along your journey. Like a great narrator, we’ll only complement your adventure. • If you try opening a .tga file created by your program and see that the file cannot be opened by your viewer, or that your viewer crashes when attempting to open the file, your code implementation is likely broken. • Start by planning out the data structures you will need, and how you’ll store those objects. Planning out the design early makes the later implementation much easier. • If your image looks like white noise (ie, pixels are random colors), you may have read the header incorrectly. If you can see the subject of the image, but the image colors are slightly off, make sure you read the pixel data in BGR format and that you are using the right data types to store your binary data. • Remember to read the file in binary mode. This isn’t default, you need to pass an argument to your fstream constructor. • Use classes and structs. Do not try to not use them – you will save yourself more time by declaring these structures up front and using them throughout your project. 7 6 MILESTONE 1: IMAGE MANIPULATIONS • Pass objects around by reference, not by value, or your program might become very slow and memory inefficient. • If you see a segmentation fault, or if you’re only able to read zeroes, this may mean that your program is not able to see the image file you are referencing. You can use the is_open() function to see if the file you are referencing has been successfully opened by your program. • A BGR value of (0, 0, 0) represents a black pixel. A BGR value of (255, 255, 255) represents a white pixel. If your image is all black or all white, you might be reading all zeroes or all max values. • If your output image has vertical or horizontal lines which are all the same color, then your loops are likely incorrect. You might be printing the same row/column of pixels repeatedly, without incrementing to the next row/column. 6 Milestone 1: Image Manipulations If you’ve used Photoshop or a similar tool, you’re likely aware of image manipulation. Tools in these programs allow you to change the pixel data in image data using common algorithms. These algorithms allow you to blend colors together, highlight certain colors, or even make objects disappear. You will be implementing some of these algorithms as part of this project. 6.1 Algorithms These algorithms are described in the Table 6.1. You will be implementing the formulas in the table in your own program. Each formula contains two values, P1 and P2. These represent the individual channel values of the two pixels used in the formula. Note that the order of the pixels matters for some algorithms – in these cases, the pixels in the “top layer” become P1, and the pixels in the “bottom layer” become P2. NP1 and NP2 refer to the normalized values of P1 and P2, as explained in subsection 6.2. Note that these formulas should be calculated for each channel of the input images, and then used in each channel of the resulting output image. You will need to use the equations in Table 6.1 three times per pixel, one time per channel. Method Channel Formula Multiply NP1 · NP2 Screen 1 − [(1 − NP1) · (1 − NP2)] Subtract P1 − P2 Addition P1 + P2 Overlay ( NP2 ≤ 0.5 : 2 · NP1 · NP2 NP2 > 0.5 : 1 − [2 · (1 − NP1) · (1 − NP2)] Table 6.1: Formulas for common manipulation algorithms. Note that the “Overlay” method shown in Table 6.1 is a conditional algorithm, meaning the specific algorithm you’ll need to run varies based on the input pixel value. You can implement this in code using an if/else statement. 6.2 Clamping and Overflow If you attempt to implement some of the above algorithms using the suggested unsigned char data type, you will experience overflow. This occurs when one of the above calculations results in a number below zero or greater than 255 – values which can not be stored by that data type. As an example, imagine two pixels, one with a green value of 100, and one with a green value of 200. If you attempt to add the green components together, you will end up with a value of 300, which will overflow to 300 − 255 = 45. • For operations involving addition, you will need to clamp values. When completing an operation, you will likely need to use a data type that can support a large range of values, such as 8 6.3 Tasks 6 MILESTONE 1: IMAGE MANIPULATIONS int. Then, after the operation is complete, all values lower than zero become zero, and all values greater than 255 become 255. Failure to clamp can lead to incorrect values which may not be noticeable at first. Figure 6.1 illustrates this: subtracting the two green values results in a negative number, which would typically overflow, resulting in an incorrect result for this task. However, clamping the green value (by making the negative result zero) leads to the correct answer. − = not − = not tinypink.tga tinygreen.tga correct.tga invalid.tga 144 B 102 G 182 R 3 B 201 G 129 R 141 B 0 G 53 R 141 B 156 G 53 R Figure 6.1: When clamping is not done, an entirely different image can arise. Notice how the green value is appropriately clamped to 0 in the correct image, while it overflows to 102 − 201 = −99 + 255 = 156 in the incorrect image. • For operations involving multiplication, you will need to use normalized values. These values are the original values of the pixel divided by 255. Therefore, the range of a normalized pixel is 0-1. In these operations, you will likely need to use float to store the intermediate values. After the operation, you just need to multiply the resulting float by 255, and voilà, you have a new pixel value! Keep in mind that operations involving float values will likely result in precision errors. Therefore, you should add 0.5f to the normalized value multiplied by 255, before converting to an unsigned char. Why 0.5f? This value is perfect for rounding numbers. Numbers closer to the next highest value (such as 79.7) will be rounded up (to 80, in our example). Numbers closer to the lower value (such as 79.2) will be rounded down (to 79). 6.3 Tasks In this project, you are expected to complete ten different tasks, each of which use different input images and image manipulation algorithms. Your program should be able to complete all ten tasks and produce images that match the images given in the examples/ folder. The output names for all files you generate should be output/partX.tga, where X is the task number. For now, all tasks should complete whenever your project is built and ran. The user should not need to enter any input. 1. Use the Multiply blending mode to combine layer1.tga (top layer) with pattern1.tga (bottom layer). 2. Use the Subtract blending mode to combine layer2.tga (bottom layer) with car.tga (top layer). 3. Use the Multiply blending mode to combine layer1.tga with pattern2.tga, and store the results temporarily, in memory (aka, don’t write this to a file somewhere, just store the pixel values somewhere in your program). Load the image text.tga and, using that as the bottom layer, combine it with the previous results of layer1/pattern2 using the Screen blending mode. 4. Multiply layer2.tga with circles.tga, and store it. Load pattern2.tga and, using that as the bottom layer, combine it with the previous result using the Subtract blending mode. 5. Combine layer1.tga (as the top layer) with pattern1.tga using the Overlay blending mode. 6. Load car.tga and add 200 to the green channel. 7. Load car.tga and scale (multiply) the red channel by 4, and the blue channel by 0. This will increase the intensity of any red in the image, while negating any blue it may have. 8. Load car.tga and write each channel to a separate file: the red channel should be part8_r.tga, the green channel should be part8_g.tga, and the blue channel should be part8_b.tga. (Hint: If your red channel image appears all red, try writing [Red, Red, Red] instead of [Red, 0, 0] to the file—ditto for green and blue!) 9 6.4 Implementation Tips 6 MILESTONE 1: IMAGE MANIPULATIONS 9. Load layer_red.tga, layer_green.tga and layer_blue.tga, and combine the three files into one file. The data from layer_red.tga is the red channel of the new image, layer_green is green, and layer_blue is blue. 10. Load text2.tga, and rotate it 180 degrees, flipping it upside down. This is easier than you think! Try diagramming the data of an image (such as earlier in this document). What would the data look like if you flipped it? Now, how to write some code to accomplish that…? 6.4 Implementation Tips Again, let’s talk about some tips that might help you out with this part of the project: • Re-read over the implementation tips listed in subsection 5.1. • Be careful when casting values to specific types. If you’re completing an operation and then casting to a specific type, remember to wrap the operation in parentheses, as in (unsigned char)(a + b), not (unsigned char)a + b. • Make sure you know which file is the “top layer” and which file is the “bottom layer” in operations where that matters. • Make separate functions for each operation. While you could just write all the operation code in your main() function, using separate functions will likely make Part 3 (the next part) easier to understand. 6.5 Testing After you have completed the tasks, you’ll want to make sure that your output matches the expected output we actually give you. To do this, you can use an external program to compare your output with our output, or you can write your own code to do so. We typically recommend both methods, as the external tools will always be correct in their analysis, while your tool will likely give you more helpful information about which pixels are wrong in your output. Your images and the example images given must match exactly. This includes the header and all pixels stored in both images. Looking at two images side-by-side is not a valid testing method. 6.5.1 External Comparison Tools To tell if two files are different, you can use command-line tools installed with your operating system. • Windows: In your terminal or PowerShell, you can use the fc.exe program to tell whether two binary files are different. Use the /b flag followed by the name of each of the binary files. If there are any differences, the program will print them to you. Otherwise, it will tell you that no differences were found. • macOS, Linux: Use the diff command. This command will simply output whether the two files you provided as arguments differ in content. If the program prints no output, then the two files provided as arguments have the same content. (a) Windows, using fc.exe (b) macOS/Linux, using diff Figure 6.2: Comparing the contents of two files, in Windows and macOS. First, the contents of two equivalent files are checked. Then, the contents of two different files are compared. 10 6.6 Makefile 6 MILESTONE 1: IMAGE MANIPULATIONS 6.5.2 Writing Tests If your files differ in content, you might have noticed that the commands aren’t super helpful in telling you how to fix your program. That’s where you come in! If your file differs in content from the examples provided, you will probably want to write a small extension to your program that shows you which pixels are different, and how they are different. This program doesn’t need to be too complex — but it should help you debug faster by showing you which pixels are incorrect, and how they are incorrect. This program should load both files into memory, and compare their pixels against each other, letting you know when it finds a difference. Writing this step is not required at all, so build it however you think it will help you most. 6.6 Makefile In your submission for this project, you will need to create a Makefile. This is a small file that allows us to build your project instantly, without needing to run any of our own commands. For this project, you will write a Makefile with just one rule to start. This rule should generate an executable named project2.out. This executable will be generated by g++, which will be the command you reference in your first rule. You can learn more about Makefiles below. 6.6.1 Introduction Compiling from the command-line interface (CLI) can be a much faster way to build a project. It’s not without its drawbacks, however. Consider the following example, where you have a file, main.cpp, that you want to build. A simple command to build it might be (for GCC): bash-5.2$ g++ -std=c++11 -o MyProgram main.cpp In this example: • g++ is the executable, and everything else is an argument sent to that program. • -std=c++11 sets which version of the C++ standard to use (C++11, in this case). Other values could be c++14, c++17, or other new versions when they are released. • -o sets the name of the output to whatever you specify in (in this example, the result is MyProgram). If nothing is specified, the file produced has a.out (or a.exe, on Windows) as the default name. main.cpp is, of course, the file that you want to compile and ultimately turn into an executable. So far so good. But, what if you wanted to add some additional files? Instead of just main.cpp, let’s say you have main.cpp and a class called FileReader which is split into FileReader.h and FileReader.cpp. The command to build this might now change to be this: bash-5.2$ g++ -o MyProgram main.cpp FileReader.cpp Still not so bad, right? What if you then added classes called FileDatabase, UI, and UIControl? You’d again have to adjust the command: bash-5.2$ g++ -o MyProgram main.cpp FileReader.cpp FileDataBase.cpp UI.cpp UIControl.cpp Still not the end of the world, but… having to type that every time could be a bit tiresome (not to mention how easy it gets to forget a single file from the list). What if you then added a class called Stopwatch, and another called UserPreferences, and another… A better way to do this would be to save that command in a file, and then call on that file to execute the command. That way, you only have to write the information once, and never need to worry about forgetting any part of it. The file that you would store this in is called a Makefile. 11 6.6 Makefile 6 MILESTONE 1: IMAGE MANIPULATIONS 6.6.2 Using Makefiles A makefile is a plain text file (very commonly just named Makefile with no extension) which can contain instructions to build your project (and possibly do a lot of other things). In order to execute these instructions you can simply run the following command from a directory that contains a makefile: Listing 6.4: Invoking make from the command-line. bash-5.2$ make (macOS/Linux) bash-5.2$ mingw32-make (Windows) Note that the command used to run your Makefile varies between operating systems, and it might be neither make nor mingw32-make, especially if you use an advanced setup. This assignment is only going to scratch the surface of Makefiles and how to use them. 6.6.3 Creating a New Document You will need to write your Makefile in a file named Makefile, with no extension. The easiest way to make this file is to use your terminal to make this file. Remember to change directories before making the new file, so you know where it is! Listing 6.5: Creating a Makefile using the command-line. bash-5.2$ touch Makefile (macOS/Linux) bash-5.2$ type nul > Makefile (Windows) If you want to use your computer’s user interface, that works as well. You can create makefiles similarly to how you create any other text file: 1. Create a new text document using your file explorer. On Windows, right click and choose New ⇒ Text Document. On macOS, launch TextEdit.app, create the document in the appropriate folder, and choose Format ⇒ Make Plain Text (otherwise, it will be a rich text format). 2. When asked to save the file, remember to use no extension. You may need to enable hidden file extensions in your operating system to see if your file has an extension before submitting it. This can be done in Windows by selecting “File name extension” in the View tab of File Explorer. This can be done in macOS by opening Finder, navigating to Preferences ⇒ Advanced, and then enabling “Show all filename extensions.” 3. If your file has the .txt extension, remove it before submitting. 6.6.4 Structure Great, you should now have a text file to write your Makefile in. Let’s begin writing your first Makefile. When you run make, the program looks for a Makefile in your current directory. If it finds one, it executes the top rule. A rule is a series of steps that executes based on a keyword. Here is an example of a Makefile with two rules: Listing 6.6: Makefile with two rules. 1 build: 2 g++ -o MyProgram main.cpp FileReader.cpp FileDataBase.cpp UI.cpp UIControl.cpp 3 4 clean: 5 rm a.out 6 rm output Here, we can run either rule by calling make followed by the rule name— for example, make clean would run the two rm commands. As mentioned before, emitting the name of a rule executes whichever rule comes first in the file. 12 6.7 Submission 6 MILESTONE 1: IMAGE MANIPULATIONS Wildcards The build command shown Listing 6.6 is great, but we have to keep updating it as we add more and more C++ files. Luckily, there’s a wildly better way to do that, using wildcards. These are expressions using the * keyword to substitute the place of other terms. For example *.c targets any filename ending in .c. Let’s use this to update our build command from Listing 6.6: Listing 6.7: Makefile using a wildcard. 1 build: 2 g++ -o MyProgram *.cpp This looks much better, and will update whenever we add more C++ files to our project. Appropriate Files There are two notes about the files you reference in your Makefile. 1. If you need to reference files in a given directory, you can use dirname/filename to reference that file. This works for wildcards, too, but note that this can be trickier. Wildcards matching directories and subdirectories can be confusing. In this assignment, you shouldn’t have too many levels of nested directories in your submission, so this won’t be an issue. 2. You should not use header files in your compiler command. Headers do not need to be compiled, only the implementation files do. After the implementation files are compiled, the header files will be appropriate linked with your source. 6.6.5 Writing your Makefile For this project, you will start with just one rule in your Makefile. The default (first) rule in your Makefile should build your project. For this rule, note that some characteristics we want from the build command include: • The output executable should be named project2.out. • The C++11 standard should be used. • You should compile all of the files relevant to your project. 6.7 Submission To submit your first milestone, submit your project zip file to the Milestone 1 assignment in Gradescope. For more information about how to package your project and submit to the Gradescope platform, see Appendix C. For details on grading, see Appendix B. 13 7 MILESTONE 2: COMMAND-LINE INTERFACE 7 Milestone 2: Command-Line Interface ! This is the second part of the project, and is due after the first part is due. You should not start this part until you have completed the first part. You should now feel comfortable reading TGA files, manipulating their contents, and writing the contents to new TGA files. However, the tasks you’ve completed can’t be easily changed by the user — they have to open the source code, change which algorithms are executed themselves, and then re-build the program. That’s complicated, ripe for new bugs, and is hard for users who may not be familiar with C++. To solve this, you’re going to develop a command-line interface to your program. This will allow the end user to provide different arguments to your program, which will change the behavior of your program on the fly. This allows the end user to change the manipulation methods used, the input files used, or the name of the output file, all without needing to re-build. 7.1 Accessing the Command-Line First, we will need to understand the different ways to access your command-line, in order to test this part of your program. There’s nothing you need to build or test yet, this is just meant to show you how to access the command-line in your editor of choice. Typically, the command-line is accessed through a dedicated terminal app. However, most modern development environments today have embedded terminals that allow you to test a command-line app from the IDE itself, rather than needing to use an external program. Furthermore, many IDEs even support debugging of programs running with specific command-line arguments! We’ll take a look at how you can access these environments in the following subsections. 7.1.1 CLion Typically, programs are run in CLion by clicking the green triangle ( ) in the upper right-hand corner of the program, which builds and runs the project. However, this button does not allow us to input arguments into our program before running it. Therefore, we should instead use the hammer ( ) to only build the project. Then, navigate to the “Terminal” menu at the bottom of CLion — this is your command-line. The specific executable built by your program will be given the name listed in your CMakeLists.txt file. You can run your program by typing ./cmake-build-debug/executablename arg1 arg2 arg3 … Figure 7.1: Accessing the command line through CLion. 14 7.1 Accessing the Command-Line 7 MILESTONE 2: COMMAND-LINE INTERFACE To enter debug mode while specifying command-line arguments, in your menu bar, go to Run ⇒ Edit configurations… ⇒ Program arguments, and input the list of arguments you’d like to have called in debug mode. 7.1.2 Visual Studio You can access the Terminal in Visual Studio by going to View ⇒ Terminal. The executable file is typically stored in Visual Studio under a folder named arm64Debug or amd64Debug, or something similar. You can call this executable and optionally pass arguments to it, as shown in Figure 7.2. Figure 7.2: Accessing the command line through Visual Studio. If you would like to open debug mode and still pass arguments in, you can add these arguments in your project settings, as shown in Figure 7.3. Figure 7.3: Altering the command-line arguments used in Debug mode. The option that is modified in the project properties is “Command Arguments”. 7.1.3 Terminal If you would like to test your command-line program using your Terminal, open up your terminal program of choice. On Windows, Windows Terminal is a good option, while on macOS, Terminal.app is a good option. After opening the Terminal, navigate to your project directory using the cd command. Then, use g++ to build your project files using the C++11 standard. Finally, run the executable with the desired arguments by typing ./executablename arg1 arg2 arg3 … 15 7.2 Goals 7 MILESTONE 2: COMMAND-LINE INTERFACE Figure 7.4: Accessing the command line through Terminal on macOS. Terminal applications on other operating systems should have a similar interface. 7.2 Goals The first step in developing your command-line interface is removing the code that automatically runs the ten tasks listed in part two of the assignment. You don’t have to delete the code for these tasks from the project, but they should no longer execute automatically. Before building the command-line interface, build your project and run it with no arguments to make sure none of the ten tasks execute automatically. The interface should not do anything other than what the user asks. If the user supplies only the –help argument, then simply print the help message and exit. If the user asks to multiply two images using the multiply method, then proceed with that method, and then exit. 7.3 Developing The Interface Now that you have access to your command-line, we can begin work on building the actual commandline interface. Your interface should use the argc and argv features of C++ to handle command-line arguments, as discussed in class. This section will discuss what methods you will need to implement in your interface, how arguments are passed to those methods, and the actual specification itself. 7.3.1 Tracking Image Just like the tasks you completed in Part 2, a user can request multiple image operations to be done one after the other, using the output of the previous step as an input to the next step. In order to support this, your command-line interface will need to keep track of a “tracking image.” This is what will be written to the output filename once all the image manipulations have been done on this image. The initial source for this tracking image will be given to you as the second argument (explained more later in subsubsection 7.3.3). It is recommended to load in the data from the source file into your tracking image first. Then read all the image manipulation algorithms requested by the user, in order. These algorithms will take in the tracking image as an input, and then the tracking image will be set to the output of the algorithm. In other words, each image manipulation algorithm will “act” on the tracking image. You can keep track of the tracking image by storing a variable representing your image before the user calls CLI arguments. This variable could be an image class you have written, it could be a vector of pixels, whatever makes sense in the context of the program. Ensure that it can be updated whenever a new image manipulation method is run. Note that this method of implementing the command-line interface is not necessarily required, but the specification was set up to support this style of developing your command-line interface. 16 7.3 Developing The Interface 7 MILESTONE 2: COMMAND-LINE INTERFACE 7.3.2 Methods You will need to implement several methods in your CLI that the user can call. The methods are discussed below. • multiply: This method takes one additional argument, the second image to use in the multiplication process, alongside your tracking image. • subtract: This method takes one additional argument, the second image to use in the subtract algorithm, alongside your tracking image. The first image, the tracking image, will be the top layer. The additional image argument constitutes the bottom layer. • overlay: This method takes one additional argument, the second image to use in the overlay algorithm, alongside your tracking image. The first image, the tracking image, will be the top layer. The additional image argument constitutes the bottom layer. • screen: This method takes one additional argument, the second image to use in the screen algorithm, alongside your tracking image. The first image, the tracking image, will be the bottom layer. The additional image argument constitutes the top layer. • combine: This method is similar to what you did in task 9 of Part 2, where three individual files each provide one channel of the resulting image. This method takes two additional arguments, the source for the green layer (the first additional argument), and the source for the blue layer (the second additional argument). The source for the red layer is the tracking image. • flip: This method takes no additional argument, and simply flips the tracking image. • onlyred: This method takes no additional arguments, and simply retrieves the red channel from the image, similar to how you did in task 8. • onlygreen: This method takes no additional arguments, and simply retrieves the green channel from the image, similar to how you did in task 8. • onlyblue: This method takes no additional arguments, and simply retrieves the blue channel from the image, similar to how you did in task 8. • addred: This method adds a certain value to the red channel of an image. This method takes one additional argument, the amount to add to the red channel. This will need to be converted to an integer. • addgreen: This method adds a certain value to the green channel of an image. This method takes one additional argument, the amount to add to the red channel. This will need to be converted to an integer. • addblue: This method adds a certain value to the blue channel of an image. This method takes one additional argument, the amount to add to the red channel. This will need to be converted to an integer. • scalered: This method scales the red channel of an image. This method takes one additional argument, the amount to scale the red channel. This will need to be converted to an integer. • scalegreen: This method scales the green channel of an image. This method takes one additional argument, the amount to scale the red channel. This will need to be converted to an integer. • scaleblue: This method scales the blue channel of an image. This method takes one additional argument, the amount to scale the red channel. This will need to be converted to an integer. Each of the final six operations listed accept an integer as an additional argument. For addred, addgreen, and addblue, you should be able to accept negative integers. These will decrement the designated channel of each pixel by some value. You should ensure that the pixel value never falls out of the 0-255 range, clamping as appropriately, as described in subsection 6.2. The scalered, scalegreen, and scaleblue operations should accept any integer greater than or equal to zero — you will not be tested with negative numbers for these operations. 7.3.3 General Specification Your program should be able to handle commands in the following format: 17 7.3 Developing The Interface 7 MILESTONE 2: COMMAND-LINE INTERFACE • If no arguments are provided, or if the first and only argument is –help, print the help message. The help message to print is shown as the first command example shown in Listing 7.2. Your help message should be exactly the same for the tests to pass. Note that the final line in the help message is indented with a tab character, not several spaces or another character. You are expected to use this character as well. • The first argument will be the name of the output file. If the argument does not end with .tga, print “Invalid file name.” If this argument is missing, you should proceed with the bullet point above. • The second argument will be the name of the source file for your tracking image. If this argument is not provided, or if the argument does not end with .tga, print “Invalid file name.” If the filename is not a real file, then print “File does not exist.” • The next few arguments describe the first image manipulation method. – The third argument will be the name of the first image manipulation method. If this argument is not provided, or if the method does not exist, print “Invalid method name.” – If the first image manipulation method selected requires additional arguments (such as the “multiply” method, which requires the name of a second file to multiply the first with), then those arguments will be provided after the name of the manipulation algorithm. If the arguments are not provided when they should be, print “Missing argument.” If the first method selected does not require additional methods (such as the “flip” method), then any following arguments will be related to the next image manipulation algorithm. ∗ If the method expects a filename argument, and the argument does not end in .tga, print “Invalid argument, invalid file name.” If the file does not exist, print “Invalid argument, file does not exist.” ∗ If the method expects an integer, but receives something other than an integer, print “Invalid argument, expected number.” • After the first image manipulation method arguments are read, any additional arguments should represent more steps in your program. Unlike the first step, these methods act on the tracking image, and therefore one image supplied to the method will be the output of the previous step. – The first argument of successive methods is the name of the image manipulation method. If the method does not exist, print “Invalid method name.” – Any additional arguments required by the method will be provided next. If an argument is missing, print “Missing argument.” For commands that are not invalid (commands where some image manipulation is successfully done), the output of the command does not matter. You can print whatever you like! Furthermore, your program is expected to return with a return code of zero when no errors occurred in a given command. This just requires you to use return 0; in your main routine when your program completed successfully. You are not expected to return with any specific error code for cases where your command-line interface encountered an error, however, you may find this helpful. 7.3.4 Examples Listing 7.1 highlights how your program should print its help message. Listing 7.1: Example of the desired help message. Note that the indent below “Usage:” is a tab character. bash-5.2$ ./project2.out –help Project 2: Image Processing, Fall 2023 Usage: ./project2.out [output] [firstImage] [method] […] bash-5.2$ ./project2.out Project 2: Image Processing, Fall 2023 Usage: ./project2.out [output] [firstImage] [method] […] 18 7.4 Testing Your Interface 7 MILESTONE 2: COMMAND-LINE INTERFACE Listing 7.2 highlights how your program should be able to complete operations. Note that files referenced in a CLI command can be in a folder, but this is not required. Listing 7.2: Example successful outputs given various arguments. bash-5.2$ ./project2.out output/out.tga input/car.tga multiply input/part1.tga Multiplying input/car.tga and input/part1.tga … … and saving output to output/out.tga! bash-5.2$ ./project2.out output/out.tga car.tga multiply part1.tga screen part2.tga Multiplying car.tga and part1.tga … … and subtracting part2.tga from previous step … … and saving output to output/out.tga! bash-5.2$ ./project2.out output/out.tga input/car.tga flip flip flip flip Flipping input/car.tga … … and flipping output of previous step … … and flipping output of previous step … … and flipping output of previous step … … and saving output to output/out.tga! bash-5.2$ ./project2.out out.tga input/car.tga scalegreen 5 addred 100 scaleblue 0 Scaling the green channel of input/car.tga by 5 … … and adding +100 to red channel of previous step … … and scaling the blue channel of previous step by 0 … … and saving output to out.tga! bash-5.2$ ./project2.out output/out.tga red.tga combine blue.tga green.tga Combining channels of red.tga, blue.tga, and green.tga … … and saving output to output/out.tga! Listing 7.3 shows how your program should emit errors when improper arguments are encountered. Listing 7.3: Example successful outputs given various arguments. bash-5.2$ ./project2.out output/out.tga Invalid file name. bash-5.2$ ./project2.out output/out.tga input/part2.tga weirdmethod Invalid method name. bash-5.2$ ./project2.out output/out.tga input/part2.tga multiply Missing argument. bash-5.2$ ./project2.out output/out.tga input/part2.tga addred notanint Invalid argument, expected number. 7.3.5 Implementation Tips Here are some tips to assist you in your implementation of the command-line interface: • Don’t forget to handle –help and passing no arguments. That should be handled first. • Consume arguments one by one; you should only need to look at each argument once, in order. If an argument is missing, you can print an error right away. • You cannot compare char* to std::string directly. Instead, use std::strcmp(char* value, std::string otherValue), located in the header, which will return zero if the two string representations have the same value. Alternatively, you can cast construct an std::string from the argument. • Segmentation faults commonly occur from trying to access an array index that does not exist. • When handling receiving numbers via the command line, you can use std::stoi, which will convert a string representing a numeric value to an int. It will throw a std::invalid_argument exception if no conversion can be performed (aka, if the argument was not a number). 7.4 Testing Your Interface Once you feel confident with your interface, test it as a replacement for your tasks! Try running task 1 from subsection 6.3 using the command-line, and ensuring that the output file is the same as the example task 1 file. This might look like: 19 7.5 Submission 7 MILESTONE 2: COMMAND-LINE INTERFACE Listing 7.4: Running task 1 through the command-line. bash-5.2$ ./project2.out output/part1.tga input/layer1.tga multiply input/pattern1.tga For best results, you should ensure that all ten tasks can be executed through the command-line (this might help with a later part of the project… hint, hint). Furthermore, ensure that your program reacts appropriately when you supply invalid or missing arguments. Currently, to test your tasks you have to re-type the executable name followed by a certain set of command-line arguments to run each task. In the next part of the assignment, we’ll use a Makefile to store these commands in a file. This will allow us to run commands that generate all ten parts of the project using just the command line. 7.5 Submission At this point, you should have a completed command-line interface to your image manipulation program. It should be able to handle variable file names and variable arguments, and should not run anything automatically. This should allow to pass several of the tasks you will be tested on. Again, Gradescope will be used to test out your command-line interface. There are three areas you will be tested on. Like Milestone 1, there are ten individual tasks, some of which you will add to your Makefile using your new command line arguments, and others will be compiled by Gradescope after you submit your submission. You won’t know some of the arguments your program is tested with. • Help Message and Error Messages: Your program should produce the correct help message when requested, and your program should produce the correct error messages when invalid arguments are supplied. • Tasks 11-13: As described below, you will add three new tasks into your Makefile under a new rule named tasks. Gradescope will expect that after this rule is called, the images for tasks 11-13 will be generated. • Tasks 14-20: Gradescope will call your command-line interface with variable arguments. Tasks 14-16 will have their arguments shown, while tasks 17-20 will have their arguments hidden. Your program should be able to handle these arguments and produce the correct images. For tasks 11-13, you will add one new rule to your Makefile: tasks. This rule should call your executable with the appropriate command-line arguments to generate the three images below. Gradescope will call make tasks to test your implementation. 11. Multiply circles.tga with layer1.tga. 12. Flip (or, rotate 180 degrees) layer1.tga. 13. Subtract layer2.tga (bottom layer) from layer1.tga (top layer). Tasks 14-16 will be executed by Gradescope calling your executable with the necessary command-line arguments. For debug purposes, they are listed below: 14. Flip car.tga four times. 15. Subtract layer1.tga from car.tga, then multiply the output with circles.tga, then flip it. 16. Given car.tga, scale its blue channel by 3, its red channel by 2, and its green channel by 0. Tasks 17-20 have been hidden from you. Gradescope will call your executable with the necessary command-line arguments. 20 A COMPATIBILITY A Compatibility Once you believe you have completed the project, ensure that the following compatibility requirements are met. Failure to follow these principles could break your project when it is being graded, and will cause you to lose 10 points. A.1 Paths For compatibility purposes, you adhere to the following principles about paths. This applies for all aspects of the project, including the source code itself, and your Makefile. • Use forward slashes, not backward slashes: On some operating systems, especially Windows, backslashes can be used to specify folder names in paths. While this format is supported on some operating systems, this is not supported on all operating systems. Therefore, please use forward slashes. – Bad: folderfilename.txt – Good: folder/filename.txt • Use relative paths, not absolute paths: Absolute paths link to a file on your computer, and your computer only. Frequently, absolute file paths will include specific details about your computer that will break your program on other people’s computers. Therefore, use relative paths instead, which specify a filename relative to your current directory. Your paths should start with input/, output/, or examples/. – Bad: C:/Users/rubberduck/College/COP3503C/Project 2/input/car.tga – Good: input/car.tga A.2 Testing on Unix Your code will be run and graded in a Unix environment. Specific operating systems are out of the scope of this course, but generally, Unix refers to a Linux-like operating system. Most importantly, not Windows. You should ensure that your code is able to run in a Unix environment before submitting it. The recommended way to do this is to make a pretend submission (as described in Appendix C) and send this submission to a Unix computer. We recommend testing your code on the CISE computers in the CSE building on-campus, or using the CISE Thunder virtual machines. A.2.1 Testing on CISE Computers The CISE department at the university hosts a suite of Ubuntu servers which you can use for personal projects, development, and research. You can use these computers to test if your submission will be graded correctly. If you don’t already have an account with the CISE department, you will need to make an account, as listed on this page (you will need to sign in with your Gatorlink to view the page). Your account approval might take a few days to process, so we recommend doing this step early, even if you haven’t finished the project. After you have access to a CISE account, you can test your code. To ensure you can signin, try using SSH to enter your virtual machine: Listing A.1: Entering the virtual machine using SSH. bash-5.2$ ssh [email protected] If you are able to enter your virtual machine after entering your Gatorlink password, you’re all set! Next, let’s move your files over to your virtual machine. You can use a program like sftp or scp. In our examples, we’ll use scp: 21 B GRADING Listing A.2: Copying the project zip file to the remote machine using scp. bash-5.2$ scp lastname.firstname.project2.zip [email protected]:project2.zip If this command does not produce any errors, you should have successfully transferred your project to your virtual machine, under the name of project2.zip. Congrats! You should also transfer the input/ and examples/ folders over as well so your program can reference these files when running. Now, re-enter your virtual machine using SSH, as shown in Listing A.1. Unzip your project using the unzip command and ensure that all files are present. Move your folders into the right structure, and then run make. Ensure that your executable builds without error. Run the executable using the proper command line arguments, and ensure that there are no differences between the files appearing in your output folder and those in the examples/ folder using the diff command as shown in Figure 6.2. If there are no differences, then you’ve completed the project. B Grading Table B.1 explains how the entire project will be graded. Please note that the rubric has been slightly modified since the original assignment release, but has remained consistent since the introduction of Gradescope. Note that the rubric was slightly modified to ensure that the autograder could accurately, consistently, and fairly grade student submissions. Several of these changes benefit students by making it easier to gain points. Task / Milestone Points Description Milestone 1 (80 points) Tasks 1-10 70 7 points possible per task. Complete task with matching pixels and header. Proportional points for pixel accuracy. Makefile 10 Makefile development: 5 points for correct executable, 5 points for make run completion. Deductions 0 Any deductions found from the Deductions section deduct 10 points. Milestone 2 (70 points) Command-Line Tests 6 Tests the help message and error messages. Tasks 11-13 15 5 points per task. Command-line tasks generated by your Makefile. Tasks 14-20 49 7 points per task. Command-line tasks generated by Gradescope. Total 150 Table B.1: Rubric for the entire project. B.1 Deductions 10 points will be deducted from the final project score if any of the following are true: • Your submission contains any other files than the src/ folder and your Makefile. (Explained more in the next section…) • Your Makefile build command does not use the C++11 standard, builds code from the wrong location, or includes header files. • Your Makefile is incorrectly named, or uses an improper encoding. Your Makefile cannot be named Makefile.txt, Makefile.rst, or anything similar, and it must be a plain-text file (ie, not created in Microsoft Word or macOS Rich Text Editor). No points will be deducted for the following: 22 C SUBMITTING TO GRADESCOPE • Printing output when running a successful image manipulation (ie, where the user’s command input does not fail). • Warnings appearing when compiling the executable. Be careful, however, as these warnings might indicate that your program will fail under specific circumstances which you might want to be aware of. • Writing code without comments. You may add helpful comments if you’d like, but commenting or explaining your code is not required. C Submitting to Gradescope If you’ve made it to this part, you might almost be done with your project! That’s exciting!! Let’s talk about how to actually submit your project to Gradescope. Gradescope is a machine grader that is used to test and grade your submission automatically. The grader has over fifty tests, and conforms to the rubric specified above. Your submission should be one zip file, containing: • Any source and header files, placed in a folder named src/. There should be only C++ files in here. Delete any object files or other files. • The Makefile you created to build your project. You do not need to include output/, input/, nor examples/ in your submission. These will be provided in the grading environment for you. An example structure is shown in Figure C.1. File.h and File.cpp are example alternate source files. lastname.firstname.project2.zip Makefile src/ main.cpp File.h File.cpp Figure C.1: The appropriate tree structure for your submission. Do not include any other files in your submission. This is not limited to the following: project images, executables, IDE-specific configurations, PDF files, etc. To submit to Gradescope, visit the Project 2 assignment on Canvas. At the bottom of the page, you will find a window to submit your assignment. Choose the zip file to upload, ensure that all files listed match the files you would like to submit, and press “Upload,” as shown in Figure C.2. 23 C.1 Gradescope Environment C SUBMITTING TO GRADESCOPE Figure C.2: Uploading a submission through Gradescope. After pressing “Upload,” wait some time for the autograder complete. You can analyze the tests that passed and did not pass using the right sidebar. C.1 Gradescope Environment The Gradescope testing environment consists of four parts: 1. Deductions: Testing to see if your submission contains deductions. 2. Building: Using your Makefile to build your submission and generate tasks 1-10. If you do not provide a Makefile, your submission will be supplied one, but you will receive a zero for the Makefile in the rubric. This allows your submission to build even if you have not yet completed the part 4. 3. Tests: Testing tasks 1-10 for Milestone 1, or tasks 11-20 for Milestone 2. After each test is run, an accuracy comparison is run on the output image. The testing environment has been created to help ensure each student receives a fair amount of points. Here are some further tips on the testing environment: • If you submit your Makefile and src/ folder in an enclosing folder, your submission may not be graded correctly. Ensure that your submission appears as shown above before submitting. • When testing your command-line interface, Gradescope may (or may not) reference files in an enclosing folder. For example, ./project2.out out.tga input/test.tga flip is a valid command, as is ./project2.out out.tga test.tga flip. • Some TGA files tested by Gradescope are not provided to you. • Many of the commands tested in Gradescope are shown to the student. However, some of the commands tested are hidden and not accessible by the student. • Some TGA files tested by Gradescope have lengths and widths other than 512×512. • If make or an individual test takes a significant amount of time, the command will be aborted, and that test will receive zero points. This helps prevent infinite loops. 24 D DOCUMENT REVISIONS D Document Revisions Table D.1 shows the edits have been made to the document since its initial release. Revision Date Section Changes 1 March 6th, 2024 — Initial release. Table D.1: Changes that have occurred in the document since its initial release.

Reviews

There are no reviews yet.

Only logged in customers who have purchased this product may leave a review.

Shopping Cart
[SOLVED] Project 2: image processing cop3503c: programming fundamentals ii
$25