From 14e856c579929a3d4ea59a6af3ba1d128ec63351 Mon Sep 17 00:00:00 2001 From: Aryan Kale <47396822+akale22@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:32:34 -0400 Subject: [PATCH 1/2] Add architecture and productionization breakdown document --- ARCHITECTURE_AND_PRODUCTION.md | 194 +++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 ARCHITECTURE_AND_PRODUCTION.md diff --git a/ARCHITECTURE_AND_PRODUCTION.md b/ARCHITECTURE_AND_PRODUCTION.md new file mode 100644 index 0000000..5e29dd9 --- /dev/null +++ b/ARCHITECTURE_AND_PRODUCTION.md @@ -0,0 +1,194 @@ +# Image Manipulator: Architecture, How It Works, and Productionization Guide + +## 1) What this project is + +This is a Java image processing app with three entry modes: +- **Batch script mode** (`-file`): executes commands from a script file. +- **Interactive text mode** (`-text`): executes commands entered in a REPL-like console. +- **GUI mode** (no args): launches a Swing interface with buttons for operations. + +The design follows an MVC shape: +- **Model**: image state + image operations. +- **Controllers**: parse/route user actions to model operations. +- **Views**: text output or Swing UI rendering. + +## 2) Runtime flow + +### Program startup +`ImageProcessingProgram.main` creates one `ImageModelImpl` and then chooses a controller/view pair based on args. + +- `-file`: reads a script file into a `Readable`, starts text controller. +- `-text`: reads from stdin, starts text controller. +- no args: starts Swing GUI view + GUI controller. + +### Text mode flow +1. `ImageTextControllerImpl` initializes a command map from command string -> command object constructor. +2. In `start()`, it loops through tokens from `Scanner`. +3. It ignores comment lines beginning with `#`. +4. `q`/`quit` exits. +5. Known commands build an `ImageCommand` and call `execute(model)`. +6. Unknown/invalid command sequences are rendered as errors. + +### GUI mode flow +1. `ImageGUIViewImpl` constructs Swing components (buttons, image panel, histogram panel). +2. `ImageGUIControllerImpl` registers as an `ActionListener`. +3. Clicking a button triggers `actionPerformed`, which dispatches to helpers (`brightenHelper`, `flipHelper`, etc.). +4. Operations are always performed on a single model key: `"guiImage"`. +5. After each successful op, `updateView()` refreshes displayed image + histogram. + +## 3) Core domain model internals + +## Image storage strategy +`ImageModelImpl` stores images in `HashMap`. + +Benefits: +- Easy named checkpoints in text mode (`source`, `source-blur`, etc.). +- Constant-time lookup by logical name. + +Tradeoff: +- In-memory only; no persistence or eviction strategy. + +## Operation delegation +`ImageModelImpl` does orchestration and delegates pixel math to `ImageImpl`: +- component greyscale +- brighten/darken +- flips +- blur/sharpen (convolution) +- greyscale/sepia (3x3 color transform) +- downsize + +This keeps controller logic thin and image math encapsulated. + +## Pixel/image immutability pattern +`ImageImpl` fields are final (`width`, `height`, `maxValue`, `pixels`), and most ops create new pixel arrays and return new `ImageImpl` instances. + +Notable detail: `getPixels()` deep-copies pixel values into new `RGBPixel` objects before returning, so callers don’t mutate internal arrays directly. + +## 4) Supported operations and algorithm sketch + +- **Greyscale component**: compute one scalar per pixel from channel/value/intensity/luma and write to R=G=B. +- **Brighten/darken**: add/subtract scalar per channel with clamping. +- **Flip**: swap pairs across horizontal or vertical axis. +- **Blur/sharpen**: convolution over each pixel with fixed kernel. +- **Color transformation**: multiply pixel RGB vector by 3x3 matrix. +- **Downsize**: maps new coordinates to old image; if non-integer, averages surrounding neighbors. + +Complexity rough order: +- Per-pixel ops (brighten, component, transform): **O(W×H)**. +- Convolution (K×K kernel): **O(W×H×K²)**. +- Downsize: about **O(newW×newH)** with local neighborhood sampling. + +## 5) File I/O architecture + +The command layer chooses reader/writer by extension: +- `Load`/`Save` switch on last 3 chars (`ppm`, `png`, `jpg`, `bmp`). + +I/O strategy classes: +- `AbstractImageInputOutput`: common `ImageIO`-based load/save (BMP/PNG/JPG). +- `PPMImageInputOutput`: custom ASCII P3 parser/writer. + +This is a clean “strategy-like” structure, but extension detection can be made more robust (e.g., case-insensitive, full suffix parsing, MIME sniffing). + +## 6) GUI subsystem behavior + +`ImageGUIViewImpl` is straightforward Swing: +- Left column: load/save + manipulation buttons. +- Right side: image preview in scroll pane + histogram panel. + +`HistogramImpl` computes 256-bin frequencies for red/green/blue/intensity and draws polylines. + +## 7) Current strengths + +- Good educational MVC separation. +- Command pattern for text mode is easy to extend. +- Clear operation library in model/image layer. +- Good test suite footprint in `test/`. + +## 8) Current bottlenecks / risks for scaling + +1. **Single-process, in-memory design** + - No persistence, caching, or streaming. + - Large images can stress heap. + +2. **Synchronous execution** + - Heavy ops run inline; no background jobs. + - GUI may feel blocked on large images. + +3. **Error handling consistency** + - Some places throw; some print to stdout. + - No typed error model or error codes. + +4. **Extension-based format detection** + - Last-3-char parsing is brittle (`jpeg`, uppercase, malformed names). + +5. **Limited observability** + - No structured logs, metrics, tracing, profiling hooks. + +6. **No deployment boundary** + - This is a desktop app architecture, not a service API architecture. + +## 9) How to productionize (practical staged roadmap) + +### Stage A: Hardening in current architecture +- Add a build tool (`Gradle` or `Maven`) and pin dependencies. +- Add CI: compile + unit tests + static analysis. +- Standardize exceptions and user-facing messages. +- Add image size guards + memory limits. +- Improve file type handling (`Path`, robust extension parsing). + +### Stage B: Performance & correctness +- Replace exception-driven bounds handling in convolution with explicit boundary checks. +- Add benchmark tests (small/medium/large images). +- Introduce optional parallel processing for independent rows/tiles. +- Add undo stack for GUI and memory-aware eviction policy. + +### Stage C: Service/API extraction +Split into modules: +1. `image-core` (pure operations, no UI) +2. `image-io` (formats + decoding) +3. `image-api` (REST/gRPC endpoints) +4. `image-worker` (async job execution) +5. `image-ui` (optional web/desktop client) + +Then expose APIs like: +- `POST /images` upload +- `POST /images/{id}/operations` run one op +- `POST /pipelines` run multiple ops +- `GET /jobs/{id}` poll async job status +- `GET /images/{id}` download output + +### Stage D: Cloud scale patterns +- **Storage**: object storage for originals/outputs. +- **Queue**: async jobs via message queue. +- **Workers**: autoscaled stateless processors. +- **DB**: metadata (job state, user ownership, audit trail). +- **CDN**: serve generated assets. +- **Caching**: deduplicate repeated transformations. + +### Stage E: Security & multi-tenant readiness +- AuthN/AuthZ (JWT/OAuth). +- Rate limits + quotas. +- Malware scanning and file validation on upload. +- Strict resource ceilings per job. +- Full audit logging. + +## 10) Concrete design recommendations if you were starting this as a startup product + +- Keep this repo’s **operation logic** as the nucleus. +- Build a stateless processing microservice around it. +- Treat each transform request as an immutable job. +- Store images outside process memory and pass references. +- Make the UI a client, not the compute host. +- Add product features: operation history, presets, batch pipelines, webhooks. + +## 11) “If I had 2 weeks” implementation plan + +1. Add Maven + JUnit runner + CI pipeline. +2. Extract `model/image` into a reusable library module. +3. Add a minimal Spring Boot API with upload + apply-op + download. +4. Use local disk/S3-compatible storage abstraction. +5. Add async execution with a worker thread pool. +6. Add health endpoint + logs + basic Prometheus metrics. +7. Containerize with Docker and ship a compose stack. + +That gives you a credible v1 production architecture while preserving your current logic. From 60e27489f2ed798bf7bd620a8a3fa6eb16c9760f Mon Sep 17 00:00:00 2001 From: Aryan Kale <47396822+akale22@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:44:18 -0400 Subject: [PATCH 2/2] Add modern image features and optimize convolution path --- README.md | 16 ++- USEME.md | 13 +- src/controller/commands/AutoContrast.java | 26 ++++ src/controller/commands/EdgeDetect.java | 29 ++++ .../commands/ImageTransformUtil.java | 128 ++++++++++++++++++ src/controller/commands/RotateClockwise.java | 26 ++++ .../commands/RotateCounterClockwise.java | 26 ++++ .../gui/ImageGUIControllerImpl.java | 64 +++++++++ .../text/ImageTextControllerImpl.java | 9 ++ src/model/image/ImageImpl.java | 30 ++-- src/view/gui/ImageGUIViewImpl.java | 31 ++++- 11 files changed, 377 insertions(+), 21 deletions(-) create mode 100644 src/controller/commands/AutoContrast.java create mode 100644 src/controller/commands/EdgeDetect.java create mode 100644 src/controller/commands/ImageTransformUtil.java create mode 100644 src/controller/commands/RotateClockwise.java create mode 100644 src/controller/commands/RotateCounterClockwise.java diff --git a/README.md b/README.md index 29b3fb8..e5859d0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ ### **Description**
Image Manipulator is a Java image processing application with text-based, GUI, and command line functionality. It currently contains 10+ features including: uploading an image, saving an image to a device, flipping (vertical and horizontal), brightening/dimming, greyscaling, blurring, sharpening, applying a sepia filter, and downsizing an image. The application currently allows users to load or save an image in the formats .jpg, jpng, .ppm, and .bmp. When using the GUI, a histogram also displays the frequencies of each value (between 0-255) for the red, green, and blue values of each pixel. All functionality has been properly tested with JUnit. +Additional modern features include edge detection, auto contrast stretching, and 90° rotation in both directions. + ### **Final Product**
The GUI is shown below. @@ -75,6 +77,18 @@ The lines above the scripts represent explanations of what the scripts themselv ####applying a sepia tone to the image of the family
`sepia family family-sepia` +####detect edges in the image of the family
+`edge-detect family family-edges` + +####apply automatic per-channel contrast stretching to the image of the family
+`auto-contrast family family-contrast` + +####rotate the image clockwise by 90 degrees
+`rotate-clockwise family family-rotated-cw` + +####rotate the image counterclockwise by 90 degrees
+`rotate-counterclockwise family family-rotated-ccw` + ####save family-brighter in the ppm format in the res/family/ directory
`save res/family/family-brighter.ppm family-brighter` @@ -108,4 +122,4 @@ The lines above the scripts represent explanations of what the scripts themselv ####q to quit the program
`q`
-
\ No newline at end of file +
diff --git a/USEME.md b/USEME.md index ffebc09..1f42228 100644 --- a/USEME.md +++ b/USEME.md @@ -64,6 +64,18 @@ Click the button to apply the sepia color transformation to the image. ### **Downsize** Click the button and enter positive integers for how much you would like to decrease the width and height percentage of the image. + +### **Edge Detect** +Click the button to highlight image edges. + +### **Auto Contrast** +Click the button to stretch contrast across RGB channels automatically. + +### **Rotate Clockwise** +Click the button to rotate the image by 90 degrees clockwise. + +### **Rotate Counterclockwise** +Click the button to rotate the image by 90 degrees counterclockwise.
### **Saving an Image** @@ -81,4 +93,3 @@ Make sure that you adhere to the appropriate constraints when performing an imag #### Aryan Kale - kale.ar@northeastern.edu
#### Anshul Shirude - shirude.a@northeastern.edu
- diff --git a/src/controller/commands/AutoContrast.java b/src/controller/commands/AutoContrast.java new file mode 100644 index 0000000..666e3f0 --- /dev/null +++ b/src/controller/commands/AutoContrast.java @@ -0,0 +1,26 @@ +package controller.commands; + +import model.ImageModel; +import model.image.Image; + +/** + * Automatically stretches contrast for all channels independently. + */ +public class AutoContrast extends AbstractImageCommand { + + /** + * Constructs this command. + * + * @param in the image name to adjust + * @param out the name for the contrast-adjusted image + */ + public AutoContrast(String in, String out) { + super(in, out); + } + + @Override + public void execute(ImageModel model) { + Image image = model.getImage(in); + model.setImage(out, ImageTransformUtil.autoContrast(image)); + } +} diff --git a/src/controller/commands/EdgeDetect.java b/src/controller/commands/EdgeDetect.java new file mode 100644 index 0000000..01d7e7e --- /dev/null +++ b/src/controller/commands/EdgeDetect.java @@ -0,0 +1,29 @@ +package controller.commands; + +import model.ImageModel; + +/** + * Applies a Laplacian edge-detection kernel to the image. + */ +public class EdgeDetect extends AbstractImageCommand { + + /** + * Constructs this command. + * + * @param in the image name to detect edges on + * @param out the name for the edge-detected image + */ + public EdgeDetect(String in, String out) { + super(in, out); + } + + @Override + public void execute(ImageModel model) { + double[][] kernel = new double[][] + {{-1, -1, -1}, + {-1, 8, -1}, + {-1, -1, -1}}; + + model.setImage(out, model.getImage(in).filter(kernel)); + } +} diff --git a/src/controller/commands/ImageTransformUtil.java b/src/controller/commands/ImageTransformUtil.java new file mode 100644 index 0000000..c1a571c --- /dev/null +++ b/src/controller/commands/ImageTransformUtil.java @@ -0,0 +1,128 @@ +package controller.commands; + +import model.image.Image; +import model.image.ImageImpl; +import model.image.Pixel; +import model.image.RGBPixel; + +/** + * A utility class with reusable image transformations used by command implementations. + */ +public final class ImageTransformUtil { + + private ImageTransformUtil() { + // utility class + } + + /** + * Rotates the given image by 90 degrees clockwise. + * + * @param image the image to rotate + * @return a rotated image + */ + public static Image rotate90Clockwise(Image image) { + if (image == null) { + throw new IllegalArgumentException("Image cannot be null!"); + } + + Pixel[][] oldPixels = image.getPixels(); + int width = image.getWidth(); + int height = image.getHeight(); + Pixel[][] newPixels = new RGBPixel[height][width]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + Pixel p = oldPixels[x][y]; + int newX = height - 1 - y; + int newY = x; + newPixels[newX][newY] = new RGBPixel(p.getRed(), p.getGreen(), p.getBlue()); + } + } + + return new ImageImpl(height, width, image.getMaxValue(), newPixels); + } + + /** + * Rotates the given image by 90 degrees counterclockwise. + * + * @param image the image to rotate + * @return a rotated image + */ + public static Image rotate90CounterClockwise(Image image) { + if (image == null) { + throw new IllegalArgumentException("Image cannot be null!"); + } + + Pixel[][] oldPixels = image.getPixels(); + int width = image.getWidth(); + int height = image.getHeight(); + Pixel[][] newPixels = new RGBPixel[height][width]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + Pixel p = oldPixels[x][y]; + int newX = y; + int newY = width - 1 - x; + newPixels[newX][newY] = new RGBPixel(p.getRed(), p.getGreen(), p.getBlue()); + } + } + + return new ImageImpl(height, width, image.getMaxValue(), newPixels); + } + + /** + * Performs per-channel contrast stretching by mapping each channel min->0 and max->255. + * + * @param image the image to adjust + * @return an image with stretched contrast + */ + public static Image autoContrast(Image image) { + if (image == null) { + throw new IllegalArgumentException("Image cannot be null!"); + } + + Pixel[][] oldPixels = image.getPixels(); + int width = image.getWidth(); + int height = image.getHeight(); + + int minRed = image.getMaxValue(); + int minGreen = image.getMaxValue(); + int minBlue = image.getMaxValue(); + int maxRed = 0; + int maxGreen = 0; + int maxBlue = 0; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + Pixel p = oldPixels[x][y]; + minRed = Math.min(minRed, p.getRed()); + minGreen = Math.min(minGreen, p.getGreen()); + minBlue = Math.min(minBlue, p.getBlue()); + maxRed = Math.max(maxRed, p.getRed()); + maxGreen = Math.max(maxGreen, p.getGreen()); + maxBlue = Math.max(maxBlue, p.getBlue()); + } + } + + Pixel[][] newPixels = new RGBPixel[width][height]; + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + Pixel p = oldPixels[x][y]; + int newRed = stretchComponent(p.getRed(), minRed, maxRed, image.getMaxValue()); + int newGreen = stretchComponent(p.getGreen(), minGreen, maxGreen, image.getMaxValue()); + int newBlue = stretchComponent(p.getBlue(), minBlue, maxBlue, image.getMaxValue()); + newPixels[x][y] = new RGBPixel(newRed, newGreen, newBlue); + } + } + + return new ImageImpl(width, height, image.getMaxValue(), newPixels); + } + + private static int stretchComponent(int value, int min, int max, int maxValue) { + if (max == min) { + return value; + } + + return (int) Math.round(((value - min) / (double) (max - min)) * maxValue); + } +} diff --git a/src/controller/commands/RotateClockwise.java b/src/controller/commands/RotateClockwise.java new file mode 100644 index 0000000..683285f --- /dev/null +++ b/src/controller/commands/RotateClockwise.java @@ -0,0 +1,26 @@ +package controller.commands; + +import model.ImageModel; +import model.image.Image; + +/** + * Rotates an image by 90 degrees clockwise. + */ +public class RotateClockwise extends AbstractImageCommand { + + /** + * Constructs this command. + * + * @param in the image name to rotate + * @param out the name to save the rotated image as + */ + public RotateClockwise(String in, String out) { + super(in, out); + } + + @Override + public void execute(ImageModel model) { + Image image = model.getImage(in); + model.setImage(out, ImageTransformUtil.rotate90Clockwise(image)); + } +} diff --git a/src/controller/commands/RotateCounterClockwise.java b/src/controller/commands/RotateCounterClockwise.java new file mode 100644 index 0000000..890c7fd --- /dev/null +++ b/src/controller/commands/RotateCounterClockwise.java @@ -0,0 +1,26 @@ +package controller.commands; + +import model.ImageModel; +import model.image.Image; + +/** + * Rotates an image by 90 degrees counterclockwise. + */ +public class RotateCounterClockwise extends AbstractImageCommand { + + /** + * Constructs this command. + * + * @param in the image name to rotate + * @param out the name to save the rotated image as + */ + public RotateCounterClockwise(String in, String out) { + super(in, out); + } + + @Override + public void execute(ImageModel model) { + Image image = model.getImage(in); + model.setImage(out, ImageTransformUtil.rotate90CounterClockwise(image)); + } +} diff --git a/src/controller/gui/ImageGUIControllerImpl.java b/src/controller/gui/ImageGUIControllerImpl.java index df7f58b..a83b464 100644 --- a/src/controller/gui/ImageGUIControllerImpl.java +++ b/src/controller/gui/ImageGUIControllerImpl.java @@ -9,6 +9,10 @@ import controller.commands.Load; import controller.commands.Save; +import controller.commands.AutoContrast; +import controller.commands.EdgeDetect; +import controller.commands.RotateClockwise; +import controller.commands.RotateCounterClockwise; import model.ImageModel; import model.enums.FlipType; import model.enums.GreyscaleComponentType; @@ -109,6 +113,18 @@ public void actionPerformed(ActionEvent event) { this.downsizeHelper(); break; } + case "edge detect": + this.edgeDetectHelper(); + break; + case "auto contrast": + this.autoContrastHelper(); + break; + case "rotate clockwise": + this.rotateClockwiseHelper(); + break; + case "rotate counterclockwise": + this.rotateCounterClockwiseHelper(); + break; default: throw new IllegalArgumentException("Invalid action event!"); } @@ -350,4 +366,52 @@ private void downsizeHelper() { " (including 0)!"); } } + + /** + * A helper method for carrying out edge detection functionality. + */ + private void edgeDetectHelper() { + try { + new EdgeDetect("guiImage", "guiImage").execute(model); + this.updateView(); + } catch (IllegalArgumentException e) { + view.renderErrorMessage("An image must be loaded before edges can be detected!"); + } + } + + /** + * A helper method for carrying out auto contrast functionality. + */ + private void autoContrastHelper() { + try { + new AutoContrast("guiImage", "guiImage").execute(model); + this.updateView(); + } catch (IllegalArgumentException e) { + view.renderErrorMessage("An image must be loaded before auto contrast can be applied!"); + } + } + + /** + * A helper method for carrying out 90 degree clockwise rotation. + */ + private void rotateClockwiseHelper() { + try { + new RotateClockwise("guiImage", "guiImage").execute(model); + this.updateView(); + } catch (IllegalArgumentException e) { + view.renderErrorMessage("An image must be loaded before it can be rotated!"); + } + } + + /** + * A helper method for carrying out 90 degree counterclockwise rotation. + */ + private void rotateCounterClockwiseHelper() { + try { + new RotateCounterClockwise("guiImage", "guiImage").execute(model); + this.updateView(); + } catch (IllegalArgumentException e) { + view.renderErrorMessage("An image must be loaded before it can be rotated!"); + } + } } diff --git a/src/controller/text/ImageTextControllerImpl.java b/src/controller/text/ImageTextControllerImpl.java index 2dd6d31..dbc62bc 100644 --- a/src/controller/text/ImageTextControllerImpl.java +++ b/src/controller/text/ImageTextControllerImpl.java @@ -9,11 +9,15 @@ import controller.commands.Blur; import controller.commands.Brighten; import controller.commands.Darken; +import controller.commands.EdgeDetect; import controller.commands.Flip; import controller.commands.GreyscaleColorTransformation; import controller.commands.GreyscaleComponent; import controller.commands.ImageCommand; import controller.commands.Load; +import controller.commands.AutoContrast; +import controller.commands.RotateClockwise; +import controller.commands.RotateCounterClockwise; import controller.commands.Save; import controller.commands.Sepia; import controller.commands.Sharpen; @@ -84,6 +88,11 @@ private void setCommands() { knownCommands.put("sharpen", s -> new Sharpen(s.next(), s.next())); knownCommands.put("greyscale", s -> new GreyscaleColorTransformation(s.next(), s.next())); knownCommands.put("sepia", s -> new Sepia(s.next(), s.next())); + knownCommands.put("edge-detect", s -> new EdgeDetect(s.next(), s.next())); + knownCommands.put("auto-contrast", s -> new AutoContrast(s.next(), s.next())); + knownCommands.put("rotate-clockwise", s -> new RotateClockwise(s.next(), s.next())); + knownCommands.put("rotate-counterclockwise", + s -> new RotateCounterClockwise(s.next(), s.next())); } @Override diff --git a/src/model/image/ImageImpl.java b/src/model/image/ImageImpl.java index 484dbf8..63fa272 100644 --- a/src/model/image/ImageImpl.java +++ b/src/model/image/ImageImpl.java @@ -270,32 +270,28 @@ public Image filter(double[][] kernel) throws IllegalArgumentException { * @return a new Pixel at the specified location after the filter has been applied */ private Pixel applyKernelToPixel(int i, int j, double[][] kernel) { - int red = 0; - int green = 0; - int blue = 0; + double red = 0; + double green = 0; + double blue = 0; int matrixSize = kernel.length; for (int r = 0; r < matrixSize; r++) { for (int c = 0; c < matrixSize; c++) { - try { - red += kernel[r][c] * pixels[i + (r - (matrixSize / 2))][j + (c - (matrixSize / 2))] - .getRed(); - green += kernel[r][c] * pixels[i + (r - (matrixSize / 2))][j + (c - (matrixSize / 2))] - .getGreen(); - blue += kernel[r][c] * pixels[i + (r - (matrixSize / 2))][j + (c - (matrixSize / 2))] - .getBlue(); - } catch (ArrayIndexOutOfBoundsException e) { - // DO NOTHING if we are trying to access an index out of bounds because that means - // that that spot doesn't exist and doesn't contribute to the new value of a component + int pixelX = i + (r - (matrixSize / 2)); + int pixelY = j + (c - (matrixSize / 2)); + + if (pixelX >= 0 && pixelX < this.width && pixelY >= 0 && pixelY < this.height) { + red += kernel[r][c] * pixels[pixelX][pixelY].getRed(); + green += kernel[r][c] * pixels[pixelX][pixelY].getGreen(); + blue += kernel[r][c] * pixels[pixelX][pixelY].getBlue(); } } } - red = this.enforceConstraints(red); - green = this.enforceConstraints(green); - blue = this.enforceConstraints(blue); - return new RGBPixel(red, green, blue); + return new RGBPixel(this.enforceConstraints((int) Math.round(red)), + this.enforceConstraints((int) Math.round(green)), + this.enforceConstraints((int) Math.round(blue))); } @Override diff --git a/src/view/gui/ImageGUIViewImpl.java b/src/view/gui/ImageGUIViewImpl.java index 652f026..cd3a2a2 100644 --- a/src/view/gui/ImageGUIViewImpl.java +++ b/src/view/gui/ImageGUIViewImpl.java @@ -48,6 +48,10 @@ public class ImageGUIViewImpl extends JFrame implements ImageGUIView { private final JButton greyscaleButton; private final JButton sepiaButton; private final JButton downsizeButton; + private final JButton edgeDetectButton; + private final JButton autoContrastButton; + private final JButton rotateClockwiseButton; + private final JButton rotateCounterClockwiseButton; private final JPanel mainPanel; private final JLabel imageLabel; private final HistogramImpl histogram; @@ -82,7 +86,7 @@ public ImageGUIViewImpl() { // creating the manipulation button panel JPanel manipulationButtonPanel = new JPanel(); - manipulationButtonPanel.setLayout(new GridLayout(15, 1)); + manipulationButtonPanel.setLayout(new GridLayout(19, 1)); manipulationButtonPanel.setBorder(BorderFactory.createTitledBorder("Manipulations")); buttonPanel.add(manipulationButtonPanel); @@ -171,6 +175,26 @@ public ImageGUIViewImpl() { downsizeButton.setActionCommand("downsize"); manipulationButtonPanel.add(downsizeButton); + // adding edge detect button + edgeDetectButton = new JButton("edge detect"); + edgeDetectButton.setActionCommand("edge detect"); + manipulationButtonPanel.add(edgeDetectButton); + + // adding auto contrast button + autoContrastButton = new JButton("auto contrast"); + autoContrastButton.setActionCommand("auto contrast"); + manipulationButtonPanel.add(autoContrastButton); + + // adding rotate clockwise button + rotateClockwiseButton = new JButton("rotate clockwise"); + rotateClockwiseButton.setActionCommand("rotate clockwise"); + manipulationButtonPanel.add(rotateClockwiseButton); + + // adding rotate counterclockwise button + rotateCounterClockwiseButton = new JButton("rotate counterclockwise"); + rotateCounterClockwiseButton.setActionCommand("rotate counterclockwise"); + manipulationButtonPanel.add(rotateCounterClockwiseButton); + // making a panel for the image and the histogram JPanel mediaPanel = new JPanel(); mediaPanel.setLayout(new BoxLayout(mediaPanel, BoxLayout.Y_AXIS)); @@ -234,6 +258,10 @@ public void setListener(ActionListener listener) { greyscaleButton.addActionListener(listener); sepiaButton.addActionListener(listener); downsizeButton.addActionListener(listener); + edgeDetectButton.addActionListener(listener); + autoContrastButton.addActionListener(listener); + rotateClockwiseButton.addActionListener(listener); + rotateCounterClockwiseButton.addActionListener(listener); } @Override @@ -296,4 +324,3 @@ public void renderErrorMessage(String s) { -